/** * 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. * 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: "" } * * 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; };