agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
97 lines (96 loc) • 5.18 kB
TypeScript
/**
* toolResultCap — the opt-in ceiling on what ONE tool result may cost.
*
* Pattern: a measured replacement at the dispatch boundary. Not a transform of
* the tool's value (the tool still returns whatever it returns), and
* not a policy about tools — a last-resort net on the ONE number a
* context window cannot survive: characters.
* Role: consumed by the tool-dispatch loop (`stages/toolCalls.ts`) on every
* path that turns a tool result into a `role: 'tool'` message. The
* one `role: 'tool'` message it never measures is the batch
* settlement's (9.113.0): that is the library's own fixed sentence
* for a call that never ran, not a tool result (`stages/README.md`).
* Emits: nothing. The truncation is visible because the marker IS the result
* — `agentfootprint.stream.tool_end` carries it, and so does history.
*
* ── Why this is opt-in, and why it will stay opt-in ──────────────────────────
* A default here would silently modify tool results. A tool that returns 200KB
* of rows is doing what somebody wrote it to do, and a framework that quietly
* replaced that with a stub the first time it ran would be lying to the app
* about its own tool. So: no `maxToolResultChars`, no measurement, no marker,
* byte-identical behaviour. Set it and every result on every dispatch path is
* measured against it.
*
* ── What the model is handed, and why it is shaped like a lesson ─────────────
* The failure this exists for is not "the result was big". It is "the result
* was big, the window blew, and the model never learned that asking for less
* was an option". A bare truncation teaches nothing — the model reads a
* sentence that stops mid-word and assumes the data ends there. The marker says
* what happened, how far over the cap it was, and what to do next:
*
* { truncated: true,
* reason: "orders_export returned 812431 chars, over the 20000-char cap.
* Narrow the request and call again.",
* head: "<the first N characters, verbatim>" }
*
* ONE shape, always the object, for a string result and an object result alike
* — a sink can branch on `.truncated` without parsing prose, and the model
* reads it as JSON on the `role: 'tool'` message. `head` is verbatim: it is the
* only part of the answer that survived, and paraphrasing it would make the
* marker the second thing in the transcript that cannot be trusted.
*
* ── The head budget ─────────────────────────────────────────────────────────
* `head` gets whatever the cap has left after the envelope that explains it —
* so the serialized marker stays within the cap, and a bigger cap buys a
* proportionally bigger head. When the cap is smaller than its own explanation,
* `head` is absent and the marker is the whole result: an explanation that had
* to be truncated to fit under a cap would teach nothing, which is the failure
* this module exists to prevent.
*/
/**
* The result a capped dispatch hands on — the marker IS the result.
*
* Reaches the model as JSON on the `role: 'tool'` message, and reaches
* `agentfootprint.stream.tool_end` as this object.
*/
export interface TruncatedToolResult {
/** Always `true`. The field a consumer branches on. */
readonly truncated: true;
/**
* What happened, in the model's own reading order: which tool, how big, what
* the cap was, and the one action that helps. Never carries the tool's
* arguments or the omitted content.
*/
readonly reason: string;
/**
* The first characters of the real result, verbatim. Absent when the cap is
* too small to afford any — see the head budget note above.
*/
readonly head?: string;
}
/** Type guard for consumers reading `tool_end.result` or a tool message. */
export declare function isTruncatedToolResult(value: unknown): value is TruncatedToolResult;
/**
* The refusal for a cap that cannot cap anything.
*
* `0` and negatives are not "disable it" — omitting the option is. A cap of `0`
* would replace every result with a marker that has no room for a head, which
* is a working agent turned into a wall of refusals; a negative one has no
* reading at all. Refused where it is configured, naming the value, rather than
* at the first tool call of the first run.
*/
export declare function assertMaxToolResultChars(site: string, value: number | undefined): void;
/**
* Measure one tool result against the cap and, when it is over, replace it.
*
* Returns the ORIGINAL value by reference when the cap is absent or the result
* fits — so an under-cap dispatch is byte-identical, not a round-trip through
* `JSON.parse(JSON.stringify(...))` that would quietly re-shape it.
*/
export declare function capToolResult(value: unknown, opts: {
readonly toolName: string;
readonly maxChars?: number;
}): {
readonly value: unknown;
readonly truncated: boolean;
};