UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

43 lines (42 loc) • 3.33 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 LANE OWNS TIME, 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 one origin per call, because the two * producers do not start their clocks together — the station registers a call when it ENTERS EXECUTION, 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 emitted FOR 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. * * 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"; /** 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) and its identity mint. */ export declare function createToolProgressLane(push: (e: TaskEvent) => void, ident: () => TaskEventIdentity): ToolProgressLane;