@sema-agent/core
Version:
Stateless, task-oriented AI agent core
357 lines (356 loc) • 19.9 kB
JavaScript
import { SessionError, normalizeAnnouncedListing, normalizeReminderMark, normalizeGitAnnouncement, normalizeWorkspaceState, normalizeCompactionStateCarrier, isValidThinkingLevelChange, isValidModelChange, isOptionalDisplayString, } from "../harness/types.js";
import { leafIdAfterEntry } from "./storage-base.js";
import { parseSessionTimestampMs } from "./timestamps.js";
import { flattenableUserText, normalizeEngineSegments } from "../../core/untrusted-text.js";
import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js";
import { MAX_MINTED_TOOL_RESULT_REF_CHARS } from "../../core/tool-result-store.js";
import { PERSISTED_OUTPUT_REFS_MAX_ENTRIES, SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, } from "../compaction/utils.js";
const PATH_LIST_MAX_CHARS = 4096;
const PATH_LIST_MAX_ENTRIES = 1000;
const INVOKED_SKILLS_MAX_ENTRIES = 1000;
const INVOKED_SKILL_NAME_MAX_CHARS = 1024;
const PERSISTED_REF_MAX_CHARS = MAX_MINTED_TOOL_RESULT_REF_CHARS;
const ACTIVE_TOOL_NAME_MAX_CHARS = 1024;
const ACTIVE_TOOLS_MAX_ENTRIES = 1000;
export class StreamingImportValidator {
seen = new Set();
parentOf = new Map();
rootCount = 0;
runningLeaf = null;
done = false;
preserveActorAssertions;
constructor(options) {
this.preserveActorAssertions = options?.preserveActorAssertions === true;
}
step(entry) {
if (this.done) {
throw new SessionError("invalid_session", "StreamingImportValidator.step() called after finish()");
}
const e = entry;
if (this.seen.has(e.id)) {
throw new SessionError("invalid_session", `Duplicate entry id "${e.id}" in import log`);
}
if (e.parentId === null) {
this.rootCount++;
}
else if (!this.seen.has(e.parentId)) {
throw new SessionError("invalid_session", `Entry "${e.id}" references parent "${e.parentId}" that does not appear before it ` +
`(non-oldest-first order, or a truncated pre-floor chain — exportEntries must return the FULL log)`);
}
if (e.type === "compaction") {
if (!this.seen.has(e.firstKeptEntryId)) {
throw new SessionError("invalid_session", `Compaction "${e.id}" firstKeptEntryId "${e.firstKeptEntryId}" not present before it (truncated log)`);
}
let anc = e.parentId;
let ok = e.firstKeptEntryId === e.parentId;
const guard = new Set();
while (!ok && anc != null && !guard.has(anc)) {
guard.add(anc);
if (anc === e.firstKeptEntryId)
ok = true;
else
anc = this.parentOf.get(anc) ?? null;
}
if (!ok) {
throw new SessionError("invalid_session", `Compaction "${e.id}" firstKeptEntryId "${e.firstKeptEntryId}" is not an ancestor of it (a floor on a different branch makes every later read fail)`);
}
}
if ((e.type === "label" || e.type === "leaf") && e.targetId !== null && !this.seen.has(e.targetId)) {
throw new SessionError("invalid_session", `${e.type} entry "${e.id}" targets "${e.targetId}" which is not present before it (truncated log)`);
}
if (e.type === "message") {
validateMessageContentShape(e.message, `Entry "${e.id}"`);
normalizeEngineProvenanceStamp(e.message, this.preserveActorAssertions);
}
else if (e.type === "custom_message") {
validateContentShape(e.content, CUSTOM_CONTENT_BLOCK_TYPES, true, `Entry "${e.id}".content`);
}
else if (e.type === "prompt_epoch") {
const shaped = normalizePromptEpoch({
epoch: e.epoch,
artifactDigest: e.artifactDigest,
packId: e.packId,
assemblyApi: e.assemblyApi,
activatedBy: e.activatedBy,
});
if (!shaped) {
throw new SessionError("invalid_session", `prompt_epoch entry "${e.id}" is structurally invalid`);
}
}
else if (e.type === "announced_listing") {
if (!normalizeAnnouncedListing(e)) {
throw new SessionError("invalid_session", `announced_listing entry "${e.id}" is structurally invalid`);
}
}
else if (e.type === "workspace_state") {
if (!normalizeWorkspaceState(e)) {
throw new SessionError("invalid_session", `workspace_state entry "${e.id}" is structurally invalid`);
}
}
else if (e.type === "reminder_mark") {
if (normalizeReminderMark(e) === undefined) {
throw new SessionError("invalid_session", `reminder_mark entry "${e.id}" is structurally invalid`);
}
}
else if (e.type === "git_announcement") {
if (!normalizeGitAnnouncement(e)) {
throw new SessionError("invalid_session", `git_announcement entry "${e.id}" is structurally invalid`);
}
}
if (e.type === "thinking_level_change" && !isValidThinkingLevelChange(e)) {
throw new SessionError("invalid_session", `thinking_level_change entry "${e.id}" carries an invalid thinkingLevel`);
}
if (e.type === "model_change" && !isValidModelChange(e)) {
throw new SessionError("invalid_session", `model_change entry "${e.id}" carries an invalid provider/modelId`);
}
if (e.type === "message") {
const msg = e.message;
if (msg?.role === "assistant" && (msg.provider !== undefined || msg.model !== undefined)) {
if (!isValidModelChange({ provider: msg.provider, modelId: msg.model })) {
throw new SessionError("invalid_session", `assistant message "${e.id}" names an invalid provider/model for the branch's active model`);
}
}
}
if (parseSessionTimestampMs(e.timestamp) === undefined) {
throw new SessionError("invalid_session", `entry "${e.id}" carries an unparseable timestamp — every later buildSessionContext would throw`);
}
if (e.type === "label" && !isOptionalDisplayString(e.label)) {
throw new SessionError("invalid_session", `label entry "${e.id}" carries a non-string label`);
}
if (e.type === "session_info" && !isOptionalDisplayString(e.name)) {
throw new SessionError("invalid_session", `session_info entry "${e.id}" carries a non-string name`);
}
if (e.type === "compaction") {
const restated = e.details?.promptEpoch;
if (restated !== undefined && !normalizePromptEpoch(restated)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid promptEpoch restatement`);
}
const restatedListings = e.details?.announcedListings;
if (restatedListings !== undefined && !normalizeAnnouncedListing(restatedListings)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid announcedListings restatement`);
}
const restatedGit = e.details?.gitAnnouncement;
if (restatedGit !== undefined && !normalizeGitAnnouncement(restatedGit)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid gitAnnouncement restatement`);
}
const carrier = e.details;
const shaped = normalizeCompactionStateCarrier(e.details);
if (carrier?.thinkingLevel !== undefined && shaped.thinkingLevel === undefined) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid thinkingLevel restatement`);
}
if (carrier?.model !== undefined && shaped.model === undefined) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid model restatement`);
}
const paths = e.details;
for (const field of ["modifiedFiles", "modifiedFilesByRecency", "readFiles"]) {
const v = paths?.[field];
if (v === undefined)
continue;
if (!Array.isArray(v) || v.some((p) => typeof p !== "string" || p.length === 0 || p.length > PATH_LIST_MAX_CHARS)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid ${field} list`);
}
if (v.length > PATH_LIST_MAX_ENTRIES) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a ${field} list of ${v.length} paths (max ${PATH_LIST_MAX_ENTRIES})`);
}
}
const skills = e.details?.invokedSkills;
if (skills !== undefined) {
if (!Array.isArray(skills)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid invokedSkills list`);
}
if (skills.length > INVOKED_SKILLS_MAX_ENTRIES) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries ${skills.length} invokedSkills entries (max ${INVOKED_SKILLS_MAX_ENTRIES})`);
}
let totalChars = 0;
for (const s of skills) {
const entry = s;
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid invokedSkills entry`);
}
if (typeof entry.name !== "string" || entry.name.length === 0 || entry.name.length > INVOKED_SKILL_NAME_MAX_CHARS) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries an invokedSkills entry with an invalid name (must be a non-empty string of at most ${INVOKED_SKILL_NAME_MAX_CHARS} chars)`);
}
if (typeof entry.content !== "string" || entry.content.length > SKILL_RETENTION_PER_SKILL_MAX_CHARS) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries an invokedSkills entry "${entry.name}" whose content is not a string of at most ${SKILL_RETENTION_PER_SKILL_MAX_CHARS} chars`);
}
totalChars += entry.content.length;
}
if (totalChars > SKILL_RETENTION_TOTAL_MAX_CHARS) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries an invokedSkills area of ${totalChars} chars (max ${SKILL_RETENTION_TOTAL_MAX_CHARS})`);
}
}
const elided = e.details?.elidedMessages;
if (elided !== undefined && !(typeof elided === "number" && Number.isSafeInteger(elided) && elided >= 0)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid elidedMessages count (non-negative integer)`);
}
const uncovered = e.details?.unsummarizedMessages;
if (uncovered !== undefined) {
if (!(typeof uncovered === "number" && Number.isSafeInteger(uncovered) && uncovered >= 0)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid unsummarizedMessages count (non-negative integer)`);
}
if (uncovered > 0 && !(typeof elided === "number" && elided >= uncovered)) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries unsummarizedMessages ${uncovered} without an elidedMessages count at least that ` +
`large (uncovered messages are a subset of the folded ones)`);
}
}
const refs = e.details?.persistedOutputRefs;
if (refs !== undefined) {
if (!Array.isArray(refs) || refs.length > PERSISTED_OUTPUT_REFS_MAX_ENTRIES) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid persistedOutputRefs list (array of at most ${PERSISTED_OUTPUT_REFS_MAX_ENTRIES} refs)`);
}
for (const r of refs) {
if (typeof r !== "string" || r.length === 0 || r.length > PERSISTED_REF_MAX_CHARS) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a persistedOutputRefs entry that is not a non-empty string of at most ${PERSISTED_REF_MAX_CHARS} chars`);
}
}
}
const activeToolsCarrier = e.details?.activeTools;
if (activeToolsCarrier !== undefined) {
if (!Array.isArray(activeToolsCarrier) || activeToolsCarrier.length > ACTIVE_TOOLS_MAX_ENTRIES) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid activeTools list (array of at most ${ACTIVE_TOOLS_MAX_ENTRIES} names)`);
}
for (const n of activeToolsCarrier) {
if (typeof n !== "string" || n.length === 0 || n.length > ACTIVE_TOOL_NAME_MAX_CHARS) {
throw new SessionError("invalid_session", `compaction "${e.id}" carries an activeTools entry that is not a non-empty string of at most ${ACTIVE_TOOL_NAME_MAX_CHARS} chars`);
}
}
}
}
this.parentOf.set(e.id, e.parentId);
this.seen.add(e.id);
this.runningLeaf = leafIdAfterEntry(e);
}
finish() {
if (this.done) {
throw new SessionError("invalid_session", "StreamingImportValidator.finish() called twice");
}
this.done = true;
if (this.seen.size === 0) {
return { leafId: null };
}
if (this.rootCount !== 1) {
throw new SessionError("invalid_session", `A session log must have exactly ONE root entry (parentId null); found ${this.rootCount}`);
}
if (this.runningLeaf !== null && !this.seen.has(this.runningLeaf)) {
throw new SessionError("invalid_session", `Resolved leaf "${this.runningLeaf}" is not present in the import log`);
}
return { leafId: this.runningLeaf };
}
}
export function validateEntriesForImport(entries, options) {
const protectedEntries = entries.map((e) => e.type === "message" && typeof e.message === "object" && e.message !== null && !Array.isArray(e.message)
? { ...e, message: { ...e.message } }
: e);
const v = new StreamingImportValidator(options);
for (const e of protectedEntries)
v.step(e);
v.finish();
return protectedEntries;
}
const TEXT_IMAGE_BLOCK_TYPES = new Set(["text", "image"]);
const CUSTOM_CONTENT_BLOCK_TYPES = TEXT_IMAGE_BLOCK_TYPES;
const CONTENT_BLOCK_TYPES_BY_ROLE = new Map([
["user", TEXT_IMAGE_BLOCK_TYPES],
["assistant", new Set(["text", "thinking", "toolCall"])],
["toolResult", new Set(["text", "image", "document"])],
["custom", CUSTOM_CONTENT_BLOCK_TYPES],
]);
const STRING_CONTENT_ROLES = new Set(["user", "custom"]);
function describeShape(value) {
if (value === null)
return "null";
if (Array.isArray(value))
return "an array";
if (typeof value === "object") {
const keys = Object.keys(value).slice(0, 8).join(", ");
return `a bare object (keys: ${keys || "none"})`;
}
return `a ${typeof value}`;
}
function normalizeEngineProvenanceStamp(message, preserveActorAssertions = false) {
if (message === null || typeof message !== "object")
return;
const m = message;
if (m.role !== "user")
return;
if (m.actor !== undefined) {
const a = m.actor;
const wellFormed = preserveActorAssertions &&
a !== null &&
typeof a === "object" &&
typeof a.id === "string" &&
a.id.length > 0 &&
typeof a.hostAsserted === "boolean" &&
(a.issuer === undefined || (typeof a.issuer === "string" && a.issuer.length > 0));
if (wellFormed) {
m.actor = { id: a.id, hostAsserted: a.hostAsserted, ...(a.issuer !== undefined ? { issuer: a.issuer } : {}) };
}
else {
delete m.actor;
}
}
if (m.engineMinted !== undefined && m.engineMinted !== true)
delete m.engineMinted;
if (m.provenance !== undefined && m.provenance !== "engine-note")
delete m.provenance;
if (m.engineNoteKey !== undefined && !(typeof m.engineNoteKey === "string" && m.engineNoteKey.length > 0))
delete m.engineNoteKey;
if (m.engineNoteKey !== undefined && m.provenance !== "engine-note")
delete m.engineNoteKey;
if (m.engineMinted === true && m.provenance === "engine-note") {
delete m.engineMinted;
}
if (m.enginePrefixChars !== undefined &&
!(typeof m.enginePrefixChars === "number" && Number.isInteger(m.enginePrefixChars) && m.enginePrefixChars > 0)) {
delete m.enginePrefixChars;
}
if (m.engineSegments !== undefined) {
const flat = flattenableUserText(m.content);
const norm = flat === undefined ? undefined : normalizeEngineSegments(flat, m.engineSegments);
if (norm !== undefined && norm.state === "valid") {
m.engineSegments = norm.segments;
}
else if (norm === undefined || norm.state === "malformed") {
delete m.engineSegments;
delete m.enginePrefixChars;
}
}
}
function validateMessageContentShape(message, path) {
if (message === null || typeof message !== "object" || Array.isArray(message)) {
throw new SessionError("invalid_session", `${path}.message: must be a message object; got ${describeShape(message)}`);
}
const role = message.role;
if (typeof role !== "string") {
throw new SessionError("invalid_session", `${path}.message.role: must be a string; got ${describeShape(role)}`);
}
const allowed = CONTENT_BLOCK_TYPES_BY_ROLE.get(role);
if (allowed === undefined) {
return;
}
validateContentShape(message.content, allowed, STRING_CONTENT_ROLES.has(role), `${path}.message(${role}).content`);
}
function validateContentShape(content, allowed, allowString, path) {
if (typeof content === "string") {
if (allowString)
return;
throw new SessionError("invalid_session", `${path}: must be an array of typed content blocks; got a string`);
}
if (!Array.isArray(content)) {
throw new SessionError("invalid_session", `${path}: must be ${allowString ? "a string or " : ""}an array of typed content blocks; got ${describeShape(content)}`);
}
for (let i = 0; i < content.length; i++) {
const block = content[i];
if (block === null || typeof block !== "object" || Array.isArray(block)) {
throw new SessionError("invalid_session", `${path}[${i}]: content block must be an object with a "type"; got ${describeShape(block)}`);
}
const t = block.type;
if (typeof t !== "string") {
throw new SessionError("invalid_session", `${path}[${i}]: content block is missing a string "type"; got ${describeShape(block)} ` +
`(e.g. a raw tool output object written directly as a block)`);
}
if (!allowed.has(t)) {
throw new SessionError("invalid_session", `${path}[${i}]: unknown content block type "${t}" (allowed here: ${[...allowed].join("/")})`);
}
}
}