图解 Pi AgentPi Agent, Illustratedv1.1.0
包Packages· codemode
演示Slides
Extensions已完成Donepackages/codemode · v1.1.0

codemode@earendil-works/pi-codemode

沙箱化 JavaScript 执行:唯一能力是调用注入的工具。Sandboxed JavaScript execution where the only capability is calling injected tools.

1.1.0
版本version
10
源文件src files
1,837
源码行src LOC
3
测试文件test files
68
测试用例test cases
4
导出路径export paths
依赖Depends on无内部依赖no internal deps被依赖Used bycoding-agent

这个包是什么What this package ispi-codemode 让模型写一段 JavaScript 来调用工具,而不是一次调用一个工具。脚本跑在 worker 线程里的 QuickJS 虚拟机(独立的 wasm 实例)中,唯一的能力就是宿主注入的工具与 globals——没有计时器、网络、文件系统或模块。包里还有把 JSON Schema 渲染成 TypeScript 声明的工具,好让模型知道怎么调用。pi-codemode lets the model write a piece of JavaScript that calls tools, instead of calling one tool at a time. The script runs in a QuickJS VM (a separate wasm instance) inside a worker thread, and its only capability is the tools and globals the host injects — no timers, network, filesystem or modules. The package also renders JSON Schemas as TypeScript declarations so the model knows how to call them.

共 1,837 行 TypeScript,唯一的运行时依赖是 quickjs-wasi;不依赖其他 pi 包。pi 内只有 coding-agent 的 codemode 工具(1,358 行)使用它,而它又是 MCP 工具的默认调用方式。1,837 lines of TypeScript; the only runtime dependency is quickjs-wasi, and no other pi package. Inside pi, only coding-agent's codemode tool (1,358 lines) uses it — which in turn is the default way MCP tools are called.

01Extensions

模块结构Module structure#

一句话结论Takeaway三块:宿主侧(runtime/host.ts 的 CodemodeSandbox 与 Execution、消息协议、wasm 加载)、沙箱内(runtime/worker.ts 与在 VM 里执行的 prelude-source.ts)、给模型看的文本(declarations.ts、source.ts、identifier.ts)。四个导入路径分别对应整体、声明渲染、源码解析与 worker 入口。Three blocks: the host side (CodemodeSandbox and Execution in runtime/host.ts, the message protocol, wasm loading), the sandbox (runtime/worker.ts and prelude-source.ts, which runs inside the VM), and model-facing text (declarations.ts, source.ts, identifier.ts). Four import paths cover the whole package, declaration rendering, source parsing and the worker entry.

宿主、沙箱内与给模型看的三组模块Host, in-sandbox and model-facing module groups
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
pi-codemode modules (1,837 lines of TypeScript)宿主侧(主线程)Host side (main thread)沙箱内(worker 线程)Inside the sandbox (worker thread)给模型看的文本Text for the model导入路径、依赖与使用者Import paths, dependencies and users416runtime/host.tsCodemodeSandbox · Execution51runtime/protocol.ts宿主 ↔ worker 消息host ↔ worker messages35wasm.tsloadQuickJSWasm():编译一次loadQuickJSWasm(): compile once138types.tsTool · Result · Error · Options161runtime/worker.tsQuickJS VM · bridge · drain512runtime/prelude-source.tsVM 内的 JS:tools · text · storeJS in the VM: tools · text · store355declarations.tsJSON Schema → TypeScript 声明JSON Schema → TypeScript declarations115source.ts// @options: 行 · Lark 语法// @options: line · Lark grammar12identifier.ts工具名 → JS 标识符tool name → JS identifier@earendil-works/pi-codemode@earendil-works/pi-codemode/declarations@earendil-works/pi-codemode/source@earendil-works/pi-codemode/workerquickjs-wasi 3.6.2coding-agent · extensions/codemode (1,358)postMessage 两边只传 JSON 字符串;Only JSON strings cross postMessage;VM 内没有 timers、fetch、process、require、模块。the VM has no timers, fetch, process, require or modules.index.ts(42 行)重新导出全部index.ts (42 lines) re-exports them allsrc/index.tssrc/declarations.tssrc/source.tssrc/runtime/worker.ts运行时依赖:runtime dep:(QuickJS 编译为 WASI wasm)(QuickJS compiled to WASI wasm)"./worker" 单独导出:打包后的宿主(如 Bun 编译的可执行文件)要把它作为独立入口,再通过 workerUrl 传入。"./worker" is exported on its own: bundled hosts (e.g. a Bun-compiled binary) make it a separate entry and pass it as workerUrl.标签 = 行数tag = lines of code宿主host沙箱sandbox模型可见的文本model-facing text

要点Key points

  • 最大的文件是 runtime/prelude-source.ts:它是一段字符串形式的 JavaScript,在 VM 里搭出 tools、text()、store() 等全部脚本 API。The largest file is runtime/prelude-source.ts: JavaScript held in a string that builds the whole script API — tools, text(), store() and the rest — inside the VM.
  • ./declarations 与 ./source 不加载 wasm,宿主可以只用它们生成工具描述或校验脚本,而不启动沙箱。./declarations and ./source do not load wasm, so a host can generate tool descriptions or validate scripts without starting a sandbox.
  • ./worker 单独导出,是为了打包场景:打包后的宿主把它作为独立入口,再通过 workerUrl 传给沙箱。./worker is exported separately for bundling: a bundled host makes it a separate entry and passes it to the sandbox as workerUrl.

工作原理How it works

  1. new CodemodeSandbox({ tools, globals, timeoutMs, memoryLimitBytes })。new CodemodeSandbox({ tools, globals, timeoutMs, memoryLimitBytes }).
  2. renderDeclarations({ tools, globals }) 生成放进工具描述的 TypeScript。renderDeclarations({ tools, globals }) produces the TypeScript that goes into the tool description.
  3. parseCodemodeSource(input) 拆出可选的 // @options: 行,再 sandbox.execute(code, { signal, store })。parseCodemodeSource(input) splits off an optional // @options: line, then sandbox.execute(code, { signal, store }).

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/codemode/src/index.tsL13export …包入口package entry
packages/codemode/src/runtime/host.tsL340CodemodeSandbox沙箱the sandbox
packages/codemode/src/runtime/worker.tsL52worker main()worker 入口worker entry
packages/codemode/src/runtime/prelude-source.tsL46PRELUDE_SOURCEVM 内的 APIthe in-VM API
packages/codemode/src/wasm.tsL21loadQuickJSWasm()wasm 加载wasm loading
02Extensions

