UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

51 lines (50 loc) • 3.95 kB
/** * The run's TOOL-PROGRESS lane — the single mint of the `tool_progress` frame: its clock, its cadence, * and the heartbeat that keeps a silent long-running tool call visible. * * TWO PRODUCERS, ONE MINT. A tool that observes its own work ticks fast and carries what it produced: the * shell's execute leg watches its command's output and calls `emit` about once a second. Every OTHER call is * opaque here — the runner knows only that it started and has not ended — so this lane runs a slow beat for * any in-flight call silent for {@link TOOL_HEARTBEAT_MS}. * * THE CALLS IN FLIGHT ARE THE LEG'S OCCURRENCES (`tool-occurrence.ts`), not a set of this lane's own: the record * that carries a call's start frame carries its elapsed origin, its silence clock and its wait on the person, so * the beat can never describe a call the frames say ended, nor miss one they say started. * * ONE ORIGIN PER CALL, the producer owns FACTS. A producer reports what it has seen (the tail, the totals), * never how long it has run: `elapsedTimeSeconds` is computed HERE from the occurrence's origin — its * announcement — because a shell command's own clock could only start at its spawn. With two origins a 30 s * beat could be followed moments later by a shell tick reporting 1 s, breaking the monotonicity the frame promises. * * THE CADENCE HAS ONE ENFORCEMENT POINT, same reason: a producer's timer is a SAMPLING clock, not the * contract. `emit` drops a frame landing inside {@link TOOL_PROGRESS_MIN_INTERVAL_MS} of that call's last. * * ONE RULE FOR THE BEAT: a call is beaten when nothing has been heard FROM IT for a whole interval, * counting its own start as the first such event — covering both "it only just started" and "the tool * already ticks faster than this". The interval is the RUN's, so a first beat lands anywhere in * [TOOL_HEARTBEAT_MS, 2 × TOOL_HEARTBEAT_MS): the price of one timer per run, stated rather than hidden. * And the beat reads the one activity fact: when EVERY call in flight is waiting on the person, nothing is * working and nothing is beaten; a call back from the person is heard from at that moment, so its silence * is counted afresh. * * THE CLOCK IS MONOTONIC: every predicate here is a DIFFERENCE of two readings (is this call due a beat, is * this offer inside the floor, how long has it run), and off the wall clock a backward step — NTP, a * container's clock settling after a resume — makes each difference negative and silences BOTH producers * until it catches up: the exact failure this lane exists to prevent. * * LIVENESS ONLY — see the `tool_progress` member's contract in `task-event.ts`. The timer is `unref`'d and * every emission is fault-contained. */ import type { TaskEvent, TaskEventIdentity } from "../types.js"; import type { ToolProgressLane } from "./contracts.js"; import { type ToolOccurrenceTable } from "./tool-occurrence.js"; /** How long a tool call may stay silent before this lane beats for it, in ms. ONE SOURCE, and CC * 2.1.259's own per-tool heartbeat interval (`Cct`) — this engine keeps the cadence and folds the beat * into the one progress frame instead of minting a second shape (registered divergence). */ export declare const TOOL_HEARTBEAT_MS = 30000; /** The floor between two frames of ONE call — the `tool_progress.cadence` contract, enforced here and * nowhere else so it holds across producers rather than inside each one. */ export declare const TOOL_PROGRESS_MIN_INTERVAL_MS = 1000; /** Build the lane over the run's content pusher (`pushContent` — the ONE seat that puts a frame on this * run's queue and, for a sub-agent, forwards it to the parent's sink), its identity mint and the leg's * occurrence table (the calls in flight). */ export declare function createToolProgressLane(push: (e: TaskEvent) => void, ident: () => TaskEventIdentity, occurrences: ToolOccurrenceTable): ToolProgressLane;