10 KiB
NOTES-pi-runtime — Pi Agent 运行时侦察报告(Agent-A)
侦察日期:2026-09-02。侦察员:Agent-A。 每条结论标注【实测】= 在本机跑过命令验证;【推断】= 来自包内官方文档/源码注释,未实际调用验证。 安装版本:
@mariozechner/pi-coding-agent@0.73.0(含 pi-agent-core / pi-ai / pi-tui 同版本)。
0. 本机环境结论
| 项 | 结果 | 标注 |
|---|---|---|
| Node | v24.15.0 ✅(要求 >= 22.12.0) | 【实测】 |
| npm | 11.12.1 ✅,但 不在 Git Bash PATH 上 | 【实测】 |
| npm 完整路径 | 通过当前 Node 安装目录动态解析 npm.cmd(与 node.exe 同目录) |
【实测】 |
| Git Bash | 存在(pi 的 bash 工具在 Windows 上依赖它) | 【实测】 |
⚠️ Agent-B 在 Python 里拉起 npm/node 时,直接用 node 可以(在 PATH),但调 npm 必须用上面的完整路径或把该目录加进 PATH。
1. 安装(可直接照抄)
不要全局安装,不要在没有 package.json 的目录里安装(npm 会向上找到工作区根的 package.json 并污染它——我踩过,已回滚并清理干净)。正确姿势:
# 在工作区根目录(Git Bash)执行:
mkdir -p poc/pi-fallback/runtime
cd poc/pi-fallback/runtime
# 先写 package.json,锚定 npm 的安装位置
cat > package.json <<'EOF'
{ "name": "pi-fallback-runtime", "private": true, "version": "0.0.0" }
EOF
NPM="$(dirname "$(which node)")/npm.cmd"
"$NPM" install @mariozechner/pi-coding-agent --no-audit --no-fund
结果:190 个包,约 10 秒装好,全部落在 poc/pi-fallback/runtime/node_modules/,不碰产品代码。【实测】
CLI 入口:poc/pi-fallback/runtime/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
(npm 也会生成 node_modules/.bin/pi / pi.cmd,Windows 下用 node .../cli.js 最稳)【实测】
2. 关键问题 a:headless 调用方式
有三种,按 PoC 推荐度排序:
方式 1(首选):print 模式 + JSON 事件流 —— 一条命令拿结构化输出【实测】
node poc/pi-fallback/runtime/node_modules/@mariozechner/pi-coding-agent/dist/cli.js \
--offline --no-session \
--model <provider>/<model-id> \
--mode json \
-p "你的任务描述"
-p / --print:非交互,处理完即退出【实测】--mode json:stdout 输出 JSONL 事件流,每行一个事件【实测】- 事件序列实测为:
session头 →agent_start→turn_start→message_start/update/end→tool_execution_start/end(有工具调用时)→turn_end→agent_end(agent_end.messages含全部消息,取最后一条 assistant 消息的 text 即最终答案) - 实测样例留档:
poc/pi-fallback/probes/headless-json.out
两个必须知道的坑【实测】:
- 进程退出码永远是 0,即使 LLM 调用失败!必须在 JSONL 里检查最后一条 assistant 消息的
stopReason("stop"=正常 /"error"=失败,失败时带errorMessage)。 - 失败会自动重试 3 次(
auto_retry_start,退避约 2s/4s/8s,共 ~14 秒)。编排器超时预算要把重试算进去,或在事件流里识别auto_retry_*事件提前熔断。
方式 2:RPC 模式 —— stdin/stdout JSON 协议的长驻会话【推断】
pi --mode rpc # stdin 发 {"id":"req-1","type":"prompt","message":"..."},stdout 收事件流
适合需要多轮 steer/followUp 的场景。协议细节见包内 docs/rpc.md(1400 行,很全)。
注意:RPC 帧只能按 \n 切分,不能用 Node readline(它会错误切 U+2028/2029)。
方式 3:Node SDK 内嵌 —— Python 编排器拉起一个 Node 脚本【推断(文档完整,未实测调用)】
pi-agent-core 可被 import,最小脚本(照抄自包内 docs/sdk.md Quick Start):
// poc/pi-fallback/runtime/run-task.mjs
import { AuthStorage, createAgentSession, ModelRegistry, SessionManager }
from "@mariozechner/pi-coding-agent";
const authStorage = AuthStorage.create(); // 默认读 ~/.pi/agent/auth.json
const modelRegistry = ModelRegistry.create(authStorage); // 默认读 ~/.pi/agent/models.json
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
authStorage, modelRegistry,
});
session.subscribe((event) => {
process.stdout.write(JSON.stringify(event) + "\n"); // 转发给 Python 编排器
});
await session.prompt(process.argv[2]);
也可用 AuthStorage.create(path) / ModelRegistry.create(auth, path) 指定 poc 内的配置文件,做到完全隔离。
3. 关键问题 b:自定义模型 provider(OpenAI 兼容)
传言属实:配置文件就是 ~/.pi/agent/models.json【实测】,且可用环境变量 PI_CODING_AGENT_DIR 把配置目录整体改到别处【实测】——PoC 用它把配置圈在 poc 内:
export PI_CODING_AGENT_DIR="$PWD/poc/pi-fallback/runtime/pi-home"
# 然后 models.json 放在 $PI_CODING_AGENT_DIR/models.json
可直接照抄的 models.json(已实测被 --list-models 识别,baseUrl 换成真实网关即可):
{
"providers": {
"aps-local": {
"baseUrl": "http://127.0.0.1:9/v1",
"api": "openai-completions",
"apiKey": "dummy-key-for-poc",
"models": [
{
"id": "qwen3-32b-local",
"name": "Qwen3 32B Local",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
"maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
}
]
}
}
}
验证方式(不需要真实 LLM)【实测】:
node .../cli.js --offline --list-models
# 输出表格中应出现: aps-local qwen3-32b-local 32.8K 4.1K no no
要点【推断,来自 docs/models.md】:
api选openai-completions即可覆盖绝大多数 OpenAI 兼容端点;也支持anthropic-messages/openai-responses/google-generative-ai。apiKey三种取值:字面量 / 环境变量名(写"MY_KEY"则读 env)/"!shell命令"(执行命令取 stdout——⚠️ 这是代码执行面,PoC 里建议只用字面量或 env)。- 调用时用
--model aps-local/qwen3-32b-local选中【实测】。 - 意外收获:pi 内置了
kimi-codingprovider(模型k2p6、kimi-for-coding)【实测自 --list-models 输出】,若手上有 Kimi/Moonshot key 可零配置使用。 - 更复杂的 provider(OAuth、自定义流式)可用扩展
pi.registerProvider(),见 docs/custom-provider.md——PoC 用不上。
4. 关键问题 c:工具与权限机制
内置工具清单【推断,docs/usage.md 明确列出】
read、bash、edit、write、grep、find、ls —— 共 7 个。
第一道墙:CLI 工具白名单(启动参数,最简单)【实测标志可用】
pi -p --tools read,grep,find,ls "任务" # 只读模式:禁掉 bash/edit/write
pi -p --no-tools "任务" # -nt,禁所有工具(纯文本分析)
pi -p --no-builtin-tools "任务" # -nbt,只留扩展/自定义工具
--tools 是白名单语义,同时作用于内置/扩展/自定义工具。已实测 --tools read 启动正常(无 LLM 无法观察运行时过滤效果,过滤行为本身标【推断】)。
第二道墙:扩展的 tool_call 钩子(可编程拦截,能 block)【半实测】
- 已实测:headless 模式下扩展正常加载、
session_start事件触发(留档probes/ext-loaded.marker,探针扩展源码probes/guard-ext.ts)。 - 文档明确(docs/extensions.md §Tool Events):
pi.on("tool_call", ...)在工具执行前触发,返回{ block: true, reason: "..." }即可拦截;event.input还可原地改写工具参数;event.toolCallId天然就是我们要的 callId。拦截逻辑本身因无真实 LLM 未能端到端跑通,标【推断】但文档语义明确、官方示例(permission-gate.ts、protected-paths.ts)即用此机制。
可直接照抄的守卫扩展(探针实测版,见 probes/guard-ext.ts):
pi -p -e probes/guard-ext.ts --tools read,grep,find,ls "任务"
圈禁与网络限制的边界(重要)【推断】
- pi 自身没有目录圈禁/沙箱:read/write/edit/bash 都能访问任意路径,扩展也以用户完整权限运行(docs/extensions.md 明示 "Extensions run with your full system permissions")。
- 因此「文件圈禁、网络限制、进程回收」这三层墙必须由编排器/sandbox 侧实现(Agent-B 的 sandbox.py:cwd 圈禁 + tool_call 钩子校验路径 + 子进程杀进程树),不能指望 pi 原生能力。
- bash 工具在 Windows 走 Git Bash,可在 settings.json 用
"shellPath"改【推断,docs/windows.md】。
5. 给 Agent-B 的话(最容易踩的坑)
- 退出码不可信:pi headless 失败也退 0。判成败只能解析 JSONL 里
stopReason/errorMessage,这是我们 PoC「不许把失败包装成成功」价值观的直接落点。 - npm 会向上找 package.json:任何 npm 操作前确认 cwd 里有自己的 package.json,否则会污染工作区根的 package.json(我踩过,已回滚;根 package.json 里残留的
docx改动是我接手前就有的,别动它)。 - npm 不在 Git Bash PATH:用
"$(dirname "$(which node)")/npm.cmd"从当前 Node 安装目录动态解析。 - 自动重试 3 次:LLM 网关不通时一次
-p调用会拖 ~14s(2/4/8s 退避),编排器超时 < 15s 会误判为挂起;建议超时常量 ≥ 60s 且识别auto_retry_start事件做提前熔断。 - 配置隔离用
PI_CODING_AGENT_DIR:指向 poc 内的runtime/pi-home/。注意我观察到一次原因未完全确定的~/.pi空骨架创建(已清理;复现测试显示设置 env 后--list-models不会再写~/.pi)——建议 Agent-B 起进程时显式在 env 里传该变量并断言~/.pi不出现新文件。 apiKey: "!命令"会执行 shell:models.json 这个特性本身是代码执行面,我们的 models.json 只用字面量/env 变量名。- callId 现成可用:JSONL 的
tool_execution_start/end和扩展tool_call事件都带toolCallId,tool_bridge.py 的「伪造成果无 callId」测试可以直接围绕它设计。 - 墙要自己砌:pi 无原生沙箱,白名单(--tools)+ tool_call 钩子(路径校验)只是前两道,文件圈禁和网络限制必须在 Python sandbox 层做。
- 探针文件都在
poc/pi-fallback/probes/(guard-ext.ts、headless-json.out、ext-loaded.marker),可直接复用。