/** * Runs one codemode script in the sandbox. Split from tool.ts and loaded through * execute.lazy.ts so the sandbox runtime only loads when a script runs. */ import type { AgentTool, AgentToolCallOutcome, AgentToolResult } from "@earendil-works/pi-agent-core"; import type { AnyModel, ClassifierContext, ImageContent, ImagesContext, ModelType, ModelTypeMap, TextContent, Usage, } from "@earendil-works/pi-ai"; import { type CodemodeResult, CodemodeSandbox, type CodemodeTool, loadQuickJSWasm, parseCodemodeSource, renderToolSample, toCodemodeIdentifier, } from "@earendil-works/pi-codemode"; import { getCodemodeWorkerSpecifier, getQuickJSWasmPath } from "../../config.ts"; import type { ExtensionToolContext, ToolNamespace } from "../../core/extensions/types.ts"; import type { SessionEntry } from "../../core/session-manager.ts"; import { formatSize } from "../../core/tools/truncate.ts"; import { combineUsage } from "../../core/usage-totals.ts"; import { writeOutputFile } from "../../utils/output-files.ts"; import { Bm25Ranker, createToolSearchDocument, DEFAULT_TOOL_SEARCH_LIMIT } from "../tool-search/tool.ts"; import { CODEMODE_DOCS_PATH, CODEMODE_STORE_ENTRY_TYPE, type CodemodeModelRuntime, type CodemodeNestedCall, type CodemodeStoreEntryData, type CodemodeToolDetails, type CodemodeToolInput, type CodemodeToolOptions, getCodemodeCallableTools, toCodemodeDeclaration, } from "./tool.ts"; const ARGS_PREVIEW_CHARS = 200; const ERROR_PREVIEW_CHARS = 500; /** `models.classify()` and `models.generateImages()` calls one script may have in flight; `Promise.all` over many items queues the rest. */ const MAX_CONCURRENT_MODEL_CALLS = 4; /** * Heap limit for the QuickJS VM. The worker shares pi's process, so without a limit a runaway * script can grow to wasm32's 4 GiB and take the session down. Overruns throw * `InternalError: out of memory` inside the script. */ const CODEMODE_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024; const MODEL_TYPES: ReadonlySet = new Set(["chat", "image", "classifier"]); function truncateText(text: string, maxChars: number): string { return text.length > maxChars ? `${text.slice(0, maxChars - 3)}...` : text; } function previewArgs(args: unknown): string { if (args === undefined) return ""; try { return truncateText(JSON.stringify(args) ?? "", ARGS_PREVIEW_CHARS); } catch { return ""; } } function textOf(result: AgentToolResult): string { return (result.content ?? []) .filter((block): block is TextContent => block.type === "text") .map((block) => block.text) .join("\n"); } function toModelType(value: unknown): ModelType { if (typeof value === "string" && MODEL_TYPES.has(value)) return value as ModelType; throw new Error(`Unknown model type ${JSON.stringify(value)}. Use "chat", "image", or "classifier".`); } function toProvider(value: unknown): string | undefined { if (value === undefined || value === null) return undefined; if (typeof value !== "string") throw new Error("provider must be a string"); return value; } /** Catalog entry for scripts. `headers` is dropped because models.json headers can carry credentials. */ function toModelInfo(model: AnyModel): Record { const info: Record = { ...model }; delete info.headers; return info; } /** `an image`, `a classifier`. */ function withArticle(word: string): string { return `${/^[aeiou]/.test(word) ? "an" : "a"} ${word}`; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** A script value in an error message: `undefined`, `a string`, `an array`, or its keys (`{ prompt }`). */ function describeValue(value: unknown): string { if (value === undefined || value === null) return String(value); if (Array.isArray(value)) return value.length === 0 ? "an empty array" : "an array"; if (typeof value === "object") { const keys = Object.keys(value); if (keys.length === 0) return "{}"; return `{ ${keys.slice(0, 6).join(", ")}${keys.length > 6 ? ", ..." : ""} }`; } return typeof value === "string" ? "a string" : `a ${typeof value}`; } const CLASSIFIER_CONTEXT_SHAPE = '{ state: { ... }, questions: { : { type: "choice", instructions, criteria: {