@sema-agent/core
Version:
Stateless, task-oriented AI agent core
79 lines (78 loc) • 6.11 kB
TypeScript
/**
* What the run loop derives from a tool call's END, in every leg that closes one: the `tool_end` frame
* body (the ONE projection the live loop, the resumed batch and the reconcile-recovered orphan all go
* through), the `toolResult` transcript message the resume engine closes pending calls with, the thin
* `response` digest the post-tool-batch observer is handed, and the canonical write-tool → reminder-window
* family the attachment reducer keys on when a write tool ends.
*
* Projections over values the caller already holds, with ONE clock read: `toolResultMsg` stamps the message
* it mints with `Date.now()` (as it did inside the driver — the transcript's timestamp is minted where the
* message is). The one callback here is the leg's card projector, handed in by every caller (it keeps the
* once-per-leg announce state; this module keeps none). Nothing here awaits.
* Layer-1 machinery, not a driven lane; the prefix rule it follows is written once, on initial-run-state.ts.
*/
import { type AgentMessage } from "../../internal/harness.js";
import type { GateOutcome } from "../gate-outcome.js";
import { type McpDelivered } from "../mcp-failure.js";
import type { RecoveredOrphan } from "../session-reconcile.js";
import { type StructuredProjector } from "./tool-output-projection.js";
import type { WriteFamily } from "./turn-attachments.js";
/** Derive the thin `response` digest from a harness tool result: model-facing text
* content only (never `details` — the H4 thin-projection discipline), capped with a truncation note. */
export declare function batchResponseDigest(result: unknown): string | undefined;
/** The `tool_end` body fields projected from a harness tool result — output/truncated/totalChars via
* {@link toolOutputFrom} and the CC card via the LEG's projector (`Prepared.structuredProjector`: the engine card set
* ∪ the caller rows' declared `renderHints.resultCards`, an undeclared type dropped and announced once per leg —
* handed in because the once-per-leg state lives on the leg. Single construction point for BOTH
* the live loop's frames and the resumed batch's frames (`resolvePendingCall` + the deferred-sibling
* close): the resumed frames used to carry only `isError`, so a client rendering tool output from frames
* showed an empty body for every durable-approved call. Same projection = same source as the transcript.
* Also the one place the gate outcome reaches a frame — see the parameter. */
export declare function toolEndBodyFrom(result: unknown, isError: boolean,
/** The leg's card projector — see the doc above. A parameter, never a module default: a body minted with the engine
* set alone would silently drop every declared caller card and never announce. */
structured: StructuredProjector,
/** The gate's record of the pass that admitted or refused this call, supplied by the CALLER of this
* projection — the live loop reads it off the gate's per-call sideband, the resumed leg off the decide's
* minted record. Deliberately a parameter and never derived from `result`: a tool's own `details`
* (which post-tool hooks may also replace) is writable by layers that adjudicate nothing, so reading
* provenance out of it would let a failing tool claim a person approved it. Omitted ⇒ the call never
* went through the gate (see the `tool_end.gate` doc, the one home). */
gate?: GateOutcome,
/** WHICH call this run's committed durable park is holding ({@link import("./park-commit.js").gatedCallIdOf}), for the frames the
* abort short-circuits. Same never-derived-from-`result` posture as the outcome above, and for the
* sharpest version of that reason: this one is an assertion about a DIFFERENT call, so a tool able to
* author it could point an approval UI at a call nobody is waiting on. Omitted ⇒ no park is holding a
* call (nothing parked, or the park that did holds none), and the frame then carries no id at all. */
gatedCallIdOfRun?: string): {
output?: unknown;
truncated?: boolean;
totalChars?: number;
structured?: unknown;
errorCode?: string;
delivered?: McpDelivered;
gatedCallId?: string;
gate?: GateOutcome;
};
/**
* scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
* construction point for BOTH minting legs (the live-abort loop at the end of a run, and the wake/crash
* leg's replay at run open), so the two can never disagree about the shape of the same event.
*
* Both frames used to carry `isError:true` and NOTHING else: a consumer rendering tool output from the
* event stream showed an EMPTY body for every interrupted call, even though the persisted transcript
* (which the model reads) carried the full `[INTERRUPTED]` explanation — the two faces of one call
* disagreed. The projection goes through the same {@link toolEndBodyFrom} every live tool result uses, so
* `output` = the persisted model-facing text and `errorCode` = the persisted `details.errorKind`
* (`interrupted_never_started` / `interrupted_outcome_unknown`) — a consumer discriminates on the code
* instead of prose-matching. No `structured`: the reconcile mints no CC card (no `details.type`), which the
* leg's projector already enforces.
*/
export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind">, structured: StructuredProjector): ReturnType<typeof toolEndBodyFrom>;
/** task #51 F2/F3 — the CANONICAL write-tool → reminder-window mapping (CC qFm :480253-480254
* `a.name === bD || a.name === hF` = TaskCreate/TaskUpdate; BFm :480217-480219 = TodoWrite). Live
* deployment-alias resolution happens at the call sites (run-local `writeFamilyOf` over the mounted
* roster); retired-name normalization is gone. */
export declare function writeFamilyOfCanonical(name: string): WriteFamily | undefined;
/** A `toolResult` transcript message (text content). Used by the resume engine to close pending calls. */
export declare function toolResultMsg(toolCallId: string, toolName: string, text: string, isError: boolean): AgentMessage;