aps-agent/poc/pi-fallback/NOTES-pi-runtime.md

10 KiB
Raw Blame History

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

两个必须知道的坑【实测】:

  1. 进程退出码永远是 0,即使 LLM 调用失败!必须在 JSONL 里检查最后一条 assistant 消息的 stopReason("stop"=正常 / "error"=失败,失败时带 errorMessage)。
  2. 失败会自动重试 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-coding provider(模型 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 的话(最容易踩的坑)

  1. 退出码不可信:pi headless 失败也退 0。判成败只能解析 JSONL 里 stopReason / errorMessage,这是我们 PoC「不许把失败包装成成功」价值观的直接落点。
  2. npm 会向上找 package.json:任何 npm 操作前确认 cwd 里有自己的 package.json,否则会污染工作区根的 package.json(我踩过,已回滚;根 package.json 里残留的 docx 改动是我接手前就有的,别动它)。
  3. npm 不在 Git Bash PATH:用 "$(dirname "$(which node)")/npm.cmd" 从当前 Node 安装目录动态解析。
  4. 自动重试 3 次:LLM 网关不通时一次 -p 调用会拖 ~14s(2/4/8s 退避),编排器超时 < 15s 会误判为挂起;建议超时常量 ≥ 60s 且识别 auto_retry_start 事件做提前熔断。
  5. 配置隔离用 PI_CODING_AGENT_DIR:指向 poc 内的 runtime/pi-home/。注意我观察到一次原因未完全确定的 ~/.pi 空骨架创建(已清理;复现测试显示设置 env 后 --list-models 不会再写 ~/.pi)——建议 Agent-B 起进程时显式在 env 里传该变量并断言 ~/.pi 不出现新文件。
  6. apiKey: "!命令" 会执行 shell:models.json 这个特性本身是代码执行面,我们的 models.json 只用字面量/env 变量名。
  7. callId 现成可用:JSONL 的 tool_execution_start/end 和扩展 tool_call 事件都带 toolCallId,tool_bridge.py 的「伪造成果无 callId」测试可以直接围绕它设计。
  8. 墙要自己砌:pi 无原生沙箱,白名单(--tools)+ tool_call 钩子(路径校验)只是前两道,文件圈禁和网络限制必须在 Python sandbox 层做。
  9. 探针文件都在 poc/pi-fallback/probes/(guard-ext.ts、headless-json.out、ext-loaded.marker),可直接复用。