UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

117 lines (116 loc) • 5.66 kB
/** * work — an admitted operation's charge, and the work under it until that work * has settled. * * Pattern: one object per unit of work that is at once the request guard * (`mcp/requestGuard.ts`) and the count of what is still running. * Role: The pool's third budget (`docs/design/2026-10-mcp-client-pool.md` * § 5.3). The accounting unit is the admitted LOGICAL operation: a * `tools/call`, a listing (one `list(ctx)` for one conversation), an * `acquire` and an ending each take one slot at admission. Setup an * operation performs for itself — the open of its key's client, the * revalidating `tools/list` of a replacement — runs inside that slot. * * AN ABORT IS NOT SETTLEMENT. A slot is released only when the operation's * logical request has its answer AND every piece of work under it has settled: * the `send()` it made, a credential vend, an attempt's `fetch`, an AWS `send`, * the open it owns. A caller's timeout answers the caller; it frees nothing. * Work that never settles keeps its charge — `stats().unsettled` reports it, * and the budget refuses new work rather than admit it against room that does * not exist. There is no release that returns capacity sooner. */ import type { RequestGuard } from '../requestGuard.js'; /** What a unit of work is doing now — what `stats().unsettled` counts it under. */ export type WorkKind = 'open' | 'listing' | 'call' | 'ending'; /** What an operation is, as the budget admits it. */ export type OperationKind = 'listing' | 'call' | 'acquire' | 'ending'; /** What a request's answer proved, as the pool's fetch read it (`pool.ts`). */ export type WorkEvidence = 'server-404' | 'handle-lost'; /** * One unit of pooled work: its requests are checked against it at every * boundary (`check`), and every promise started for it is counted until it * settles (`track`). It is SETTLED once its logical part has finished and the * count is zero. */ export declare class Work implements RequestGuard { /** What it is doing now. */ phase: WorkKind; private readonly link; private pending; private finished; private done; /** Still running underneath when the caller was answered: it lingered. */ private lingered; private readonly listeners; /** What `check` asks besides the abort — a call's binding. */ private extra; /** What the latest answered request proved, if anything. */ evidence: WorkEvidence | undefined; constructor( /** What it is doing now. */ phase: WorkKind, sources: readonly (AbortSignal | undefined)[]); /** What aborts its own work: its sources, its deadline, its caller detaching. */ get signal(): AbortSignal; /** True once its logical part finished and everything under it settled. */ get settled(): boolean; /** Its caller was answered while work under it still ran. */ get unsettledPastAnswer(): boolean; /** Abort its work with `reason` (a deadline, a detached caller). Idempotent. */ abort(reason: unknown): void; /** Ask `gate` too, each time a request is checked. */ guardWith(gate: () => void): void; /** * The send gate: refused once its caller was answered (nothing more is sent * for an answered operation), once aborted, or when the extra gate refuses. */ check(): void; /** Count `work` until it settles. Never throws; never surfaces its rejection. */ track(work: PromiseLike<unknown>): void; /** Record what a request's answer proved. */ note(evidence: WorkEvidence): void; /** * Its logical part is over: the caller has its answer. It settles now, or — * when work under it still runs — when that work does. */ finish(): void; /** Did it linger — answered while work under it ran? Read once it settled. */ get settledLate(): boolean; /** Call `listener` once it has settled (at once when it already has). */ onSettled(listener: () => void): void; /** A promise that resolves once it has settled. */ whenSettled(): Promise<void>; private settleIfDone; } /** An admitted operation: a unit of work that holds one slot of the budget. */ export declare class Operation extends Work { readonly kind: OperationKind; /** When it was admitted, on the pool's clock. */ readonly admittedAt: number; constructor(kind: OperationKind, phase: WorkKind, sources: readonly (AbortSignal | undefined)[], /** When it was admitted, on the pool's clock. */ admittedAt: number); } /** * The pool-wide operation budget: at most `max` operations charged, each from * its admission until it has SETTLED (see the module note). Admission is the * only way in; settlement the only way out. */ export declare class OperationBudget { readonly max: number; private readonly charged; private readonly onRelease; constructor(max: number); /** Operations charged now. */ get count(): number; /** Is there a free slot? */ get hasRoom(): boolean; /** * Admit one operation, or `undefined` when the budget is full. The slot is * charged from here until the operation settles. */ admit(kind: OperationKind, phase: WorkKind, sources: readonly (AbortSignal | undefined)[], now: number): Operation | undefined; /** Call `listener` each time an operation settles and frees its slot. Returns the unsubscribe. */ whenReleased(listener: (operation: Operation) => void): () => void; /** Charged work whose caller was already answered and which has not settled, by what it is doing. */ unsettled(): Record<WorkKind, number>; }