@sema-agent/core
Version:
Stateless, task-oriented AI agent core
51 lines (50 loc) • 3.95 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 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;