公共 APIPublic API#

一句话结论Takeaway4 个导入路径。根入口从 7 个模块重新导出 19 个值和 16 个类型;核心只有一个类 CodemodeSandbox,其余是声明渲染、源码解析、限额常量和类型。4 import paths. The root re-exports 19 values and 16 types from 7 modules; the core is a single class, CodemodeSandbox, and the rest is declaration rendering, source parsing, limit constants and types.

导入路径Import path目标Target说明Notes
@earendil-works/pi-codemode./dist/index.js沙箱、类型、声明渲染、源码解析与限额the sandbox, types, declaration rendering, source parsing and limits
@earendil-works/pi-codemode/declarations./dist/declarations.js只有声明渲染,不加载 wasmdeclaration rendering only, no wasm
@earendil-works/pi-codemode/source./dist/source.js只有源码解析与语法source parsing and grammar only
@earendil-works/pi-codemode/worker./dist/runtime/worker.jsworker 入口,供打包时单独引用the worker entry, referenced separately when bundling

package.json · exportsL8

常用导出Key exports

模块Module常用导出Key exports用途Use
packages/codemode/src/runtime/host.tsCodemodeSandbox沙箱:execute(code)the sandbox: execute(code)
packages/codemode/src/types.tsCodemodeTool CodemodeResult工具与结果tools and results
packages/codemode/src/declarations.tsrenderDeclarations renderToolSignature schemaToType给模型的 TypeScriptTypeScript for the model
packages/codemode/src/source.tsparseCodemodeSource CODEMODE_SOURCE_GRAMMAR@options 与 Lark 语法@options and the Lark grammar
packages/codemode/src/identifier.tstoCodemodeIdentifier工具名 → 标识符tool name → identifier
packages/codemode/src/runtime/prelude-source.tsMAX_OUTPUT_CHARS MAX_STORE_VALUE_CHARS脚本限额script limits

index.ts 的全部导出(按来源模块)Every index.ts export, by source module

展开全部 7 个来源模块Show all 7 source modules19 values16 types
来源模块Source module值导出Value exports类型导出Type exports
packages/codemode/src/declarations.tsDEFAULT_INPUT_SCHEMA_MAX_CHARS MCP_TYPESCRIPT_PREAMBLE mcpStructuredContentSchema renderDeclarations renderToolOutputType renderToolSample renderToolSignature schemaToTypeRenderDeclarationsOptions
packages/codemode/src/identifier.tstoCodemodeIdentifier—
packages/codemode/src/runtime/host.tsCodemodeSandbox—
packages/codemode/src/runtime/prelude-source.tsMAX_OUTPUT_CHARS MAX_OUTPUT_ITEMS MAX_STORE_TOTAL_CHARS MAX_STORE_VALUE_CHARS—
packages/codemode/src/source.tsCODEMODE_OPTIONS_PREFIX CODEMODE_SOURCE_GRAMMAR CodemodeSourceError parseCodemodeSourceCodemodeSourceOptions ParsedCodemodeSource
packages/codemode/src/types.ts—CodemodeCall CodemodeCallStatus CodemodeError CodemodeErrorKind CodemodeExecuteOptions CodemodeJsonSchema CodemodeOutputItem CodemodeResult CodemodeSandboxOptions CodemodeStoreWrites +2
packages/codemode/src/wasm.tsloadQuickJSWasmCodemodeWasmModule

由构建脚本解析 export { … } from 语句生成。Generated by parsing the export { … } from statements at build time.

03Extensions

三层隔离Three layers of isolation#

一句话结论Takeaway宿主线程只持有工具表;每次 execute() 新建一个 worker 线程,worker 里再创建一个 QuickJS 虚拟机。VM 是独立的 wasm 实例,和宿主的 JavaScript 世界没有共享对象——脚本能做的唯一一件事,就是通过 prelude 闭包里的 bridge 请求宿主调用某个工具。The host thread holds only the tool table; each execute() creates a worker thread, and the worker creates a QuickJS VM. The VM is a separate wasm instance with no objects shared with the host's JavaScript world — the only thing a script can do is ask the host, through the bridge hidden in the prelude's closure, to call a tool.

宿主线程、worker 线程与 QuickJS 虚拟机The host thread, the worker thread and the QuickJS VM
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Three layers: host thread · worker thread · QuickJS VM宿主线程Host threadruntime/host.tsworker 线程Worker threadruntime/worker.tsQuickJS VM(独立的 wasm 实例)QuickJS VM (a separate wasm instance)CodemodeSandbox工具表 · globals · 默认值tool table · globals · defaults不持有 VMholds no VMExecution一次 execute() = 一个 worker + 一个 VMone execute() = one worker + one VM计时器 · pending 调用 · output · callstimer · pending calls · output · callsCodemodeTool.execute(args, { signal })真正的能力在这里(宿主侧)the real capability lives here (host side)QuickJS.create({ wasm, memoryLimit, maxStackSize, interruptHandler, wasi })wasm 模块由宿主编译后经 workerData 共享;引擎自己的 stdout/stderr 被丢弃the wasm module is compiled by the host and shared via workerData; the engine's own stdout/stderr is discardedbridge(kind, a, b, c)只接收原始值primitives onlycall · global · output · donePRELUDE_SOURCE先冻结所有内建对象(lockdown)first freezes every built-in (lockdown)bridge 藏在闭包里,脚本拿不到keeps bridge in a closure the script cannot reach(async (tools, console) => { <script> })脚本 = async 函数体:可以 return 和顶层 awaitscript = an async function body: return and top-level await worktoolsALL_TOOLStextimageexitconsolestoreloadsetTimeoutfetchprocessrequireimport结束时:On finish:1 中止所有未完成的工具调用1 abort every pending tool call2 Atomics.store(interrupt, 1)3 worker.terminate(),再 resolve 结果3 worker.terminate(), then resolveinterrupt 是共享的 4 字节;VM 的中断处理器轮询它,interrupt is a shared 4-byte buffer the VM's interrupt因为 Bun 的 terminate() 停不住在 wasm 里空转的线程。handler polls: Bun's terminate() cannot stop a thread spinning in wasm.每次执行都是新 worker:失控的脚本被直接杀掉,A fresh worker per run: a runaway script is killed不会污染下一次执行。outright and cannot poison the next run.脚本能看到的全部:Everything the script can see:+ 宿主配置的 globals(如 searchTools、models.*)+ host-configured globals (e.g. searchTools, models.*)没有:Not there:resultworker → 宿主:worker → host:"call""output""done""crash"宿主 → worker:host → worker:"result" { id, ok, payload }参数、结果、值都以 JSONArgs, results and values字符串跨越边界cross as JSON strings宿主线程host threadworker 线程worker threadQuickJS VM宿主 → workerhost → workerworker → 宿主worker → host

