import type { AgentTool } from "../internal/harness.js"; /** * Pluggable backing store for **offloaded large tool results** (design/30). When a tool returns more * text than the threshold, the full content is moved here and the conversation keeps only a preview + * a `ref`; the model fetches the rest via the injected `read_tool_result` tool. This is the local, * provider-agnostic equivalent of CC's `toolResultStorage` (no Anthropic `cache_edits` dependency). * * Default is {@link InMemoryToolResultStore} (task-scoped, lives only for the run). Inject a durable * implementation (TiDB/blob, keyed by the opaque `ref`) so an offloaded result survives wake/resume; * with the in-memory default, a fetch after a cross-process wake returns undefined and the model is told * the output is no longer available — the preview still stands in context, so it degrades, never crashes. */ export interface ToolResultStore { /** * Persist full content under a stable `ref`. **Write-once and idempotent**: if `ref` already exists, * keep the existing content and do nothing (the aggregate budget re-runs every query with the same * deterministic ref, so `put` for an already-stored ref MUST be a no-op — re-writing would risk a * non-identical preview and break the prompt cache). A durable backend treats `ref` as the primary key * and should validate/sanitize it (it is composed from sessionId+toolCallId, not raw model input, but a * durable store still owns key hygiene for its storage layer). */ put(ref: string, content: string): Promise | void; /** Read a slice of the stored content. Unknown `ref` → undefined. */ get(ref: string, opts?: { offset?: number; limit?: number; }): Promise | ToolResultSlice | undefined; } export interface ToolResultSlice { content: string; /** Byte/char offset this slice starts at. */ offset: number; /** Total chars of the full stored content. */ totalChars: number; } /** Default in-memory store (no cross-process durability); write-once per ref. * * Blackboard 2026-07-03 (clay dogfood: `ReadToolResult(ref)` came back empty for a workflow child's * offloaded output): the Runner now shares ONE instance across its tasks (instead of a per-task * island), so a parent can deref a child's ref within the process. A shared long-lived instance * needs a bound — `maxTotalChars` FIFO-evicts the oldest refs past the cap (an evicted deref lands * on the existing honest "may have expired" text). Durability across restarts still requires a real * store (e.g. FileToolResultStore) via `RunnerDeps.toolResultStore`. */ export declare class InMemoryToolResultStore implements ToolResultStore { private readonly opts?; private readonly map; private totalChars; constructor(opts?: { maxTotalChars?: number; } | undefined); put(ref: string, content: string): void; /** design/80 D-2: true when NOTHING has been offloaded — a durable suspend can then proceed safely even on * this in-memory store, because a cross-replica resume has no offloaded result to deref to null. */ isEmpty(): boolean; get(ref: string, opts?: { offset?: number; limit?: number; }): ToolResultSlice | undefined; } /** Marker subclass for the Runner's own shared fallback instance (no injected store). prepare-task * detects it and wraps a per-task {@link ScopedToolResultStore} around it — an INJECTED store is * never wrapped (a multi-tenant deployment that injects a durable store owns its own scoping). */ export declare class RunnerSharedToolResultStore extends InMemoryToolResultStore { } /** * Per-task view over the Runner-shared fallback (impl-review 2026-07-03, codex 2 BLOCKER): * 1. **Tenant isolation**: refs are namespaced by the task's trust scope (principal), so tenant B's * `ReadToolResult` can never deref tenant A's ref — the per-task-instance era made this isolation * implicit; sharing one instance re-opened it. Same-scope cross-task deref (the A2 fix's whole * point) still works: same scope ⇒ same namespace. * 2. **D-2 durable-suspend gate fidelity**: `isEmpty()` reflects only THIS task's offloads (local put * count) — matching the per-task-instance semantics the gate was written against — so an unrelated * prior task's offloaded entry can no longer disable durable suspend for a task that offloaded * nothing (prepare-task's fail-fast reads `isEmpty()`). */ export declare class ScopedToolResultStore implements ToolResultStore { private readonly inner; private readonly scope; private localPuts; /** True when the underlying shared store is process-memory (volatile across replicas) — the D-2 * suspend gate treats a NON-empty volatile store as unsafe to durably suspend. */ readonly volatileBacking: boolean; constructor(inner: ToolResultStore, scope: string); /** Length-prefixed namespace — unambiguous for any scope string (no delimiter-injection ambiguity). */ private key; put(ref: string, content: string): Promise | void; get(ref: string, opts?: { offset?: number; limit?: number; }): Promise | ToolResultSlice | undefined; /** THIS task's offload count only — the D-2 gate's per-task semantics (see class doc). */ isEmpty(): boolean; } /** True when a durable suspend would lose offloaded results held only in process memory (D-2 gate). */ export declare function isVolatileOffloadStore(store: ToolResultStore): boolean; /** Reserved name of the injected large-result reader (design/108 PascalCase: `read_tool_result`→`ReadToolResult`). */ export declare const OFFLOAD_TOOL_NAME = "ReadToolResult"; /** Leading marker of an offload preview (see {@link buildPreview}) — single source so the aggregate * budget (design/64 §17.2) can recognize an already-offloaded result without re-deriving the string. */ export declare const PERSISTED_OUTPUT_PREFIX = "