/** * Cross-cutting run mechanics for an agent loop: * * 1. a **wall-clock deadline** that aborts a hung run (thread its signal into * every model and tool call, and re-check at the top of each iteration); * 2. a **coalesced progress heartbeat** for hosts that record liveness; * 3. a **failure classifier** mapping a thrown run to a terminal status * (`timed_out` vs `failed`). * * Any driver of the loop needs all three, and reimplementing them per driver * is how one ends up unbounded: a loop whose abort signal never fires holds its * worker slot until an external reaper notices, which is a stall the user sees * as an agent that never answers. * * This module owns no persistence and makes no model calls — it is pure * mechanics, so a small consumer can use it without pulling in a loop's * transitive graph. */ /** Thrown by {@link RunDeadline.throwIfTimedOut} once the wall-clock budget is * exhausted. Distinct from a caller-driven cancellation so callers can map it * to a `timed_out` terminal status (or simply read {@link RunDeadline.timedOut}). */ export declare class RunTimeoutError extends Error { readonly timeoutMs: number; constructor(timeoutMs: number, label: string); } /** * Call `unref()` on a timer when the runtime exposes it (Node/Bun) so a * forgotten clear can't hold the event loop open; a no-op under the DOM `number` * timer type. Feature-detected rather than `as`-cast to keep type safety. */ export declare function unrefTimer(timer: ReturnType): void; /** * A wall-clock budget for a single run. Backed by an `AbortController` that * fires after `timeoutMs`; `signal` is threaded into LLM/tool calls so an * in-flight upstream request is actually torn down on timeout (not merely * abandoned). `timedOut` reflects *this* deadline firing only — it stays false * when a combined external (cancellation) signal aborts — so it is a reliable * basis for classifying a `timed_out` outcome regardless of which error shape * surfaced (a `RunTimeoutError` from {@link throwIfTimedOut} or an `AbortError` * from a torn-down `callLLM` stream). */ export interface RunDeadline { /** The deadline's own abort signal. Combine with a cancellation signal via * {@link withExternal} before handing to `callLLM`. */ readonly signal: AbortSignal; /** True once this deadline's timer has fired. Unaffected by external signals. */ readonly timedOut: boolean; /** Throw {@link RunTimeoutError} if the budget is exhausted; no-op otherwise. * Call at the top of each loop iteration. */ throwIfTimedOut(): void; /** Combine this deadline with an optional external signal (e.g. a DB-backed * cancellation controller). Returns the deadline's own signal when no * external signal is given. */ withExternal(external?: AbortSignal | null): AbortSignal; /** Clear the underlying timer. Idempotent; call in a `finally`. */ dispose(): void; } export declare function createRunDeadline(opts: { timeoutMs: number; label?: string; }): RunDeadline; /** * Classify a run that ended by throwing into its terminal status. A run whose * deadline fired — or whose error is a {@link RunTimeoutError} — is `timed_out`; * anything else is `failed`. Checking the error too makes the result robust to * the surfaced shape (an `AbortError` from a torn-down stream leaves * `deadline.timedOut` true; a `RunTimeoutError` thrown between iterations is * caught directly even if a combined-signal edge left `timedOut` unread). * * Caller-driven cancellation is a distinct outcome and must be handled *before* * calling this (the executor special-cases `AgentRunCancelledError`); the * one-off/onboarding path has no cancellation, so this split is complete there. */ export declare function classifyRunFailure(deadline: RunDeadline, error?: unknown): "timed_out" | "failed"; /** A progress heartbeat that collapses bursts of calls into at most one flush * per `coalesceMs` window (a `force` beat always flushes). */ export interface CoalescedHeartbeat { beat(opts?: { force?: boolean; }): Promise; } /** * Build a coalesced heartbeat. A run with N concurrent tool calls would * otherwise fire N near-simultaneous run-row UPDATEs; coalescing collapses * them to one per window while a `force: true` beat (used at the end of each * iteration) guarantees a bump within the reaper's stale threshold. `flush` * owns the actual write; a flush failure is reported to `onError` and * swallowed so a transient DB hiccup never aborts the run. */ export declare function createCoalescedHeartbeat(opts: { coalesceMs: number; flush: () => Promise; onError?: (err: unknown) => void; }): CoalescedHeartbeat;