要点Key points

  • worker 的存在是为了不让空转的脚本卡住宿主线程;每次执行都用新 worker,失控的脚本直接被 terminate(),不会影响下一次。The worker exists so that a spinning script never blocks the host thread; a fresh worker per run means a runaway script is simply terminate()d and cannot affect the next run.
  • 结束时宿主先把共享的 4 字节 interrupt 置 1,再 terminate():VM 的中断处理器会轮询它,因为 Bun 的 worker.terminate() 停不住在 wasm 里空转的线程(见 runtime/protocol.ts)。On finish the host sets the shared 4-byte interrupt to 1 before terminate(): the VM's interrupt handler polls it, because Bun's worker.terminate() cannot stop a thread spinning in wasm (see runtime/protocol.ts).
  • maxStackSize 让深递归变成可捕获的 RangeError 而不是 wasm 陷阱;QuickJS 自己写到 fd 1/2 的诊断被丢弃,不会弄乱宿主的 TUI。maxStackSize turns deep recursion into a catchable RangeError instead of a wasm trap; QuickJS's own diagnostics on fd 1/2 are discarded so they never garble the host's TUI.
  • prelude 在脚本运行前冻结所有内建对象,因为它和脚本共用这些对象:被篡改的 toJSON 否则可能让 prelude 向宿主发出错误的数据。The prelude freezes every built-in before the script runs because it shares them with the script: a patched toJSON could otherwise make the prelude send the host malformed data.

工作原理How it works

  1. 宿主:new Worker(workerUrl, { workerData }),workerData 带上编译好的 wasm 模块(结构化克隆时共享代码)。Host: new Worker(workerUrl, { workerData }); workerData carries the compiled wasm module (structured clone shares the code).
  2. worker:QuickJS.create({ memoryLimit, maxStackSize, interruptHandler, wasi }) → vm.newFunction("bridge") → 求值 prelude。Worker: QuickJS.create({ memoryLimit, maxStackSize, interruptHandler, wasi }) → vm.newFunction("bridge") → evaluate the prelude.
  3. prelude 返回 { settle, run, stalled },worker 用它们驱动脚本。The prelude returns { settle, run, stalled }, which the worker uses to drive the script.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/codemode/src/runtime/host.tsL125Execution一次执行one run
packages/codemode/src/runtime/host.tsL323interrupt中断标志interrupt flag
packages/codemode/src/runtime/worker.tsL54QuickJS.create()创建 VMcreating the VM
packages/codemode/src/runtime/worker.tsL32discardOutput()丢弃引擎输出discarding engine output
packages/codemode/src/runtime/prelude-source.tsL56lockdown()冻结内建对象freezing built-ins
04Extensions

一次执行One execution#

一句话结论Takeawayexecute(code) 复制当前的工具表,启动计时器(默认 300,000 ms,包括工具耗时),加载 wasm,再启动 worker。worker 创建 VM、求值 prelude、把脚本包进 async 函数后运行。结局只有五种:成功,或 script、timeout、aborted、sandbox 四类错误;finish() 只执行一次。execute(code) copies the current tool table, starts the timer (default 300,000 ms, tool time included), loads wasm and starts the worker. The worker creates the VM, evaluates the prelude, wraps the script in an async function and runs it. There are only five endings: success, or one of four errors — script, timeout, aborted, sandbox; finish() runs exactly once.

一次 execute() 的步骤与五种结局The steps of one execute() and its five endings
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
One execute(code): from script text to CodemodeResult宿主:new Execution(…)Host: new Execution(…)工具表在此刻复制一份the tool table is copied at this momentworker:准备 VM 并运行Worker: prepare the VM and runruntime/worker.ts结束:finish() 只执行一次Finishing: finish() runs exactly onceexecute(code, opts)opts: signal · timeoutMs · storesetTimeout(300,000 ms)默认截止时间;Infinity = 不限default deadline; Infinity = noneloadQuickJSWasm()每个路径只读取并编译一次read and compiled once per pathnew Worker(url, workerData)code · tools · globals · wasm · store · interruptQuickJS.create()memoryLimit · maxStackSizeevalCode(PRELUDE_SOURCE)(bridge, tools, globals, store)evalCode(async wrapper)语法错误 → 直接 done(false)syntax error → done(false) at oncerun(fn) → drain()executePendingJobs + stalled()ok: truereturn 值或 exit()return value or exit()value · output · calls · storeWrites"script"脚本抛错或解析失败the script threw or failed to parsename · message · stack"timeout"截止时间到the deadline expiredworker 被终止worker terminated"aborted"signal 触发或 sandbox.close()signal fired or sandbox.close()worker 被终止worker terminated"sandbox"wasm 陷阱 · worker 退出 · 桥被破坏wasm trap · worker exit · broken bridge如脚本改了 toJSONe.g. a script patched toJSON之后每收到一个 "result" 消息:settle(id) → drain()。没有计时器和 I/O,所以"无 pending 调用却没结束"= 永远不会结束,stalled() 立即判失败。Then for every "result" message: settle(id) → drain(). With no timers or I/O, "not finished and no call pending" means it never will be, so stalled() fails it at once.失败的结果同样带着 output 与 calls(直到失败那一刻),只有成功的执行才报告 storeWrites。Failed results still carry output and calls (up to the failure); only successful runs report storeWrites.execute() 本身从不因脚本失败而 reject;只有在 sandbox 已关闭时 reject。execute() itself never rejects for script failures — only when the sandbox is already closed.宿主hostworker / VM成功successCodemodeError.kind

