UNPKG

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
/** * 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