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

180 lines
10 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 并污染它——我踩过,已回滚并清理干净)。正确姿势:
```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="$PWD/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"` 从当前 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),可直接复用。