要点Key points

  • VM 里没有计时器和 I/O:如果脚本没结束、又没有挂起的工具调用,它就永远不会结束——stalled() 在每次 drain 后检查这种情况并立即判失败。The VM has no timers or I/O: if the script has not finished and no tool call is pending, it never will — stalled() checks for this after every drain and fails it at once.
  • 脚本包装成 (async (tools, console) => {<code>\n}),前缀和脚本同在第一行,所以报错的行号与脚本原文一致。The script is wrapped as (async (tools, console) => {<code>\n}), with the prefix on the script's first line so reported line numbers match the script as written.
  • execute() 不会因为脚本失败而 reject,失败以 { ok: false } 返回,并保留失败前的 output 与 calls。execute() never rejects for script failures; they come back as { ok: false } with the output and calls up to the failure.
  • loadQuickJSWasm() 每个路径只读取并编译一次;加载失败的 Promise 会被移出缓存,下次重试。loadQuickJSWasm() reads and compiles once per path; a failed load is evicted from the cache and retried next time.

工作原理How it works

  1. execute(code, { signal, timeoutMs, store }) → new Execution(…):计时器、abort 监听、等待 wasm。execute(code, { signal, timeoutMs, store }) → new Execution(…): timer, abort listener, wait for wasm.
  2. start() → new Worker();监听 message / error / exit。start() → new Worker(); listen for message / error / exit.
  3. 收到 done 后先解码值与 store 写入,再 finish():中止 pending 调用 → 置 interrupt → terminate → resolve。On done, decode the value and store writes first, then finish(): abort pending calls → set interrupt → terminate → resolve.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/codemode/src/runtime/host.tsL394execute()入口entry
packages/codemode/src/runtime/host.tsL22DEFAULT_TIMEOUT_MS默认截止时间default deadline
packages/codemode/src/runtime/host.tsL297finish()唯一的结束点the single exit
packages/codemode/src/runtime/worker.tsL148async wrapper脚本包装script wrapper
packages/codemode/src/runtime/prelude-source.tsL499stalled()死等检测stall detection
packages/codemode/src/types.tsL59CodemodeErrorKind4 类错误four error kinds
05Extensions

一次工具调用的往返One tool call round trip#

一句话结论Takeawayawait tools.x(args) 在 VM 里变成 bridge("call", id, name, argsJson),worker 把它转成 postMessage,宿主的 Execution 查到工具、执行、把结果以 result 消息送回,worker 调用 settle(id) 并运行挂起的任务,脚本从 await 处继续。两边之间只有 JSON 字符串。await tools.x(args) becomes bridge("call", id, name, argsJson) in the VM; the worker turns it into a postMessage; the host's Execution finds the tool, runs it and sends the result back as a result message; the worker calls settle(id) and runs pending jobs, and the script resumes after await. Only JSON strings cross between them.

一次工具调用在 VM、worker 与宿主之间的往返One tool call's round trip between VM, worker and host
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
One tool call round trip: await tools.x(args)脚本scriptpreludeworker.tsExecutionCodemodeTooltools.mcp__docs__search({ q })代理按 jsName 或原名找到工具;不存在 → TypeError + 相近名称提示proxy finds the tool by jsName or name; missing → TypeError with near-miss suggestionsbridge("call", id, name, argsJson)参数先 JSON.stringify;返回 Promise,记入 pendingargs JSON.stringify-ed; returns a Promise kept in pending{ type: "call", id, target: "tool", name, args }postMessageexecute(JSON.parse(args), { signal })calls[] 先记 "cancelled",完成后改为 ok / error 并记录耗时calls[] starts as "cancelled", becomes ok / error with its duration返回值(必须可 JSON 序列化)或抛错a value (JSON-serializable) or a throw{ type: "result", id, ok, payload }ok → JSON 文本 · 否则 → 错误消息ok → JSON text · else → the error messagesettle(id, ok, payload) → drain()executePendingJobs:脚本从 await 处继续executePendingJobs: the script resumes after awaitresolve(JSON.parse(payload)) · reject(new Error(msg))并发:Promise.all 同时发出的多个调用各有自己的 id,宿主并行执行;同一 id 出现两次 → "sandbox" 错误。Concurrency: calls issued together (Promise.all) each get an id and run in parallel on the host; a repeated id → a "sandbox" error.globals 走同一条路,只是 target: "global",且不记入 result.calls。Globals take the same path with target: "global", and are not recorded in result.calls.12345678请求方向request返回方向reply

要点Key points

  • 工具抛出的错误在脚本里变成同一条消息的 Error;返回值必须可 JSON 序列化。An error thrown by a tool becomes an Error with the same message in the script; return values must be JSON-serializable.
  • result.calls 记录每次工具调用:先记为 cancelled,完成后改成 ok 或 error 并记下耗时——脚本结束时仍未完成的调用保持 cancelled。result.calls records every tool call: it starts as cancelled and becomes ok or error with its duration — calls still running when the script ends stay cancelled.
  • tools 是一个代理:访问不存在的工具名会抛 TypeError,列出相近的名字(或在不超过 20 个工具时列出全部)。tools is a proxy: an unknown tool name throws a TypeError that lists near misses (or, with 20 tools or fewer, all of them).
  • globals(如 searchTools)走同一条路径,只是 target: "global",且不记入 calls。Globals (such as searchTools) take the same path with target: "global" and are not recorded in calls.

