@sema-agent/core
Version:
Stateless, task-oriented AI agent core
65 lines (64 loc) • 4.61 kB
TypeScript
/**
* 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[];