import { DEFAULT_CODE_MODE_OUTPUT_TOKENS, MAX_CODE_MODE_OUTPUT_TOKENS, } from "./host-protocol.js"; import type { NotebookMemoryUsage, RuntimeContentItem, RuntimeResponse } from "./types.js"; const MAX_OUTPUT_IMAGE_COUNT = 4; const MAX_OUTPUT_IMAGE_CHARS = 16 * 1024 * 1024; export function toCodeModeToolResult( response: RuntimeResponse, maxTokens?: number, ) { const scriptError = response.kind === "result" ? withScriptErrorRecovery(response.errorText) : undefined; const status = scriptError ? `Script error: ${scriptError}` : response.kind === "yielded" ? `Still running (exec cell "${response.cellId}"). Use wait once near expected completion; avoid short polling` : response.kind === "terminated" ? "Script terminated" : "Script completed"; let imageChars = 0; let imageCount = 0; let omittedImages = 0; const output = response.contentItems .map((item) => { const content = toPiContent(item); if (content?.type !== "image") return content; if ( imageCount >= MAX_OUTPUT_IMAGE_COUNT || imageChars + content.data.length > MAX_OUTPUT_IMAGE_CHARS ) { omittedImages += 1; return undefined; } imageCount += 1; imageChars += content.data.length; return content; }) .filter((item): item is NonNullable => Boolean(item)); output.unshift( ...runningExecSessionGuidance(response.traces ?? []).map((text) => ({ type: "text" as const, text, })), ); if (response.notebookMemory) { output.unshift({ type: "text", text: formatNotebookMemory(response.notebookMemory) }); } if (omittedImages > 0) output.push({ type: "text", text: `[${omittedImages} code-mode image${omittedImages === 1 ? "" : "s"} omitted]`, }); const outputTokens = Math.min( MAX_CODE_MODE_OUTPUT_TOKENS, Math.max( 1, maxTokens ?? response.maxOutputTokens ?? DEFAULT_CODE_MODE_OUTPUT_TOKENS, ), ); return { content: [ { type: "text" as const, text: status }, ...truncateTextContent(output, outputTokens * 4), ], details: { codeMode: true, cellId: response.cellId, status: response.kind, ...(response.traces ? { traces: response.traces } : {}), ...(response.droppedTraceCount ? { droppedTraceCount: response.droppedTraceCount } : {}), ...(response.notebookMemory ? { notebookMemory: response.notebookMemory } : {}), ...(scriptError ? { scriptError } : {}), }, }; } function withScriptErrorRecovery(errorText: string | undefined): string | undefined { if (!errorText || !/Identifier ['"][^'"]+['"] has already been declared/.test(errorText)) return errorText; return `${errorText}\nRecovery: reuse the existing binding, choose a new name, or retry one-off code inside { ... }; restart only if the binding itself is unusable`; } export function formatNotebookMemory(memory: NotebookMemoryUsage): string { const ratio = memory.heapLimitBytes > 0 ? memory.heapUsedBytes / memory.heapLimitBytes : 0; const pressure = ratio >= 0.9 ? " · CRITICAL: finish essential work and release unneeded notebook state" : ratio >= 0.8 ? " · WARNING: release unneeded notebook state" : ""; return `Notebook memory: heap ${formatBinaryBytes(memory.heapUsedBytes)} / ${formatBinaryBytes(memory.heapLimitBytes)} · RSS ${formatBinaryBytes(memory.rssBytes)}${pressure}`; } function formatBinaryBytes(bytes: number): string { const mib = bytes / (1024 * 1024); if (mib < 1024) return `${mib.toFixed(mib < 10 ? 1 : 0)} MiB`; const gib = mib / 1024; return `${gib.toFixed(gib < 10 ? 1 : 0)} GiB`; } function runningExecSessionGuidance( traces: NonNullable, ): string[] { const sessionIds = new Set(); for (const trace of traces) { if (trace.status !== "done") continue; const details = trace.result?.details; const resultSessionId = numericSessionId(details); if (trace.name === "exec_command" && resultSessionId !== undefined) { sessionIds.add(resultSessionId); continue; } if (trace.name !== "write_stdin") continue; const inputSessionId = numericSessionId(trace.input); if (inputSessionId === undefined) continue; if (resultSessionId === undefined) sessionIds.delete(inputSessionId); else sessionIds.add(resultSessionId); } return [...sessionIds].map( formatRunningExecSessionGuidance, ); } export function formatRunningExecSessionGuidance(sessionId: number): string { return `Session ${sessionId} still running. Resume near completion with tools.write_stdin and an appropriate yield_time_ms; do not use wait`; } function numericSessionId(value: unknown): number | undefined { if ( value && typeof value === "object" && "session_id" in value && typeof value.session_id === "number" ) return value.session_id; return undefined; } function toPiContent( item: RuntimeContentItem, ): | { type: "text"; text: string } | { type: "image"; data: string; mimeType: string } | undefined { if (item.type === "input_text" && typeof item.text === "string") return { type: "text", text: item.text }; if (item.type === "input_image" && typeof item.image_url === "string") { const match = item.image_url.match(/^data:([^;,]+);base64,(.+)$/s); if (match) return { type: "image", mimeType: match[1]!, data: match[2]! }; } return undefined; } function truncateTextContent( content: T[], maxChars: number, ): T[] { let remaining = maxChars; let truncated = false; const output: T[] = []; for (const item of content) { if (item.type !== "text" || typeof item.text !== "string") { output.push(item); continue; } if (remaining <= 0) { if (!truncated) output.push({ ...item, text: "[Output truncated]" }); truncated = true; continue; } if (item.text.length <= remaining) { remaining -= item.text.length; output.push(item); continue; } const text = `${item.text.slice(0, remaining)}\n[Output truncated]`; remaining = 0; truncated = true; output.push({ ...item, text }); } return output; }