UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

357 lines (356 loc) • 19.9 kB
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("/")})`); } } }