@sema-agent/core
Version:
Stateless, task-oriented AI agent core
43 lines (42 loc) • 3.33 kB
TypeScript
/**
* 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;