/** * `run_code` — the single meta-tool that collapses N tool calls into * one model round-trip. The model emits a JavaScript program; we * execute it inside a `vm` sandbox where every other tool is exposed * as `tools.(args) => Promise<...>`, and only what the program * passes to `text(...)` (plus the resolved return value) flows back * to the model. * * Why `node:vm` instead of `isolated-vm`: * - The model is the koi's own brain, not adversarial input. We * don't need a process-level boundary, just a clean global * surface that hides `process`, `require`, `fetch`, etc. * - `isolated-vm` is a native module — every EC2 deploy would need * a prebuild for the target platform. `node:vm` is built-in. * - Execution still runs in the koi process, so per-tool side * effects (sessions, file writes, sandbox bash) work unchanged. * * Lifetime: each `run_code` call spins up a fresh context. There's no * persistent state between calls — the model can stitch one program * across many tools, but if it wants to remember something across * turns it has to call `text()`. */ import type { TSchema } from "@sinclair/typebox"; import type { AgentTool as KoiTool } from "@mariozechner/pi-agent-core"; import type { CellSession } from "../oo-harness/cell-session.js"; import type { HarnessEventLog } from "../oo-harness/harness-events.js"; /** The wall-clock budget this program gets, before the hard ceiling. */ export declare function budgetForProgram(source: string, requested?: number): number; /** * Schema for the model-facing tool. We keep it boring on purpose so * the model doesn't waste tokens on irrelevant params. */ declare const RUN_CODE_PARAMETERS: TSchema; export interface RunCodeToolDeps { /** All other tools that should be reachable from inside the sandbox. */ inner: KoiTool[]; /** Optional hook fired when a wrapped tool call starts; lets us * surface the same UI bubbles the conversational loop would. */ onInnerStart?: (toolName: string, args: unknown) => void; onInnerEnd?: (toolName: string, isError: boolean) => void; /** * Live progress snapshot for the terminal's activity card: fired on every * inner-call start/end and progress() push so a long program streams its * inner life instead of sitting as one opaque step for minutes. */ onLiveUpdate?: (snapshot: Record) => void; /** * The run's live workbench. When present, values kept by one cell stay * alive for the next one and only their bounded previews reach the model * (see oo-harness/cell-session). Without it the tool behaves as it always * did: every cell starts empty. */ session?: CellSession; /** The run's typed history, so cells and their outcomes are addressable. */ events?: HarnessEventLog; } interface ExecutionOutcome { texts: string[]; progress: string[]; returnValue: unknown; toolCalls: Array<{ name: string; durationMs: number; isError: boolean; /** Sanitized, size-capped view of the call's arguments (path/command/oldText/newText/content…) so UIs can render real cards for inner calls. */ args?: Record; /** The numbered diff an inner edit/write produced (result.details.diff), capped. */ diff?: string; /** Short plain-text preview of the inner call's result content. */ resultPreview?: string; }>; totalDurationMs: number; /** Bindings this cell put on the workbench, as `name = preview` lines. */ kept: string[]; } /** * Run a single piece of model-emitted JS in a fresh context. * Exposes: * tools.(args) — every wrapped tool, async * text(msg) — append to model-facing output * progress(msg) — push a non-final status update * console.log/.error — appended to a debug buffer (not surfaced) * * Disallows: anything Node-y. The global object is a plain {} with * exactly the curated surface above. */ /** * `return` a program that is nothing but an async IIFE. * * The program is wrapped as `(async () => { })()`. When the source is * ITSELF `(async () => { ... })();` — the shape this tool's own description * tells models to use — that inner call is an expression STATEMENT: its promise * is created, discarded, and the outer function resolves immediately. Output * collection then ends while the inner body is still suspended at its first * await, so everything after that await vanishes. * * Reproduced on the live gateway 2026-08-17: * * (async () => { text("BEFORE"); await sleep(2000); text("AFTER"); })(); * -> the entire result was "BEFORE" * * This is what "the sandbox is eating my output" was. A koi asked to shop on * IKEA made twenty-odd run_code calls in three minutes, every one truncated at * its first await, and concluded from the silence that the browser agent was * not installed. It was installed and idle the whole time. */ export declare function returnTrailingAsyncIife(source: string): string; /** * Build the single meta-tool that replaces the koi's tool surface * when code-mode is enabled. The inner tool set is captured at * construction time — the same set the conversational loop would * see — so behavior is consistent. */ export declare function createRunCodeTool(deps: RunCodeToolDeps): KoiTool; export {};