import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { parseEvalSource } from "./eval-pragma.ts"; import type { EvalControlInput, EvalToolInput, EvalToolRequest, } from "./types.ts"; const NON_INTERACTIVE_MODES = new Set(["print", "json"]); const MAX_STRINGS_KEYS = 64; const MAX_STRINGS_KEY_CHARS = 256; const MAX_STRINGS_VALUE_CHARS = 200_000; export function parseEvalStrings( value: unknown ): Record | undefined { if (value === undefined) return undefined; if (value === null || typeof value !== "object" || Array.isArray(value)) { throw new TypeError("eval strings must be an object of string values"); } const out: Record = {}; let count = 0; for (const [key, entry] of Object.entries(value)) { if (typeof entry !== "string") { throw new TypeError(`eval strings value for "${key}" must be a string`); } if (count >= MAX_STRINGS_KEYS) { throw new TypeError( `eval strings supports at most ${MAX_STRINGS_KEYS} entries` ); } if (key.length === 0 || key.length > MAX_STRINGS_KEY_CHARS) { throw new TypeError( `eval strings key must be 1..${MAX_STRINGS_KEY_CHARS} characters` ); } if (entry.length > MAX_STRINGS_VALUE_CHARS) { throw new TypeError( `eval strings value for "${key}" exceeds ${MAX_STRINGS_VALUE_CHARS} characters` ); } out[key] = entry; count += 1; } return out; } export function parseEvalRequest(params: unknown): EvalToolRequest { if (!isRecord(params)) { throw new TypeError("eval parameters must be an object"); } if (params.action === "peek" || params.action === "stop") { if (typeof params.cell_id !== "string" || params.cell_id.length === 0) { throw new TypeError(`eval action "${params.action}" requires cell_id`); } return { action: params.action, cell_id: params.cell_id }; } if (params.action !== undefined && params.action !== "run") { throw new TypeError(`Unknown eval action "${String(params.action)}"`); } if (!isEvalLanguage(params.language)) { throw new TypeError("eval run requires language"); } if (typeof params.code !== "string") { throw new TypeError("eval run requires code"); } // Parse the first-line `@eval` pragma out of the cell code. Explicit tool // parameters win over pragma fields: each pragma value applies only when // the corresponding parameter is absent (`??`). const parsed = parseEvalSource(params.code); if ( params.on_timeout !== undefined && params.on_timeout !== "detach" && params.on_timeout !== "error" ) { throw new TypeError( `Unknown eval on_timeout value "${String(params.on_timeout)}"` ); } if ( params.tools !== undefined && (!Array.isArray(params.tools) || params.tools.some( (name) => typeof name !== "string" || name.length === 0 )) ) { throw new TypeError("eval tools must be an array of non-empty tool names"); } const title = params.title ?? parsed.title; const timeout = params.timeout ?? parsed.timeout; const onTimeout = params.on_timeout ?? parsed.on_timeout; return { language: params.language, code: parsed.code, ...(params.action === "run" ? { action: "run" as const } : {}), ...(typeof title === "string" ? { title } : {}), ...(typeof timeout === "number" ? { timeout } : {}), ...(onTimeout === "detach" || onTimeout === "error" ? { on_timeout: onTimeout } : {}), ...(typeof params.reset === "boolean" ? { reset: params.reset } : {}), // Preserve the empty array: an empty allowlist blocks every session tool. ...(Array.isArray(params.tools) ? { tools: params.tools } : {}), ...(params.strings === undefined ? {} : { strings: parseEvalStrings(params.strings) }), }; } export function isEvalControlRequest( request: EvalToolRequest ): request is EvalControlInput { return request.action === "peek" || request.action === "stop"; } export function evalTimeoutBehavior( input: EvalToolInput, ctx: ExtensionContext ): "detach" | "error" { if (input.on_timeout !== undefined) { return input.on_timeout; } return NON_INTERACTIVE_MODES.has(ctx.mode) ? "error" : "detach"; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function isEvalLanguage(value: unknown): value is EvalToolInput["language"] { return value === "py" || value === "js" || value === "rb" || value === "ts"; }