agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
165 lines • 9.08 kB
TypeScript
/**
* seriesEvidence — one MCP request's retry series, written down for its run.
*
* Pattern: a collector over footprintjs's attempt records, filed once.
* Role: The series (`throttleRetry.ts` · `Series`) is the one retry owner and
* the attempt runner makes one record per try; this turns each record
* into an allow-listed row and files the request's whole story — its
* rows, how many were not listed, and how it ended — to the call's sink
* (`tools/requestEvidence.ts`) exactly ONCE.
*
* WHAT A ROW MAY SAY. Numbers and the library's own words; never the server's
* answer, the call's arguments or an error's text. A failure is a fixed sentence
* and its category — a network or SDK error message can carry a URL with a token
* in it, a host's internal name or a server's reply.
*
* A RESPONSE IS NOT A SUCCESS. The runner records an attempt that returned a
* response as `ok` with the reason `succeeded`; here it is `answered`, with the
* status the server sent — a 503 delivered is `answered` + `503`, never success.
*
* EVERY REQUEST SENT IS COUNTED. Each attempt the series started is a row — or,
* past the cap, one more in `omitted` — so `rows + omitted` is the number of
* requests the series sent. An attempt a cancellation cuts in flight is a row
* too (`cut`), exactly as an attempt the deadline cuts is (`timed-out`). A
* request the series never sent — the next one after a cancelled wait — has no
* row.
*
* A CUT IS NOT A "NOT RUN". An attempt that ended without an answer — cut,
* timed out, or failed — was sent, and the server may have received it and run
* it. Its row says so (`remote: 'unknown'`); nothing here records it as not run.
*
* THREE FACTS PER ATTEMPT, NONE READ FROM ANOTHER (`replay.ts`): the failure
* (footprintjs's category), what is known of its effect (`unknown` for every
* attempt sent that did not complete — a refusal included; `applied` when the
* call completed, never proof of a write; `none` only for what was never
* sent), and — on an attempt the series asked again after — what permitted it.
* Each row also says how long it took. The end says how many requests were
* sent and what is known of the whole series' effect; uncertainty survives
* (`SeriesKnowledge`).
*
* WHEN IT FILES. When the call settles — the series has ended by then, and the
* call's verdict is what says whether an answered attempt completed (a 200 can
* carry a tool error) — and, for a cancellation, at the moment the signal
* fires: a cancelled run stops waiting for its stage at once, so a record filed
* when the series later unwinds would land after the run was saved. The runner
* makes no record for an attempt it abandons at that moment or for a wait it
* was cut out of, so those are written here: the cut attempt's row and
* `during: 'request'`, or `during: 'wait'`. A series ended and waiting for its
* call files at once if the call is cancelled first, with what it knows.
* Anything the series reports after it has filed is not part of the request's
* story any more and is ignored.
*/
import type { AttemptRecord } from 'footprintjs';
import type { ToolRequestAttempt, ToolRequestEffect, ToolRequestPermission, ToolRequestReason } from '../events/payloads.js';
import { type RequestEvidence, type RequestEvidenceSink } from '../tools/requestEvidence.js';
import { type RequestReplay } from './replay.js';
/** Where the request went, as a row may say it: the app's label for it, or its origin. */
export type RequestWhere = Pick<RequestEvidence, 'endpoint' | 'origin'>;
/** What the series saw of one attempt that its record does not hold. */
export interface AttemptSeen {
/** The response's status, when a response arrived. */
readonly status?: number;
/** The refusal a rule recognised in that response, when one did. */
readonly refusal?: {
readonly kind: 'throttle' | 'session-busy';
readonly retryAfterMs?: number;
};
}
/** Why the series' `retryOn` said no — the reasons the runner calls `declined`. */
export type DeclineReason = Extract<ToolRequestReason, 'exhausted' | 'wait-limit' | 'no-time-left' | 'not-replayable' | 'not-retryable' | 'not-permitted' | 'changes-without-key'>;
/**
* What the series knows of one attempt beyond the runner's record (`replay.ts`):
* its effect — absent for an answered attempt, which the call's verdict decides
* when the record is filed — what permitted the next attempt, and how long it
* took.
*/
export interface AttemptFacts {
readonly effect?: ToolRequestEffect;
readonly permission?: ToolRequestPermission;
readonly durationMs: number;
}
/**
* Where the record waits for its call: the call's signal (it fires when the
* call settles or is cancelled), and the facts the call leaves there.
*/
export interface Settlement {
readonly signal: AbortSignal;
readonly replay: RequestReplay;
}
/** One request's evidence, collected as its series runs and filed once. */
export declare class SeriesEvidence {
private readonly sink;
private readonly where;
private readonly settlement?;
private readonly rows;
private omitted;
private last;
private filed;
private waiting;
/** What is known of the series' effect, attempt by attempt — the answer's excepted. */
private readonly knowledge;
/** The answered attempt, while its effect waits for the call's verdict. */
private answer;
constructor(sink: RequestEvidenceSink, where: RequestWhere, settlement?: Settlement | undefined);
/** One attempt as the runner recorded it, with what the series knows of it. */
attempt(record: AttemptRecord, seen: AttemptSeen | undefined, declined?: DeclineReason, facts?: AttemptFacts): void;
/**
* The caller's cancellation cut attempt `n` in flight. It was sent — the
* server may have it, and may have run it — so it is a row, `cut`, with the
* remote outcome `unknown`; then the request's end says the same.
*/
cutInFlight(n: number, durationMs?: number): void;
/**
* The caller's cancellation landed with NO request in flight: between two —
* `during: 'wait'`, the next request never sent — or before the first, when
* nothing was sent and there is no `during`. No row is added: there is no
* request to account for. A request that had already ended keeps its end.
*/
cancelled(): void;
/**
* The series delivered a response. It is the last attempt's — an answer, or a
* refusal it will not ask again after. A refusal it MEANT to ask again after
* is delivered when the series stopped inside the wait: the deadline landed
* there (`no-time-left`), or a pooled call's send gate refused the next
* attempt before it was sent (`stoppedBy`, `not-permitted`). The end says
* which, during the `wait`; the attempt that was never sent has no row.
* Filed when the call has read it.
*/
delivered(stoppedBy?: DeclineReason): void;
/**
* The series threw. The end is the last attempt's when that attempt ended it
* (a failure, a timed-out request). The rest is defensive — footprintjs 10.6's
* runner never throws without a record but for a cancellation, which is filed
* as one: a deadline that had passed before the first request (`timed-out`,
* no `during`, nothing sent) or one a future runner reports from inside a wait
* (`during: 'wait'`); anything else is filed as a failure the record cannot
* account for, so a request is never left unfiled. A pooled call's send gate
* that refused the next attempt before it was sent ends it as `stoppedBy`,
* during the `wait`. Filed when the call settles.
*/
threw(outOfTime: boolean, stoppedBy?: DeclineReason): void;
private add;
/**
* File `end` once the call has settled — its verdict is what says whether an
* answered attempt completed — or at once, when the call is already settled
* or cancelled, or there is no call to wait for.
*/
private fileWhenSettled;
private file;
/**
* The rows, the answered one given its effect: `applied` when the call
* completed — the server answered it, and not with an error — else `unknown`
* (a 5xx, a JSON-RPC error, a tool error, a call cancelled before it read the
* answer). The answer's effect joins the series' knowledge here.
*/
private settleAnswer;
}
/** One record as a row — see the module note for what a row may say. */
export declare function rowOf(record: AttemptRecord, seen: AttemptSeen | undefined, declined?: DeclineReason): ToolRequestAttempt;
/**
* `row` with what the series knows of its attempt beyond the record — its
* effect, what permitted the next attempt, how long it took. Without `facts`,
* the row as the record alone says it.
*/
export declare function withFacts(row: ToolRequestAttempt, facts: AttemptFacts | undefined): ToolRequestAttempt;
//# sourceMappingURL=seriesEvidence.d.ts.map