v1.1.0 Pi v1.1.0 源码分析 Analysis of Pi v1.1.0 · 2026-10-07
codemode codemode
沙箱化 JavaScript 执行:唯一能力是调用注入的工具 Sandboxed JavaScript execution where the only capability is calling injected tools
Extensions
← → 翻页 · F 全屏 · O 目录 · T 明暗 · L 语言 ← → navigate · F fullscreen · O overview · T theme · L language
01 · 模块结构 01 · Module structure Extensions
模块结构 Module structure 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 users 416 runtime/host.ts CodemodeSandbox · Execution 51 runtime/protocol.ts 宿主 ↔ worker 消息 host ↔ worker messages 35 wasm.ts loadQuickJSWasm():编译一次 loadQuickJSWasm(): compile once 138 types.ts Tool · Result · Error · Options 161 runtime/worker.ts QuickJS VM · bridge · drain 512 runtime/prelude-source.ts VM 内的 JS:tools · text · store JS in the VM: tools · text · store 355 declarations.ts JSON Schema → TypeScript 声明 JSON Schema → TypeScript declarations 115 source.ts // @options: 行 · Lark 语法 // @options: line · Lark grammar 12 identifier.ts 工具名 → JS 标识符 tool name → JS identifier @earendil-works/pi-codemode @earendil-works/pi-codemode/declarations @earendil-works/pi-codemode/source @earendil-works/pi-codemode/worker quickjs-wasi 3.6.2 coding-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 all src/index.ts src/declarations.ts src/source.ts src/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
最大的文件是 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.
02 · 三层隔离 02 · Three layers of isolation Extensions
三层隔离 Three layers of isolation Three layers: host thread · worker thread · QuickJS VM 宿主线程 Host thread runtime/host.ts worker 线程 Worker thread runtime/worker.ts QuickJS VM(独立的 wasm 实例) QuickJS VM (a separate wasm instance) CodemodeSandbox 工具表 · globals · 默认值 tool table · globals · defaults 不持有 VM holds no VM Execution 一次 execute() = 一个 worker + 一个 VM one execute() = one worker + one VM 计时器 · pending 调用 · output · calls timer · pending calls · output · calls CodemodeTool.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 discarded bridge(kind, a, b, c) 只接收原始值 primitives only call · global · output · done PRELUDE_SOURCE 先冻结所有内建对象(lockdown) first freezes every built-in (lockdown) bridge 藏在闭包里,脚本拿不到 keeps bridge in a closure the script cannot reach (async (tools, console) => { <script> }) 脚本 = async 函数体:可以 return 和顶层 await script = an async function body: return and top-level await work tools ALL_TOOLS text image exit console store load setTimeout fetch process require import 结束时: On finish: 1 中止所有未完成的工具调用 1 abort every pending tool call 2 Atomics.store(interrupt, 1) 3 worker.terminate(),再 resolve 结果 3 worker.terminate(), then resolve interrupt 是共享的 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: result worker → 宿主: worker → host: "call" "output" "done" "crash" 宿主 → worker: host → worker: "result" { id, ok, payload } 参数、结果、值都以 JSON Args, results and values 字符串跨越边界 cross as JSON strings 宿主线程 host thread worker 线程 worker thread QuickJS VM 宿主 → worker host → worker worker → 宿主 worker → host
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.
03 · 一次执行 03 · One execution Extensions
一次执行 One execution One execute(code): from script text to CodemodeResult 宿主:new Execution(…) Host: new Execution(…) 工具表在此刻复制一份 the tool table is copied at this moment worker:准备 VM 并运行 Worker: prepare the VM and run runtime/worker.ts 结束:finish() 只执行一次 Finishing: finish() runs exactly once execute(code, opts) opts: signal · timeoutMs · store setTimeout(300,000 ms) 默认截止时间;Infinity = 不限 default deadline; Infinity = none loadQuickJSWasm() 每个路径只读取并编译一次 read and compiled once per path new Worker(url, workerData) code · tools · globals · wasm · store · interrupt QuickJS.create() memoryLimit · maxStackSize evalCode(PRELUDE_SOURCE) (bridge, tools, globals, store) evalCode(async wrapper) 语法错误 → 直接 done(false) syntax error → done(false) at once run(fn) → drain() executePendingJobs + stalled() ok: true return 值或 exit() return value or exit() value · output · calls · storeWrites "script" 脚本抛错或解析失败 the script threw or failed to parse name · message · stack "timeout" 截止时间到 the deadline expired worker 被终止 worker terminated "aborted" signal 触发或 sandbox.close() signal fired or sandbox.close() worker 被终止 worker terminated "sandbox" wasm 陷阱 · worker 退出 · 桥被破坏 wasm trap · worker exit · broken bridge 如脚本改了 toJSON e.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. 宿主 host worker / VM 成功 success CodemodeError.kind
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.
04 · 一次工具调用的往返 04 · One tool call round trip Extensions
一次工具调用的往返 One tool call round trip One tool call round trip: await tools.x(args) 脚本 script prelude worker.ts Execution CodemodeTool tools.mcp__docs__search({ q }) 代理按 jsName 或原名找到工具;不存在 → TypeError + 相近名称提示 proxy finds the tool by jsName or name; missing → TypeError with near-miss suggestions bridge("call", id, name, argsJson) 参数先 JSON.stringify;返回 Promise,记入 pending args JSON.stringify-ed; returns a Promise kept in pending { type: "call", id, target: "tool", name, args } postMessage execute(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 message settle(id, ok, payload) → drain() executePendingJobs:脚本从 await 处继续 executePendingJobs: the script resumes after await resolve(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. 1 2 3 4 5 6 7 8 请求方向 request 返回方向 reply
工具抛出的错误在脚本里变成同一条消息的 Error;返回值必须可 JSON 序列化。 An error thrown by a tool becomes an Error with the same message in the script; return values must be JSON-serializable.
05 · 脚本 API 与限额 05 · The script API and its limits Extensions
脚本 API 与限额 The script API and its limits 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 throws lockdown:先冻结,再运行脚本 lockdown: freeze first, then run the script 16 Mi chars 100,000 items 256 Ki chars 1 Mi chars 300,000 ms memoryLimitBytes MAX_STACK_SIZE tools.<id>(args) · tools["name"](args) Promise;名字不存在 → TypeError,附相近名称 a Promise; unknown name → TypeError with near misses ALL_TOOLS [{ name: <id>, description }] text(value) 追加文本;非字符串先 JSON 序列化 append text; non-strings are JSON-serialized image(url | { image_url } | ImageContent) base64 data: URL;只认 PNG / JPEG / GIF / WebP base64 data: URL; PNG / JPEG / GIF / WebP only console.log · info · warn · error · debug 文本项,标记 console: true text items marked console: true exit() 立即成功结束,value = undefined finish successfully now, value = undefined store(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 argument return <value> 脚本是 async 函数体;返回值即 result.value the 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 值的 JSON JSON of one store value 全部 store 的 key + JSON keys + JSON of the whole store 默认截止时间(含工具耗时) default deadline (tool time included) 默认不限(wasm32 上限 4 GiB) default: none (wasm32 caps at 4 GiB) 深递归 → 可捕获的 RangeError deep 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 overridden constructor · 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 · load Reserved names (cannot be globals): tools · ALL_TOOLS · console · text · image · exit · globalThis · store · load
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.
06 · 公共类型与源码格式 06 · Public types and the source format Extensions
公共类型与源码格式 Public types and the source format 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 integer timeout_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
@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.
07 · TypeScript 声明 07 · TypeScript declarations Extensions
TypeScript 声明 TypeScript declarations The API the model sees: JSON Schema → TypeScript declarations schemaToType() 的规则 schemaToType() rules renderDeclarations() { 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"](…) 标量 scalars string · number / integer → number · boolean · null 字面量 literals const → 字面量;enum → 字面量联合 const → a literal; enum → a union of literals 组合 combinators anyOf / 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 数组 arrays items → Array<T>;prefixItems → [A, B];都没有 → unknown[] items → Array<T>; prefixItems → [A, B]; neither → unknown[] $ref 只解析本地 #/…;每个 schema 最多展开 32 次,递归 → unknown local #/… only; at most 32 expansions per schema, recursion → unknown 长度 size 输入类型超过 16,000 个字符 → 整个变成 unknown an input type over 16,000 characters → unknown altogether MCP outputSchema 形如 CallToolResult → Promise<CallToolResult<T>>,需附 MCP_TYPESCRIPT_PREAMBLE an outputSchema shaped like CallToolResult → Promise<CallToolResult<T>>, with MCP_TYPESCRIPT_PREAMBLE globals name → declare function;ns.member → declare const ns: { … };signature 可直接给出 name → declare function; ns.member → declare const ns: { … }; signature overrides 模型可见的文本 model-facing text 渲染 render
工具名中不合法的标识符字符变成 _(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.
08 · 在 pi 中的使用 08 · How pi uses it Extensions
在 pi 中的使用 How pi uses it How pi uses pi-codemode: coding-agent's codemode tool 脚本可调用的工具 Tools a script can call pi 注入的 globals Globals pi injects store() 的持久化 Persisting store() 激活与模式 Activation and modes 模型 the model 写一段 JavaScript writes JavaScript codemode(source) exposure: "model-only":脚本不能再启动脚本 exposure: "model-only": scripts cannot start scripts new CodemodeSandbox({ tools, globals }) 每次调用新建 · memoryLimit 256 MiB new per call · memoryLimit 256 MiB ctx.executeTool(name, args, { signal }) 经过 agent 循环的工具管线:校验 · tool_call / tool_result 钩子 · 权限 through the agent loop's pipeline: validation · tool_call / tool_result hooks · permissions customType: "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 tools describeTool(name) 取一个工具的完整声明 one tool's full declaration describeNamespace(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 model inlineBudget(默认 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 lines coding-agent pi-codemode 会话 session
脚本可调用 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.
扩展点 Extension points pi-coding-agent
codemode 的扩展点 Where to plug into codemode 1
CodemodeTool CodemodeTool
name + execute(args, { signal }):脚本里的 tools.<id>。 name + execute(args, { signal }): tools.<id> in scripts.
2
globals globals
顶层函数或 ns.member,如 pi 的 searchTools、models.*。 Top-level functions or ns.member, like pi's searchTools and models.*.
3
execute(code, { store }) execute(code, { store })
传入 load() 的快照,取回 storeWrites 自行持久化。 Pass the load() snapshot in, take storeWrites out and persist them yourself.
4
renderDeclarations() · renderToolSample() renderDeclarations() · renderToolSample()
自己组织给模型的工具描述。 Build your own tool descriptions for the model.
5
CODEMODE_SOURCE_GRAMMAR CODEMODE_SOURCE_GRAMMAR
给支持语法约束的 provider,让脚本按原文生成。 For providers with grammar sampling, so scripts come out as raw text.