/** * ADR-O001 D2 — spill-and-reference for tool results. * * Measured on one real run: tool results are 76% of all context mass (Bash * 4,387 calls / 5.7M chars and Read 420 / 2.0M chars are 97% of that), while * inter-agent mail is 0.1%. A role holds ONE SDK session for its whole life * with no truncation anywhere in orgrt/, so every tool result it ever saw is * re-sent on every later turn — cost is roughly quadratic in turns. * * The mechanism already existed, pointed at the 0.1% channel: cross-org.ts's * `mailBody` writes an oversized mail body to `.mail/.md` and leaves a * 1,024-char digest plus a path in context. This is the same shape, applied to * the channel that carries the mass — same "write in full FIRST, digest * second" ordering, same "on write failure deliver in full rather than lose * content" fallback. * * ── Threshold arithmetic (why 2,048) ────────────────────────────────────── * Result sizes: p50 451, p75 1,583, p90 3,506, p95 5,138, p99 11,739, max * 59,805, mean 1,370. It is a long tail of SMALL results, so the usual "only * spill the monsters" instinct recovers almost nothing: * * threshold spilled calls net tool mass recovered * 11,739 (p99) 1.0% 21% * 5,138 (p95) 5.0% 37% * 4,096 (mail's) 8.2% 42% * 3,506 (p90) 10.0% 45% * 2,048 21.4% 52% ← chosen * 1,536 (p75) 26.0% 52% * * Two constraints fix the choice. Below, a spilled result still costs its * digest (1,536) plus a reference line (~160) — about 1,700 chars — so any * threshold under ~1,700 ADDS mass instead of removing it; that is the floor. * Above, the curve is flat from 1,536 to ~3,000 and then falls away, so 2,048 * (= 2× the mail digest) sits at the knee: it takes essentially all the * recoverable mass while leaving four fifths of all calls untouched, which is * what keeps a role able to do its job. * * ── Digest strategy ─────────────────────────────────────────────────────── * Never a blind one-sided truncation. A Bash result puts what matters at the * END (assertion failures, the summary line, the exit status); a Read puts it * at the START. So the digest is always head + elision note + tail, with the * WEIGHT flipped by tool: 1/3 head + 2/3 tail for command-shaped tools * (the default, since Bash dominates), 2/3 head + 1/3 tail for file-shaped * ones. * * The response object's SHAPE is preserved — only its long string leaves are * digested, and the budget is water-filled across them smallest-first, so a * short `stderr` next to a huge `stdout` survives whole. */ /** Results at or under this stay byte-identical. See the threshold arithmetic * in the module comment — this is the knee of the recovery curve, not a * round number. */ export declare const TOOL_RESULT_MAX = 2048; /** Total digest text a spilled result may keep in context, across all of its * string leaves. 1.5× cross-org.ts's MAIL_DIGEST_CHARS: a tool result has to * carry a stack trace where a mail body only has to carry a subject. */ export declare const TOOL_DIGEST_CHARS = 1536; /** Where a role's spilled tool results live: `/tool-results//`. The * session builds the runner's spill hook from it and tells the role's policy it * is the role's own, so the full text a digest points to is always readable. */ export declare function toolSpillDirFor(base: string, roleId: string): string; export interface SpilledToolResult { /** Bounded replacement for the model's transcript — same shape as the * original response, with its long string leaves digested. */ output: unknown; /** Absolute path the full body was written to, as referenced in `output`. */ file: string; } /** * Bound one tool result. Returns undefined when nothing needs doing (the * result is at or under TOOL_RESULT_MAX) or when the spill write failed — in * both cases the caller leaves the original result alone, so content is never * lost to a disk error. */ export declare function spillToolResult(dir: string, toolName: string, toolUseId: string, response: unknown): SpilledToolResult | undefined; /** Minimal shape of the SDK's PostToolUse hook input this needs. */ interface PostToolUseInput { hook_event_name?: string; tool_name?: string; tool_input?: unknown; tool_response?: unknown; tool_use_id?: string; } /** * A `PostToolUse` hook for the Claude Agent SDK. This is the ONLY layer that * can rewrite a tool result before it reaches the model: Bash and Read run * inside the SDK's own CLI process, so the org runtime never touches their * output — `agent-runner.ts`'s `tool_result` stream is read-only observation, * emitted after the transcript already holds the full text. The SDK does * expose the seam we need here (`PostToolUseHookSpecificOutput. * updatedToolOutput`: "Replaces the tool output before it is sent to the * model"), so the clean approach is achievable — for the Claude path. The * vendor runners (codex/opencode/qwen/…) drive their own CLIs and have no * equivalent, so they are out of scope here. */ export declare function toolResultSpillHook(dir: string): (input: PostToolUseInput) => Promise>; export {}; //# sourceMappingURL=tool-spill.d.ts.map