UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

65 lines (64 loc) • 4.61 kB
/** * Rebuild the hands' read-before-write state from a session's own transcript (ruled 2026-08-05). * * `readFileState` is minted per TASK and, until this module existed, was only ever re-seeded from a * durable-resume checkpoint. An ordinary session CONTINUATION — a new `runTask` carrying the previous * `sessionId` — therefore started blind: turn 1 Read the file, turn 2 was told "File has not been * read yet", and every file cost a re-read round-trip on every turn of a long conversation. * * The transcript already records what is needed, so no new persisted surface is minted here: the * tool results carry the file's whole text in their `details` card. This module walks those records * and reports, per file, the LAST content the transcript can prove — the caller turns each into a * read-state entry whose staleness hash is computed from THAT text. * * Two rules make the reconstruction safe rather than merely convenient: * * 1. **The content never comes from disk.** Every hash the caller derives from these records is a * hash of what the transcript recorded. A file changed between the turns (by the user, a linter, * a `git checkout`, a bash command — anything this transcript does not record) therefore fails * the staleness comparison and the edit is refused, exactly as it is inside a single task. A * reconstruction that re-read the file to build its hash would instead wave the dirty edit * through; that is the one thing this module must never do. (This is * deliberately STRICTER than CC's own rehydrate, whose Edit arm re-reads the disk and would * admit a between-turns external change — same gate satisfaction, no staleness give-up.) * 2. **Only records whose full post-condition is PROVABLE qualify.** A whole-file `Read` and a * `Write` carry the text verbatim. An `Edit` card carries a diff BASE — the * pre-edit whole text plus the resolved mutations — so its post-condition is RECOMPUTED through * the same single-source application step the tool itself used (`applyRecordedEdit`); a card * that cannot recompute (malformed, offloaded, or its oldString absent from its own base) falls * back to RETRACTING the file's record, never to a guess. A partial read or a notebook * projection proves nothing and is simply not reported: the gate then behaves exactly as before * (the model re-reads), which is the pre-existing cost, not a new failure. */ import type { AgentMessage } from "../../internal/harness.js"; /** The whole text of one file at the moment the transcript recorded it. */ export interface TranscriptFileRecord { /** The path as the tool recorded it — absolute (see {@link wholeFileRecordsFromTranscript}). */ path: string; /** The file's ENTIRE text at that moment, in the decoded coordinate the fs tools hash. */ content: string; /** The recording tool result's own timestamp — the most honest "last read at" available. */ at: number; } /** * Per-file, the last whole-file content the transcript proves — in transcript order, last record wins. * * Records are read only from SUCCESSFUL results (an `isError` result changed nothing and proves * nothing). A `Write` proves the post-condition it just persisted; a whole-file `Read` proves what it * just served. An `Edit` card proves the post-condition RECOMPUTED from its recorded * diff base + mutations — or, when it cannot prove (malformed / offloaded / inconsistent), retracts * whatever an earlier record said (leaving a stale entry behind would blame the user for the agent's * own edit, "modified since read", the next turn). A `NotebookEdit` always retracts (deliberate * residual — see {@link RETRACTING_RESULTS}). Anything else (partial reads, dedup stubs, notebook * projections, image/document reads, foreign tools) is inert: it neither proves nor retracts, * because reading a file does not change it. * * The parameter is the session's own message lane (`AgentMessage`, custom lanes included) rather than * the narrowed model-facing union: this reads the branch as the session stores it, and every lane that * is not a tool result falls out on the first line of the loop. * * Paths are reported only when the tool recorded an ABSOLUTE one. A relative spelling was resolved * against the tracked working directory of that moment, which this task cannot reconstruct; re- * resolving it here could key the entry to a different file. */ export declare function wholeFileRecordsFromTranscript(messages: readonly AgentMessage[]): TranscriptFileRecord[];