agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
213 lines • 7.64 kB
JavaScript
/**
* 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 { abortErrorOf, linkedController } from '../../lib/abort.js';
/**
* 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 class Work {
phase;
link;
pending = 0;
finished = false;
done = false;
/** Still running underneath when the caller was answered: it lingered. */
lingered = false;
listeners = [];
/** What `check` asks besides the abort — a call's binding. */
extra;
/** What the latest answered request proved, if anything. */
evidence;
constructor(
/** What it is doing now. */
phase, sources) {
this.phase = phase;
this.link = linkedController(...sources);
}
/** What aborts its own work: its sources, its deadline, its caller detaching. */
get signal() {
return this.link.controller.signal;
}
/** True once its logical part finished and everything under it settled. */
get settled() {
return this.done;
}
/** Its caller was answered while work under it still ran. */
get unsettledPastAnswer() {
return this.finished && !this.done;
}
/** Abort its work with `reason` (a deadline, a detached caller). Idempotent. */
abort(reason) {
if (!this.link.controller.signal.aborted)
this.link.controller.abort(reason);
}
/** Ask `gate` too, each time a request is checked. */
guardWith(gate) {
this.extra = gate;
}
/**
* 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() {
if (this.finished)
throw workFinished();
const { signal } = this;
if (signal.aborted)
throw abortErrorOf(signal);
this.extra?.();
}
/** Count `work` until it settles. Never throws; never surfaces its rejection. */
track(work) {
if (this.done)
return;
this.pending += 1;
const release = () => {
this.pending -= 1;
this.settleIfDone();
};
Promise.resolve(work).then(release, release);
}
/** Record what a request's answer proved. */
note(evidence) {
this.evidence = evidence;
}
/**
* 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() {
if (this.finished)
return;
this.finished = true;
if (this.pending > 0)
this.lingered = true;
// Nothing more may be sent for it: `check` refuses from here on. Its signal
// is left alone — a stream the SDK is still reading is not this work's to cut.
this.settleIfDone();
}
/** Did it linger — answered while work under it ran? Read once it settled. */
get settledLate() {
return this.lingered;
}
/** Call `listener` once it has settled (at once when it already has). */
onSettled(listener) {
if (this.done)
listener();
else
this.listeners.push(listener);
}
/** A promise that resolves once it has settled. */
whenSettled() {
return new Promise((resolve) => this.onSettled(resolve));
}
settleIfDone() {
if (this.done || !this.finished || this.pending > 0)
return;
this.done = true;
this.link.unlink();
for (const listener of this.listeners.splice(0)) {
try {
listener();
}
catch {
// A listener is the pool's own bookkeeping; one failing must not stop the rest.
}
}
}
}
/** The reason a finished operation refuses any later send — a fixed sentence. */
function workFinished() {
return Object.assign(new Error('This pooled operation has already been answered.'), {
name: 'AbortError',
});
}
/** An admitted operation: a unit of work that holds one slot of the budget. */
export class Operation extends Work {
kind;
admittedAt;
constructor(kind, phase, sources,
/** When it was admitted, on the pool's clock. */
admittedAt) {
super(phase, sources);
this.kind = kind;
this.admittedAt = admittedAt;
}
}
/**
* 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 class OperationBudget {
max;
charged = new Set();
onRelease = [];
constructor(max) {
this.max = max;
}
/** Operations charged now. */
get count() {
return this.charged.size;
}
/** Is there a free slot? */
get hasRoom() {
return this.charged.size < this.max;
}
/**
* Admit one operation, or `undefined` when the budget is full. The slot is
* charged from here until the operation settles.
*/
admit(kind, phase, sources, now) {
if (!this.hasRoom)
return undefined;
const operation = new Operation(kind, phase, sources, now);
this.charged.add(operation);
operation.onSettled(() => {
this.charged.delete(operation);
for (const listener of [...this.onRelease])
listener(operation);
});
return operation;
}
/** Call `listener` each time an operation settles and frees its slot. Returns the unsubscribe. */
whenReleased(listener) {
this.onRelease.push(listener);
return () => {
const at = this.onRelease.indexOf(listener);
if (at >= 0)
this.onRelease.splice(at, 1);
};
}
/** Charged work whose caller was already answered and which has not settled, by what it is doing. */
unsettled() {
const counts = { open: 0, listing: 0, call: 0, ending: 0 };
for (const operation of this.charged) {
if (operation.unsettledPastAnswer)
counts[operation.phase] += 1;
}
return counts;
}
}
//# sourceMappingURL=work.js.map