工作原理How it works

  1. prelude:caller("call", name) 生成函数 → JSON.stringify 参数 → bridge() → 返回挂进 pending 的 Promise。Prelude: caller("call", name) builds the function → JSON.stringify the args → bridge() → return a Promise kept in pending.
  2. 宿主:handleCall() → tool.execute(JSON.parse(args), { signal }) → post({ type: "result", id, ok, payload })。Host: handleCall() → tool.execute(JSON.parse(args), { signal }) → post({ type: "result", id, ok, payload }).
  3. worker:settle(id, ok, payload) → drain()。Worker: settle(id, ok, payload) → drain().

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/codemode/src/runtime/protocol.tsL30WorkerToHostMessageworker → 宿主worker → host
packages/codemode/src/runtime/protocol.tsL39HostToWorkerMessage宿主 → workerhost → worker
packages/codemode/src/runtime/host.tsL265handleCall()执行工具running the tool
packages/codemode/src/runtime/worker.tsL65bridgeVM 内的桥the in-VM bridge
packages/codemode/src/runtime/prelude-source.tsL457settle()恢复脚本resuming the script
06Extensions

脚本 API 与限额The script API and its limits#

一句话结论Takeaway脚本能用的只有:tools 与 ALL_TOOLS、输出函数 text() / image() / console.*、exit()、同步的 store() / load()、宿主配置的 globals,以及 return。输出、store 都有硬限额,超出时在脚本里抛错——宿主会一直保存输出直到脚本结束,没有限额的循环打印会拖垮宿主内存。All a script gets: tools and ALL_TOOLS, the output functions text() / image() / console.*, exit(), synchronous store() / load(), host-configured globals, and return. Output and the store have hard limits that throw inside the script — the host keeps all output until the script ends, so an unbounded print loop would exhaust host memory.

脚本 API、硬限额、锁定与保留名The script API, hard limits, lockdown and reserved names
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
What a script can use: the whole API the prelude builds, and its limits脚本可见的全局Script-visible globals由 PRELUDE_SOURCE 定义,全部只读defined by PRELUDE_SOURCE, all read-only限额Limits超出即抛错exceeding throwslockdown:先冻结,再运行脚本lockdown: freeze first, then run the script16 Mi chars100,000 items256 Ki chars1 Mi chars300,000 msmemoryLimitBytesMAX_STACK_SIZEtools.<id>(args) · tools["name"](args)Promise;名字不存在 → TypeError,附相近名称a Promise; unknown name → TypeError with near missesALL_TOOLS[{ name: <id>, description }]text(value)追加文本;非字符串先 JSON 序列化append text; non-strings are JSON-serializedimage(url | { image_url } | ImageContent)base64 data: URL;只认 PNG / JPEG / GIF / WebPbase64 data: URL; PNG / JPEG / GIF / WebP onlyconsole.log · info · warn · error · debug文本项,标记 console: truetext items marked console: trueexit()立即成功结束,value = undefinedfinish successfully now, value = undefinedstore(key, value) · load(key)同步;作用于传入的 JSON 快照;value = undefined 即删除synchronous, on the JSON snapshot passed in; undefined deletes<global>(args) · <ns>.<member>(args)宿主的 globals;spread: true 时传全部参数host globals; spread: true passes every argumentreturn <value>脚本是 async 函数体;返回值即 result.valuethe script is an async body; the value becomes result.value输出总字符(文本 + base64 图片)total output chars (text + base64 images)text / image / console 调用次数text / image / console calls单个 store 值的 JSONJSON of one store value全部 store 的 key + JSONkeys + JSON of the whole store默认截止时间(含工具耗时)default deadline (tool time included)默认不限(wasm32 上限 4 GiB)default: none (wasm32 caps at 4 GiB)深递归 → 可捕获的 RangeErrordeep recursion → a catchable RangeError所有内建对象被冻结、内建全局只读——脚本改不了 prelude 赖以序列化的 Array.prototype.toJSON 等。常被覆盖的属性改成访问器:Every built-in is frozen and built-in globals are read-only, so a script cannot patch what the prelude serializes with (e.g. Array.prototype.toJSON). Commonly overriddenconstructor · name · message · toString · toLocaleString · valueOf · toJSON,在实例上赋值仍然可用(如 Error 子类里 this.name = …)。properties (constructor · name · message · toString · toLocaleString · valueOf · toJSON) become accessors, so assigning them on instances still works.保留名(不能注册为 global):tools · ALL_TOOLS · console · text · image · exit · globalThis · store · loadReserved names (cannot be globals): tools · ALL_TOOLS · console · text · image · exit · globalThis · store · load

要点Key points

  • image() 接受 base64 的 data: URL、{ image_url } 对象或 MCP 的 ImageContent 块,并且只认 PNG、JPEG、GIF、WebP。image() accepts a base64 data: URL, an { image_url } object or an MCP ImageContent block, and only takes PNG, JPEG, GIF or WebP.
  • store() / load() 作用于传入的 JSON 快照;成功的执行把写入作为 storeWrites 交还宿主,持久化由宿主决定。store() / load() work on the JSON snapshot passed in; successful runs hand writes back as storeWrites, and persisting them is up to the host.
  • 冻结内建对象会破坏“在实例上覆盖原型属性”的普通写法,所以 name、message、toJSON 等常见属性被改成访问器,this.name = … 依然可用。Freezing built-ins would break ordinary code that overrides prototype properties on instances, so common ones such as name, message and toJSON become accessors and this.name = … still works.

工作原理How it works

  1. 宿主注册 globals 时,名字必须是标识符或 ns.member,且不能与保留名冲突。When the host registers globals, names must be identifiers or ns.member, and may not clash with the reserved names.
  2. ns.member 形式的 globals 被收进一个冻结的 ns 对象。Globals named ns.member are grouped into a frozen ns object.
  3. spread: true 的 global 收到全部参数组成的数组,而不是第一个参数。A spread: true global receives all arguments as an array instead of the first one.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/codemode/src/runtime/prelude-source.tsL40MAX_OUTPUT_CHARS输出上限output limit
packages/codemode/src/runtime/prelude-source.tsL32MAX_STORE_VALUE_CHARSstore 上限store limit
packages/codemode/src/runtime/prelude-source.tsL401image()图片输出image output
packages/codemode/src/runtime/prelude-source.tsL284store()storestore
packages/codemode/src/runtime/host.tsL24RESERVED_GLOBALS保留名reserved names
07Extensions

