v1.1.0Pi v1.1.0 源码分析Analysis of Pi v1.1.0 · 2026-10-07

codemodecodemode

沙箱化 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

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.001
01 · 模块结构01 · Module structureExtensions

模块结构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 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

最大的文件是 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.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.002
02 · 三层隔离02 · Three layers of isolationExtensions

三层隔离Three layers of isolation

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

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.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.003
03 · 一次执行03 · One executionExtensions

一次执行One execution

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

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.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.004
04 · 一次工具调用的往返04 · One tool call round tripExtensions

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

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

工具抛出的错误在脚本里变成同一条消息的 Error;返回值必须可 JSON 序列化。An error thrown by a tool becomes an Error with the same message in the script; return values must be JSON-serializable.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.005
05 · 脚本 API 与限额05 · The script API and its limitsExtensions

脚本 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 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

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.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.006
06 · 公共类型与源码格式06 · Public types and the source formatExtensions

公共类型与源码格式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 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

@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.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.007
07 · TypeScript 声明07 · TypeScript declarationsExtensions

TypeScript 声明TypeScript declarations

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

工具名中不合法的标识符字符变成 _(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.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.008
08 · 在 pi 中的使用08 · How pi uses itExtensions

在 pi 中的使用How pi uses it

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

脚本可调用 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.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.009
扩展点Extension pointspi-coding-agent

codemode 的扩展点Where to plug into codemode

1
CodemodeToolCodemodeTool

name + execute(args, { signal }):脚本里的 tools.<id>。name + execute(args, { signal }): tools.<id> in scripts.

2
globalsglobals

顶层函数或 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_GRAMMARCODEMODE_SOURCE_GRAMMAR

给支持语法约束的 provider,让脚本按原文生成。For providers with grammar sampling, so scripts come out as raw text.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.010
←
1 / 10