@sema-agent/core
Version:
Stateless, task-oriented AI agent core
116 lines (115 loc) • 7.45 kB
TypeScript
/**
* Sub-agent residual-observability recorder (lanes A/B/D — CC harness parity): a parent resuming or judging a stopped
* child gets a compact step tail, the child's edited files and a one-line "currently doing" string, derived from the
* child's live `TaskEvent` stream (the `onForwardEvent` sink the display / tick lanes tap) — a pure observer.
*
* Scope: ONE direct child. A grandchild re-threads the same forward sink, and the sink does not order direct-child
* content before grandchild frames, so scoping is EXPLICIT:
* 1. the spawn site calls {@link SubagentStepRecorder.lockTo} the moment the child's taskId is known
* (fork lanes: the forked sessionId; retained lanes: the pre-minted child sessionId) — after that,
* only `sourceTaskId === lockedSource` frames are recorded;
* 2. lanes where the child id is minted inside `runTask` (plain sync / non-retained background) anchor
* by FRAME PAIRING instead: the runner stamps `sourceTaskId` + `parentToolCallId` together on every
* child content event, and the DIRECT child's `parentToolCallId` is THIS delegation's own tool-call
* id (a grandchild's frames carry the child's delegation id) — so the provisional lock only seeds
* from a matching frame;
* 3. without either anchor (bare construction — tests/legacy), the first-source lock applies as before.
* Everything is BOUNDED (step ring ≤ {@link STEP_CAP}, fields truncated): a pathological child cannot balloon the parent's view.
*/
import type { TaskEvent } from "../core/types.js";
/** One completed tool step in the child's tail — enough for the parent to say "you stopped at X". */
export interface SubagentStep {
/** Tool name (or its distinct {@link ToolSpec.label} when set). */
tool: string;
/** The step's primary argument, one line: file path / command / pattern. May be "". */
target: string;
/** Result first line — or `error: …` when the tool failed. May be "" (a result that was only an engine
* reminder block has no line of its own). The engine's marked reminder blocks are not part of a result. */
outcome: string;
}
/** A file the child mutated, with how many times (Write/Edit/MultiEdit/NotebookEdit). */
export interface SubagentEditedFile {
path: string;
edits: number;
}
/** Keep the last N tool steps (CC's tail is ~this deep; older steps are rarely load-bearing for resume). */
export declare const STEP_CAP = 10;
/** Hard-bound a line to `max` UTF-16 units — a pure prefix, no marker appended. When the cut would split a
* redaction marker, it moves back to the marker's start (the bound holds, and the output never shows a
* marker fragment); when it would split a surrogate pair, it moves back one unit (never a lone surrogate). */
export declare function cutAt(line: string, max: number): string;
/**
* Redact, THEN bound — the order every display cut in this module (and the runner's `steering_injected`
* previews) uses. Exported so a preview minted elsewhere gets the same marker-aware cut rather than a
* raw `slice` that can leave a half marker at the bound. Keeps newlines: a multi-line body is the
* caller's shape; only the bound and the redaction are this function's.
*/
export declare function redactThenCut(s: string, max: number): string;
/** The tool's primary argument as a single short line (best-effort; unknown shapes → a compact JSON head). */
export declare function extractTarget(args: unknown): string;
/**
* Observes a single child's forwarded `TaskEvent`s and derives the residual-observability projection.
* Feed it every event via {@link record}; read {@link recentSteps}/{@link editedFiles}/{@link currentAction}
* at report/notify/tick time. Swallow-safe: `record` never throws (an observer must never fault a run).
*/
export declare class SubagentStepRecorder {
private lockedSource;
private lockedResolved;
private readonly parentToolCallId;
private readonly steps;
private readonly pending;
private readonly edits;
private action;
private actionTool;
private actionTarget;
/**
* @param parentToolCallId THIS delegation's own tool-call id (`ctx.toolCallId` — the exact value the
* spawn threads as `RunInternals.parentToolCallId`, which the runner stamps back onto every direct-child
* content event). Used to seed the provisional lock ONLY from a direct-child frame while no explicit
* {@link lockTo} anchor is set. Optional: absent falls back to the legacy first-source lock.
*/
constructor(parentToolCallId?: string);
/**
* CORE-2 — explicitly anchor the observed DIRECT child by its taskId (= its sessionId on every
* delegation lane: `buildChildSpec` never sets `spec.taskId`). Call as soon as the id is known at the
* spawn site. If events arrived BEFORE the anchor and the provisional lock caught a DIFFERENT source
* (a grandchild racing through the shared sink), everything recorded so far belongs to the wrong
* descendant — reset it and re-lock, so the projection never mixes generations.
*/
lockTo(taskId: string): void;
/** Drop everything recorded under a mis-locked source (see {@link lockTo}). */
private resetRecorded;
/** True when the event belongs to the observed direct child (see the class doc for the 3-tier anchor). */
private inScope;
record(e: TaskEvent): void;
/** The child's last ≤{@link STEP_CAP} completed tool steps; undefined if none observed. */
recentSteps(): SubagentStep[] | undefined;
/** Files the child mutated with edit counts; undefined if none. */
editedFiles(): SubagentEditedFile[] | undefined;
/** The child's most recent tool intent as one human line ("Bash npm test", "Edit src/x.ts"); undefined if none. */
currentAction(): string | undefined;
/** {@link currentAction} pre-formatted into a fleet-view Progress-section
* shape — the SAME `{tool, target}` pair `currentAction` already concatenates into one line, exposed
* separately so a consumer can look the tool up in its own registry instead of parsing prose.
* Undefined if none observed (mirrors {@link currentAction}'s own undefined case exactly). */
currentActionStructured(): {
toolName: string;
target?: string;
} | undefined;
}
/** One step read back from a session's persisted messages: a {@link SubagentStep} plus WHEN. */
export interface TranscriptStep extends SubagentStep {
/** Epoch ms: the result message's timestamp when the call has a result, the calling message's when it
* has none. Absent when that message carries no finite timestamp. */
at?: number;
/** Present (`true`) exactly when the transcript holds the call but no result for it — the call is still
* in flight, or the run stopped before its result was recorded. */
inFlight?: true;
}
/**
* Residual lane C — derive the last `lastN` tool steps from a session's PERSISTED messages (the pull-model
* `AgentTranscript` read face). Mirrors {@link SubagentStepRecorder} but sources from committed history
* instead of the live event stream, so a parent can retrospectively read a child's tail on demand. Walks
* assistant `toolCall`s paired with the following `toolResult`, oldest→newest, then keeps the last N.
*/
export declare function stepsFromMessages(messages: readonly unknown[], lastN: number): TranscriptStep[];