180 lines
11 KiB
Markdown
180 lines
11 KiB
Markdown
# 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 完整路径 | `D:\AppData\Kimi\resources\resources\runtime\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 并污染它——我踩过,已回滚并清理干净)。正确姿势:
|
||
|
||
```bash
|
||
# 在工作区根目录(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 事件流 —— 一条命令拿结构化输出【实测】
|
||
|
||
```bash
|
||
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 协议的长驻会话【推断】
|
||
|
||
```bash
|
||
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):
|
||
|
||
```js
|
||
// 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 内:
|
||
|
||
```bash
|
||
export PI_CODING_AGENT_DIR="D:\ItemSpace\14.工业智核\BH-PPT\aps-agent\poc\pi-fallback\runtime\pi-home"
|
||
# 然后 models.json 放在 $PI_CODING_AGENT_DIR/models.json
|
||
```
|
||
|
||
可直接照抄的 `models.json`(已实测被 `--list-models` 识别,baseUrl 换成真实网关即可):
|
||
|
||
```json
|
||
{
|
||
"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)【实测】:
|
||
```bash
|
||
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 工具白名单(启动参数,最简单)【实测标志可用】
|
||
```bash
|
||
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`):
|
||
```bash
|
||
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"`,即 `D:\AppData\Kimi\resources\resources\runtime\node\npm.cmd`。
|
||
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),可直接复用。
|