公共类型与源码格式Public types and the source format#

一句话结论TakeawayCodemodeTool 只要 name 和 execute(args, { signal });schema 只用来渲染声明,值不会被校验。CodemodeResult 是一个按 ok 区分的联合:成功带 value 与 storeWrites,失败带 CodemodeError;两者都带 output 与 calls。脚本源码可以以一行 // @options: 开头。CodemodeTool needs only name and execute(args, { signal }); schemas are used to render declarations, values are not validated against them. CodemodeResult is a union on ok: success carries value and storeWrites, failure a CodemodeError; both carry output and calls. Script source may start with a // @options: line.

公共类型与 @options 源码格式Public types and the @options source format
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Public types and the script source format脚本源码格式(source.ts)Script source format (source.ts)parseCodemodeSource()«interface»CodemodeTool+name+description?+inputSchema?+outputSchema?+spread?+signature?+execute«interface»CodemodeToolContext+signal«interface»CodemodeSandboxOptions+tools?+globals?+timeoutMs?+memoryLimitBytes?+wasm?+workerUrl?«interface»CodemodeExecuteOptions+signal?+timeoutMs?+store?«union»CodemodeResult+ok: true|value · output · calls · storeWrites+ok: false|error · output · calls«interface»CodemodeError+kind: "script" | "timeout" | "aborted" | "sandbox"+name? · message · stack?«interface»CodemodeCall+name · durationMs+status: "ok" | "error" | "cancelled"示例EXAMPLE// @options: {"max_output_tokens": 2000, "timeout_ms": 30000}const r = await tools.read({ path: "package.json" });text(JSON.parse(r).name);可选的第一行只认两个字段:The optional first line accepts two fields only:max_output_tokens:非负安全整数max_output_tokens: a non-negative safe integertimeout_ms:1 … 2,147,483,647(setTimeout 上限)timeout_ms: 1 … 2,147,483,647 (the setTimeout cap)该行被替换成空行,行号保持不变;其他字段或坏 JSON → CodemodeSourceError。The line is blanked so line numbers stay put; other fields or bad JSON → CodemodeSourceError.CODEMODE_SOURCE_GRAMMAR:给支持语法约束的模型的 Lark 语法。CODEMODE_SOURCE_GRAMMAR: a Lark grammar for grammar-constrained models.工具tools选项options结果results错误errors

要点Key points

  • @options 只认 max_output_tokens 与 timeout_ms 两个字段;该行被替换为空行,行号不变。@options accepts only max_output_tokens and timeout_ms; the line is replaced with a blank one so line numbers stay put.
  • CODEMODE_SOURCE_GRAMMAR 是一份 Lark 语法,给支持语法约束采样的模型:脚本可以直接按原文生成,而不是转义成 JSON 字符串。CODEMODE_SOURCE_GRAMMAR is a Lark grammar for models with grammar-constrained sampling: the script is generated as raw text instead of an escaped JSON string.
  • 工具的 signal 在脚本结束(包括没被 await 的调用)、超时、调用方中止或沙箱关闭时触发。A tool's signal fires when the script finishes (including unawaited calls), times out, the caller aborts, or the sandbox closes.

工作原理How it works

  1. parseCodemodeSource(input) → { code, options };坏的 options → CodemodeSourceError。parseCodemodeSource(input) → { code, options }; bad options → CodemodeSourceError.
  2. 把 options.timeoutMs 传给 execute(),maxOutputTokens 由宿主自己执行截断。Pass options.timeoutMs to execute(); maxOutputTokens is enforced by the host's own truncation.
  3. 检查 result.ok,再读 value / error、output 与 calls。Check result.ok, then read value / error, output and calls.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/codemode/src/types.tsL14CodemodeTool工具a tool
packages/codemode/src/types.tsL84CodemodeResult结果the result
packages/codemode/src/types.tsL94CodemodeSandboxOptions沙箱选项sandbox options
packages/codemode/src/source.tsL100parseCodemodeSource()源码解析source parsing
packages/codemode/src/source.tsL22CODEMODE_SOURCE_GRAMMARLark 语法Lark grammar
08Extensions

TypeScript 声明TypeScript declarations#

一句话结论Takeaway模型通过 TypeScript 声明学会调用工具:renderDeclarations() 把每个工具渲染成 declare const tools 的一个成员 name(args: T): Promise<R>;,描述变成文档注释,schemaToType() 把 JSON Schema 转成类型。MCP 风格的输出 schema 渲染成 CallToolResult<T>。The model learns to call tools from TypeScript declarations: renderDeclarations() renders each tool as a member name(args: T): Promise<R>; of declare const tools, descriptions become doc comments, and schemaToType() turns JSON Schema into types. MCP-style output schemas render as CallToolResult<T>.

