agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
158 lines (157 loc) • 9.46 kB
TypeScript
/**
* abort — the ONE owner of how the library honours a cancellation.
*
* Pattern: Small pure helpers over `AbortSignal`. No state between calls.
* Role: Leaf. Every provider adapter, the provider decorators and the
* agent's own provider calls stop through these, so "the caller
* cancelled" means the same thing at every layer. Two adapters
* (Ollama, Foundry Local) each kept a private copy of the first
* three helpers and the classifier kept a third; most adapters kept
* none, and an aborted run left their remote calls running — and
* billing. `test/architecture/abortOwner.test.ts` refuses a fourth.
* Emits: N/A.
*
* ── THE LAW ──────────────────────────────────────────────────────────────
* Once a request's signal has fired:
* 1. nothing more is SENT — `throwIfAborted` before every dispatch, because
* an already-aborted signal never dispatches another 'abort' event, so a
* listener alone cannot see it;
* 2. nothing more is WAITED FOR — `untilAborted` races every wait (the
* response, a body read, a stream's next chunk) against the signal, so a
* transport that ignores its signal still cannot hold the call open;
* 3. nothing more is READ — `abortableIterable` closes the source (its
* iterator's `return()`, plus the caller's own `close`, e.g. an SDK
* stream's `abort()`), so the socket is released and the server stops
* generating for nobody;
* 4. nothing that arrives LATE counts — a response or chunk that lands after
* the abort is refused with the abort, never returned as a success, so it
* cannot be recorded as an answer or billed into a run's usage. "After"
* means after the abort was SEEN: a read that settled a moment before the
* abort, but whose reader had not yet run, is refused too — the signal is
* checked again after every wait, not only raced during it.
*
* And a CLOSE is a wait like any other: a consumer that stops early waits for
* the transport to finish closing while the signal is live; once it fires,
* that wait ends with the abort and the caller's own close runs, so the
* transport is shut either way. The abort wins whenever it is seen before the
* close's result is handed back — even if the source finished closing a moment
* earlier.
*
* ── WHAT AN ABORT REACHES THE CALLER AS ──────────────────────────────────
* The signal's own `reason` when it is an `Error` — what every runtime
* supplies (a `DOMException` named 'AbortError', or 'TimeoutError' for
* `AbortSignal.timeout`), and what a caller who passed their own reason
* expects back. Anything else becomes an `Error` named 'AbortError' that keeps
* the reason as `cause`. An abort is the caller's word: it is never dressed up
* as a provider failure, never retried, never fallen back from.
*/
/**
* The error an aborted wait rejects with. See the module notes.
*
* Never throws. A reason that cannot even be classified (a revoked `Proxy`,
* one whose `getPrototypeOf` throws) becomes a plain `AbortError` WITHOUT it:
* this runs inside abort listeners, where a throw would escape as an uncaught
* exception and leave the wait it should end unsettled.
*/
export declare function abortErrorOf(signal: AbortSignal): Error;
/** Stop right here when `signal` has already fired. */
export declare function throwIfAborted(signal: AbortSignal | undefined): void;
/**
* `pending`, except that an abort ends the wait at once — with the abort,
* whatever `pending` later does. The abandoned promise's own rejection is
* swallowed, so the work given up on never surfaces as an unhandled rejection.
* With no signal, `pending` itself.
*/
export declare function untilAborted<T>(pending: T | PromiseLike<T>, signal: AbortSignal | undefined): Promise<T>;
/**
* A signal that fires when ANY of `signals` does, with that signal's reason.
* `undefined` when none is given, the one signal itself when only one is.
*
* `AbortSignal.any` where the runtime has it (Node 20.3+, every current
* browser): it holds its sources weakly, so a long-lived signal combined into
* every call does not collect a listener per call. The fallback (an older
* browser) listens until the first source fires, so the library's hot paths do
* not lean on it: a run's signal and a call's deadline are combined with
* {@link linkedController}, whose links end with the run or the call.
*/
export declare function eitherSignal(...signals: readonly (AbortSignal | undefined)[]): AbortSignal | undefined;
/**
* A controller of the caller's own, following `sources` until `unlink()` —
* for work that must be cancellable by the caller's signals AND by itself (a
* deadline), for exactly that work's lifetime. Aborted at once when a source
* already has; aborted with a source's reason when one fires; `unlink()` takes
* every listener off the sources, so nothing outlives the work on a
* long-lived signal. Idempotent.
*/
export declare function linkedController(...sources: readonly (AbortSignal | undefined)[]): {
readonly controller: AbortController;
readonly unlink: () => void;
};
/**
* Abort `controller` with `reason()` once `ms` have passed — a DEADLINE, not a
* wait (`sleep.ts` owns waits). Returns the disarm: a deadline is released
* when the work it bounds settles, as the engine releases its own, so it never
* fires on work that already finished. A delay past what a timer holds
* (2^31 − 1 ms, about 24.8 days) is held at that maximum rather than firing at
* once.
*
* The timer is NOT `unref`'d, exactly like the engine's: while the work it
* bounds is pending it keeps the process alive, so a stage that hangs on work
* holding no handle of its own still gets its timeout instead of a process
* that exits with the run never settled. The disarm is what lets it go.
*/
export declare function armDeadline(controller: AbortController, ms: number, reason: () => unknown): () => void;
/**
* `request` carrying `signal` as well as any signal it already carried — the
* SAME object when there is nothing to add. Every place the library hands a
* request to a provider stamps the run's signal through here.
*/
export declare function withRequestSignal<R extends {
readonly signal?: AbortSignal;
}>(request: R, signal: AbortSignal | undefined): R;
/**
* One non-streaming call under `signal` — the four laws in one place, for the
* library's own provider calls (the agent, `LLMCall`, the summarizers, the
* extractors, the gates): refused before it is sent when the signal has
* already fired, handed the request WITH the signal so the provider can cancel
* its own request, raced so an adapter that ignores it cannot hold the caller,
* and refused when its answer lands after the abort — so a late answer is
* never recorded or billed as if the call had completed.
*/
export declare function callUnder<R extends {
readonly signal?: AbortSignal;
}, T>(request: R, signal: AbortSignal | undefined, call: (request: R) => T | PromiseLike<T>): Promise<T>;
/**
* Laws 1, 2 and 4 for one piece of work that cannot be handed the signal (a
* callback with no signal in its contract): not started when the signal has
* already fired, not waited for once it fires, and refused when it lands after.
*/
export declare function awaitUnder<T>(signal: AbortSignal | undefined, start: () => T | PromiseLike<T>): Promise<T>;
/**
* Iterate `source` under `signal` — laws 2, 3 and 4 for a stream. With no
* signal, `source` itself.
*
* Each `next()` is raced against the signal; on abort the source is CLOSED —
* `close()` first (an SDK stream's `abort()`, a body reader's `cancel()`),
* then its iterator's `return()` — and the iteration rejects with the abort. A
* step that lands after the abort is refused, never handed on. A consumer that
* stops early (`break`, a throw) on a live signal closes the source as
* `for await` does — its `return()` waited for, its errors propagated; an
* abort during that wait ends it with the abort and runs `close()`, so the
* transport is closed even when its `return()` never finishes.
*
* The source's `return()` is NOT awaited: a source suspended inside its own
* await (a read the transport will never answer) cannot finish returning, and
* waiting for it would re-create the hang this exists to end.
*/
export declare function abortableIterable<T>(source: AsyncIterable<T>, signal: AbortSignal | undefined, close?: () => unknown): AsyncIterable<T>;
/**
* The chunks of a fetch body, read under `signal` — the raw-`fetch` adapters'
* one reader. Each read is raced against the signal and the signal checked
* again once it settles, so a chunk read a moment before the abort is never
* handed on. The body is CANCELLED on every way out (the end, a consumer that
* stops early, an error, an abort): an open body is a socket held and, on a
* local model, a generation that keeps running for nobody. A cancel waited on
* by a consumer that stopped early ends with the abort if one fires during it.
*/
export declare function bodyChunks(body: ReadableStream<Uint8Array>, signal: AbortSignal | undefined): AsyncGenerator<Uint8Array, void, undefined>;