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 • 5.7 kB
TypeScript
/**
* 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>;
}
//# sourceMappingURL=work.d.ts.map