从 JSON Schema 到 TypeScript 声明From JSON Schema to TypeScript declarations
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
The API the model sees: JSON Schema → TypeScript declarationsschemaToType() 的规则schemaToType() rulesrenderDeclarations(){ tools, globals }CodemodeTool(宿主注册)CodemodeTool (registered by the host){name: "mcp__docs__search-pages",description: "Search the documentation.",inputSchema: {type: "object",properties: {query: { type: "string" },limit: { type: "integer" }},required: ["query"]}}输出(换行仅为排版)OUTPUT (line breaks added for layout)declare const tools: {/** Search the documentation. */mcp__docs__search_pages(args: { limit?: number; query: string; }): Promise<unknown>;};"-" 不是合法标识符字符 → "_"(toCodemodeIdentifier)"-" is not an identifier character → "_" (toCodemodeIdentifier)属性按名字排序;不在 required 里 → 可选(?)properties are sorted; not in required → optional (?)脚本里两种写法都行:tools.mcp__docs__search_pages(…)scripts may call either tools.mcp__docs__search_pages(…)或 tools["mcp__docs__search-pages"](…)or tools["mcp__docs__search-pages"](…)标量scalarsstring · number / integer → number · boolean · null字面量literalsconst → 字面量;enum → 字面量联合const → a literal; enum → a union of literals组合combinatorsanyOf / oneOf → A | B;allOf → A & B(含 unknown 的部分丢弃)anyOf / oneOf → A | B; allOf → A & B (unknown parts dropped)对象objects无属性且无 additionalProperties → [key: string]: unknown;属性有 description → // 注释no properties or additionalProperties → [key: string]: unknown; described properties → // comments数组arraysitems → Array<T>;prefixItems → [A, B];都没有 → unknown[]items → Array<T>; prefixItems → [A, B]; neither → unknown[]$ref只解析本地 #/…;每个 schema 最多展开 32 次,递归 → unknownlocal #/… only; at most 32 expansions per schema, recursion → unknown长度size输入类型超过 16,000 个字符 → 整个变成 unknownan input type over 16,000 characters → unknown altogetherMCPoutputSchema 形如 CallToolResult → Promise<CallToolResult<T>>,需附 MCP_TYPESCRIPT_PREAMBLEan outputSchema shaped like CallToolResult → Promise<CallToolResult<T>>, with MCP_TYPESCRIPT_PREAMBLEglobalsname → declare function;ns.member → declare const ns: { … };signature 可直接给出name → declare function; ns.member → declare const ns: { … }; signature overrides模型可见的文本model-facing text渲染render

要点Key points

  • 工具名中不合法的标识符字符变成 _(toCodemodeIdentifier),所以 my-tool 在脚本里是 tools.my_tool;原名 tools["my-tool"] 同样可用。Characters that are not valid in identifiers become _ (toCodemodeIdentifier), so my-tool is tools.my_tool in scripts; the original tools["my-tool"] works too.
  • 渲染有上限:输入类型超过 16,000 个字符整个变成 unknown;本地 $ref 每个 schema 最多展开 32 次,递归引用变成 unknown。Rendering is bounded: an input type over 16,000 characters becomes unknown; local $refs expand at most 32 times per schema, and recursive references become unknown.
  • 用到 CallToolResult<T> 时要把 MCP_TYPESCRIPT_PREAMBLE(MCP 结果类型的定义)一起放进描述。When CallToolResult<T> is used, MCP_TYPESCRIPT_PREAMBLE (the MCP result type definitions) must go into the description too.

工作原理How it works

  1. renderToolSignature(tool):输入类型 + renderToolOutputType(outputSchema)。renderToolSignature(tool): the input type + renderToolOutputType(outputSchema).
  2. mcpStructuredContentSchema() 识别 MCP 结果 schema(content 数组、布尔 isError、对象 _meta)。mcpStructuredContentSchema() recognises MCP result schemas (a content array, boolean isError, object _meta).
  3. renderToolSample() 输出“描述 + 单个工具的声明”,用于工具列表与 ALL_TOOLS 条目。renderToolSample() outputs “description + that tool's declaration”, used for tool listings and ALL_TOOLS entries.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/codemode/src/declarations.tsL105renderDeclarations()整体声明all declarations
packages/codemode/src/declarations.tsL138renderToolSignature()单个工具one tool
packages/codemode/src/declarations.tsL228schemaToType()Schema → 类型schema → type
packages/codemode/src/declarations.tsL18MCP_TYPESCRIPT_PREAMBLEMCP 类型MCP types
packages/codemode/src/identifier.tsL5toCodemodeIdentifier()名字 → 标识符name → identifier
09Extensions

在 pi 中的使用How pi uses it#

一句话结论Takeawaycoding-agent 把它包装成 codemode 工具:模型写脚本,脚本里的每次嵌套调用都经过 agent 循环的工具管线(校验、钩子、权限),而只有脚本自己的输出回到模型。pi 还注入 searchTools()、describeTool()、describeNamespace() 与 models.*,并把 store() 写入保存为会话条目。coding-agent wraps it as the codemode tool: the model writes a script, every nested call goes through the agent loop's tool pipeline (validation, hooks, permissions), and only the script's own output returns to the model. pi also injects searchTools(), describeTool(), describeNamespace() and models.*, and saves store() writes as session entries.

codemode 工具:嵌套调用、globals 与 store 条目The codemode tool: nested calls, globals and store entries
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
How pi uses pi-codemode: coding-agent's codemode tool脚本可调用的工具Tools a script can callpi 注入的 globalsGlobals pi injectsstore() 的持久化Persisting store()激活与模式Activation and modes模型the model写一段 JavaScriptwrites JavaScriptcodemode(source)exposure: "model-only":脚本不能再启动脚本exposure: "model-only": scripts cannot start scriptsnew CodemodeSandbox({ tools, globals })每次调用新建 · memoryLimit 256 MiBnew per call · memoryLimit 256 MiBctx.executeTool(name, args, { signal })经过 agent 循环的工具管线:校验 · tool_call / tool_result 钩子 · 权限through the agent loop's pipeline: validation · tool_call / tool_result hooks · permissionscustomType: "codemode-store" · { set, delete }当前激活的 direct 工具 + 所有 codemode / deferred 曝光的工具(多为 MCP)active direct tools + every codemode / deferred-exposure tool (mostly MCP)嵌套结果怎样交给脚本:How nested results reach the script:有 outputSchema → structuredContent(MCP 工具 → CallToolResult,含 isError)has outputSchema → structuredContent (MCP tools → CallToolResult, incl. isError)否则 → 文本内容拼成一个字符串otherwise → its text content as one string失败 / 被阻止 / 参数无效 → reject(Error(错误文本))failed / blocked / invalid → reject(Error(error text))只有脚本自己的输出回到模型;嵌套结果不会。Only the script's own output reaches the model; nested results do not.输出按 max_output_tokens 截断(默认 10,000),完整输出存文件。Output is cut to max_output_tokens (default 10,000); the full output is saved to a file.searchTools(query)BM25 排序搜索可调用工具BM25-ranked search over callable toolsdescribeTool(name)取一个工具的完整声明one tool's full declarationdescribeNamespace(name)命名空间说明(如 MCP 服务器 instructions)namespace notes (e.g. MCP server instructions)models.*模型目录 · classify · generateImages;同时最多 4 个model catalog · classify · generateImages; at most 4 in flight脚本里 load() / store() 同样可用(见下)load() / store() are available too (below)成功的脚本把写入追加为会话条目:Successful scripts append their writes as a session entry:下一次执行前,从根到当前叶子重放这些条目,得到 load() 的快照——Before the next run, the entries from root to the current leaf are replayed into the load()因此每个分支只看到自己路径上写入的值。snapshot, so each branch sees only values written on its own path.超时:只有 // @options 里的 timeout_ms;否则不限(Infinity)。Timeout: only // @options timeout_ms; otherwise none (Infinity).注册时是 inactive;--tools、defaultTools、setActiveTools(),Registered inactive; --tools, defaultTools, setActiveTools(),或 MCP 扩展(codemode 曝光的服务器)会激活它。or the MCP extension (codemode-exposure servers) activate it.codemode.mode = "on"(默认):列出非 direct 的可调用工具codemode.mode = "on" (default): lists the non-direct callable tools"only":列出全部,并对模型隐藏 direct 工具的声明"only": lists all of them and hides direct tools from the modelinlineBudget(默认 3,000 token):描述里内联多少声明inlineBudget (default 3,000 tokens): how many declarations are inlined支持的 provider 用 Lark 语法约束采样:脚本按原文输出。Providers with grammar sampling use the Lark grammar: scripts come out as raw text.extensions/codemode/:1,358 行extensions/codemode/: 1,358 linescoding-agentpi-codemode会话session

