/** * Sub-agent residual-observability recorder (黑板 [558] A/B/D — CC harness parity). * * A parent observing a child agent — especially one that was stopped mid-flight by a stream drop or * TaskStop — needs more than final text + terminal status to write a PRECISE resume prompt ("you * stopped at file X, step Y"). CC's harness feeds the parent a compact step tail + the child's edited * files + a one-line "currently doing" progress string. This recorder derives those three from the * child's live `TaskEvent` stream (the same `onForwardEvent` sink the display/tick lanes already tap), * so no new engine plumbing is needed — it is a pure observer. * * Scope: ONE direct child. A child that itself spawns a grandchild re-threads the same forward sink, * so grandchild events bubble through here too. Scoping is EXPLICIT, not first-come-first-served * ([426] CORE-2 — the old "first `sourceTaskId` seen" lock assumed direct-child content always precedes * any grandchild frame, an ordering the shared sink does not guarantee): * 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) so a pathological child can't * balloon the parent's notification/report. */ 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 "". */ 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; /** 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; /** * @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); /** * [426] 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; } /** * 黑板 [558] 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): SubagentStep[]; //# sourceMappingURL=subagent-steps.d.ts.map