UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

108 lines (107 loc) • 7.89 kB
/** * ONE TOOL CALL ATTEMPT, ONE RECORD — the occurrence. * * A tool call on one leg (one `toolCallId`, one attempt) has a single record here, and everything the engine * says about the call is read off it: the `tool_start` / `tool_end` frames (projected in `tool-end-body.ts`), the * `tool_progress` heartbeat (`tool-progress-lane.ts`), the trace row's `ok`, and the resumed leg's * `executionOutcome` filing. The record has three parts: * * - the EXECUTION LIFE, monotonic: `announced` (the start frame is out) → `released` (the engine let the call go * to its executor: the resumed leg right before it runs the tool body itself, a live leg at the gate's allow * exit — the loop starts the body after it and the runner does not observe that moment) → `settled` or * `interrupted`. A call that closes still `announced` was never let go: whatever the tool's body did, this call * did not do it (`refusedBeforeRelease`, `tool-not-executed.ts`). Leaving a terminal state is an engine defect and throws; * - the WAIT ON A PERSON, re-entrant: every ask put to the person's seat for this call counts one entry and one * exit (several rounds of one gate pass, a confirmation asked during the execution) — a count, never a bit, * because the waits nest and overlap; * - the GATE ROUNDS: one record per gate exit for this call, and one more when a station AFTER the gate refuses * it (the shell read admission that re-establishes what the gate judged, a resumed leg's belts). The latest * record is the call's disposition: a refusal after the gate is a refusal, never a tool failure under an * `allowed` record. * * WHAT IS NOT HERE: frames, clocks of a lane, the deny observer. This module is the record and its transitions; * the projections read it. A durable park that crosses legs is two occurrences on two tables (the parking leg's, * the resumed leg's), joined by the checkpoint row the resumed leg was opened from — never by a field here. */ import type { DeniedBy, GateOutcome } from "../gate-outcome.js"; /** The one clock the occurrence's heartbeat facts are stamped on. MONOTONIC for the reason the progress lane * states: every reading is subtracted from another, and a wall-clock step backwards would silence the beat. A * host without `performance` falls back to the wall clock and inherits its rollback behaviour. */ export declare const occurrenceClock: () => number; /** Where a call's execution life stands — see the module header for the order. */ export type OccurrenceLife = "announced" | "released" | "settled" | "interrupted"; /** What opens an occurrence: the facts its start frame carries. */ export interface OccurrenceStart { readonly toolCallId: string; /** The name the frames carry — the model's spelling on a live leg, the parked row's spelling on a resumed one. */ readonly spelledName: string; /** The name of the tool the call dispatches to on the live roster — the name every gate station judged (absent when * the station opening the record does not know it). */ readonly wireName?: string; /** The tool's display label, when it differs from the name. */ readonly label?: string; /** The arguments the start frame carries (for a recovered start: the call's own input as the transcript holds it). */ readonly args: unknown; } /** The record, as its readers see it. Written only through {@link ToolOccurrenceTable}. */ export interface ToolOccurrence extends OccurrenceStart { /** 1 for the first attempt of this id on this table, counting re-announcements of a reused id. */ readonly attempt: number; readonly life: OccurrenceLife; /** How many asks put to the person for this call are open right now. */ readonly awaitingUser: number; /** One outcome per gate exit, plus one per refusal after the gate; the last one is the call's disposition. */ readonly gateRounds: readonly GateOutcome[]; /** The input the latest gate exit decided on, as a FROZEN snapshot (absent when no gate saw the call). */ readonly finalInput?: unknown; /** The call's start frame is out on this leg. */ readonly startEmitted: boolean; /** When the call was announced, on {@link occurrenceClock} — the heartbeat's elapsed origin. */ readonly startedAt: number; /** The last moment anything was heard from the call (a frame for it, or its return from a wait on the person). */ readonly lastHeardAt: number; /** The heartbeat covers it. Cleared when the loop leaves the call behind at a turn end with no end of its own: * the record stays (a later settlement must still know its start frame went out) but no beat describes it. */ readonly beating: boolean; } /** The table of one leg's occurrences — the record's only writer. */ export interface ToolOccurrenceTable { /** The live (unsettled) attempt for this id, if any. */ get(toolCallId: string): ToolOccurrence | undefined; /** Every live attempt, in the order they were opened. */ live(): readonly ToolOccurrence[]; /** Open a new attempt. A live earlier attempt for the same id is superseded: the loop left it behind without an * end (a reused id can only be re-announced after its earlier call stopped producing events), and nothing can * settle it any more. */ open(start: OccurrenceStart): ToolOccurrence; /** The live attempt for this id, opened from `start` when there is none — a call the engine is told about * before it was announced (a frame the loop's own start never preceded). */ ensure(start: OccurrenceStart): ToolOccurrence; /** The call's start frame is out. */ noteStartEmitted(toolCallId: string): void; /** The engine lets the call go to its executor — the one transition into `released`, whichever leg makes it. */ release(toolCallId: string): void; /** A gate exit for this call: its record and the input it decided on (frozen by the gate's exit). */ gateRound(toolCallId: string, outcome: GateOutcome, finalInput?: unknown): void; /** A station after the gate refused the call: one more gate record, minted as a veto of whatever the latest one * settled (a person's yes stays on the settlement; the refusing layer is the disposition). Returns the record. */ refuse(toolCallId: string, layer: DeniedBy): GateOutcome; /** An ask for this call is put to the person: the count rises, and the returned `resumeFromUser` lowers it once * (a second call is a no-op). The pair is bound to THIS attempt — a return after the call ended, or after a * reused id re-opened, touches nothing — and an ask for a call this leg does not hold counts nothing: the wait * is the person's, and no bookkeeping here may turn their answer into a failure. */ awaitUser(toolCallId: string): () => void; /** The one bracket every asker uses: the count rises before `ask` puts its question and lowers when the answer * settles, either way. Returns `ask`'s own value, so the asker awaits exactly what it awaited before; a synchronous * answer (or anything that is not a promise) was no wait at all. */ whileAwaitingUser<T>(toolCallId: string, ask: () => T): T; /** A frame for the call went out: its silence clock restarts. */ heard(toolCallId: string): void; /** The call ended: it leaves the table and the record is returned for the end projection. */ close(toolCallId: string, life: "settled" | "interrupted"): ToolOccurrence | undefined; /** The heartbeat stops covering every live attempt (turn end, leg teardown). Records stay. */ quiesce(): void; /** Called whenever an attempt opens (the heartbeat arms its timer). One listener — the leg's one lane. */ onOpen(listener: () => void): void; } export declare function createToolOccurrenceTable(): ToolOccurrenceTable;