要点Key points

  • 脚本可调用 loadout 中所有可调用的工具(包括 deferred 与 codemode 曝光的工具——MCP 工具默认在此),只有 codemode 自己除外。它是 model-only,脚本不能再启动脚本。Scripts can call every callable tool in the loadout (deferred and codemode-exposure tools included — where MCP tools go by default), except codemode itself. It is model-only: scripts cannot start scripts.
  • 嵌套结果交给脚本时:有 outputSchema 的工具给 structuredContent(带 isError 的 MCP 结果也一样),其他工具给文本,失败则 reject。Nested results reach the script as: structuredContent for tools with an outputSchema (MCP results with isError too), text for other tools, and a rejection on failure.
  • store() 写入成为 codemode-store 自定义条目;下次执行前从根到当前叶子重放,所以每个分支只看到自己路径上的值。store() writes become codemode-store custom entries; before the next run they are replayed from root to the current leaf, so each branch sees only values from its own path.
  • 沙箱在第一次调用时才加载(worker 与 wasm),启动时没有成本;超时只来自脚本的 timeout_ms,否则不限。The sandbox (worker and wasm) loads on the first call, so start-up pays nothing; the only timeout is the script's timeout_ms, otherwise none.

工作原理How it works

  1. createCodemodeExtension() 注册不激活的 codemode 工具;--tools、defaultTools、setActiveTools() 或 MCP 扩展激活它。createCodemodeExtension() registers the inactive codemode tool; --tools, defaultTools, setActiveTools() or the MCP extension activate it.
  2. 激活时 prepareCodemodeLoadout() 用可调用工具的声明替换描述(inlineBudget 控制内联多少;mode: "only" 列出全部可调用工具,并从请求中去掉激活的 direct 工具的声明)。On activation, prepareCodemodeLoadout() replaces the description with the callable tools' declarations (inlineBudget limits how much is inlined; mode: "only" lists every callable tool and leaves active direct tools' declarations out of requests).
  3. executeCodemode():解析源码 → new CodemodeSandbox({ tools, globals }) → execute(code, { signal, store }) → 截断输出、追加 store 条目。executeCodemode(): parse the source → new CodemodeSandbox({ tools, globals }) → execute(code, { signal, store }) → truncate output, append store entries.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/coding-agent/src/extensions/codemode/index.tsL31createCodemodeExtension()扩展入口extension entry
packages/coding-agent/src/extensions/codemode/tool.tsL380createCodemodeToolDefinition()工具定义tool definition
packages/coding-agent/src/extensions/codemode/tool.tsL342prepareCodemodeLoadout()描述与模式description and modes
packages/coding-agent/src/extensions/codemode/execute.tsL462ctx.executeTool()嵌套调用nested calls
packages/coding-agent/src/extensions/codemode/execute.tsL554createDiscoveryGlobals()searchTools 等searchTools and friends
packages/coding-agent/src/extensions/codemode/tool.tsL53CODEMODE_STORE_ENTRY_TYPEstore 条目store entries
10Extensions

扩展点Extension points#

CodemodeToolname + execute(args, { signal }):脚本里的 tools.<id>。name + execute(args, { signal }): tools.<id> in scripts.
globals顶层函数或 ns.member,如 pi 的 searchTools、models.*。Top-level functions or ns.member, like pi's searchTools and models.*.
execute(code, { store })传入 load() 的快照,取回 storeWrites 自行持久化。Pass the load() snapshot in, take storeWrites out and persist them yourself.
renderDeclarations() · renderToolSample()自己组织给模型的工具描述。Build your own tool descriptions for the model.
CODEMODE_SOURCE_GRAMMAR给支持语法约束的 provider,让脚本按原文生成。For providers with grammar sampling, so scripts come out as raw text.
wasm · workerUrl打包或 Bun 编译时自带 wasm 与 worker 入口。Ship your own wasm and worker entry when bundling or compiling with Bun.
timeoutMs · memoryLimitBytes每个沙箱或每次执行的资源上限。Resource limits per sandbox or per run.
11Extensions

测试Tests#

一句话结论Takeaway三个测试文件:沙箱(真实的 worker 与 QuickJS,覆盖工具调用、超时、中止、限额与锁定)、声明渲染与源码解析。Three test files: the sandbox (a real worker and QuickJS, covering tool calls, timeouts, aborts, limits and lockdown), declaration rendering and source parsing.

用例数 = 源码中 it( / test( 的出现次数(静态统计,不运行测试)。Cases = occurrences of it( / test( in the source (static count; tests were not run).

12Extensions

交叉链接Cross-links#