- 删除 docs/plan 下 00–18 的计划、评审、验收、发布记录与升级证据,docs/spikes 与 spikes 验证代码
(保留 spikes/movieclaw-src 本地克隆),以及 4 份一次性审查 / 调研;全部可在 ceb66f8 查到
- MP 对照移到 docs/porting/moviepilot-v3-comparison.md;经验教训改为 docs/plan/lessons.md(补第 25 条)
- docs/plan/README.md:文档放哪、当前状态、待办
- 引用改写:代码注释指向在维护的文档,其余注明已删除与 git 位置;清掉原有 6 个失效链接
- README 开头改为当前状态与入口;CHANGELOG 补齐 0.3.8–0.3.14 与工程变更
Co-Authored-By: Claude Opus 5.5 <[email protected]>
94 lines
7.6 KiB
Markdown
94 lines
7.6 KiB
Markdown
# ADR-003:Pi 接入采用 pi-agent-core `AgentHarness`,封装在 `AgentRuntimePort` 之后
|
||
|
||
- 状态:已采纳(2026-09-27,W0-B spike 后)
|
||
- 关联:REQ §10、§11.4;plan §1、§3.1-9;验证记录 spike `w0b-pi`(已删除,见 git `ceb66f8`);代码 `spikes/pi/`(已删除,见 git `ceb66f8`)
|
||
|
||
## 决定
|
||
|
||
1. 依赖锁精确版本:`@earendil-works/[email protected]`、`@earendil-works/[email protected]`,另加它们要求的 `[email protected]`。**不引入** `@earendil-works/pi-coding-agent`,因为它的 ResourceLoader 会做隐式发现、自带 bash/read 工具,并依赖 TUI 和 jiti 扩展加载。
|
||
2. 使用 core 的 **`AgentHarness` / `AgentLane`**。它就是 0.87.1 里 REQ §10.1 所说的 AgentSession 层:持久会话、事件流、压缩、abort/resume、工具重放策略都在这一层;而且只使用显式传入的 tools/resources/systemPrompt,不读取任何目录。
|
||
3. 所有 SDK 类型只出现在 `packages/agent/src/pi/*` 里,对外只暴露 `AgentRuntimePort`。
|
||
4. 测试统一使用 pi-ai 自带的 `fauxProvider`,配合 `FauxResponseFactory` 按 transcript 决策;生产中的上游用 `createProvider` 显式构造,不依赖环境变量。
|
||
|
||
## AgentRuntimePort 草案
|
||
|
||
```ts
|
||
export interface AgentRunSpec {
|
||
agentRunId: string; // = agent_run.id,同时作为 pi session id
|
||
goal: string; // 用户目标或事件派生的有界任务
|
||
scope: { actorId: string; scopeIds: string[]; grantIds: string[] };
|
||
capabilities: CapabilityId[]; // 本次允许的能力白名单(能力注册中心解析)
|
||
model: { upstreamId: string; modelId: string; thinkingLevel: ThinkingLevel };
|
||
budget: { maxTurns: number; maxTokens: number; maxCostUsd?: number; deadlineAt: Date };
|
||
compaction: { enabled: boolean; reserveTokens: number; keepRecentTokens: number };
|
||
evidenceRefs: string[];
|
||
}
|
||
|
||
export type AgentRunEvent =
|
||
| { type: "text_delta"; seq: number; delta: string }
|
||
| { type: "tool_started"; seq: number; capabilityId: string; invocationId: string; args: unknown }
|
||
| { type: "tool_finished"; seq: number; capabilityId: string; invocationId: string; isError: boolean; summary: string; recovery?: true }
|
||
| { type: "usage"; seq: number; input: number; output: number; costUsd: number }
|
||
| { type: "compaction"; seq: number; reason: "manual" | "threshold" | "overflow"; status: string }
|
||
| { type: "run_finished"; seq: number; status: "completed" | "aborted" | "failed" | "budget_exceeded"; error?: string };
|
||
|
||
export interface AgentRuntimePort {
|
||
createRun(spec: AgentRunSpec): Promise<{ agentRunId: string }>; // 建会话 + accept prompt,不驱动
|
||
stream(agentRunId: string, fromSeq?: number): AsyncIterable<AgentRunEvent>; // SSE 用,事件先落库(带 seq)再推送
|
||
drive(agentRunId: string, signal?: AbortSignal): Promise<AgentRunOutcome>; // 由持久任务调用;内部是 lane.resume / drive
|
||
abort(agentRunId: string, reason: string): Promise<void>; // lane.abort → 工具收到 context.abortSignal
|
||
resume(agentRunId: string): Promise<AgentRunOutcome>; // 进程重启后:AgentHarness.create 返回 open → lane.resume
|
||
followUp(agentRunId: string, message: string): Promise<void>; // 用户纠正
|
||
}
|
||
```
|
||
|
||
实现要点:
|
||
|
||
- `drive/resume` 由 `packages/tasks` 的持久任务(例如 `agentRun` 任务)调用。聊天 HTTP 只做 createRun + stream,断线不影响执行(REQ §10.7)。
|
||
- 事件监听器(`harness.events.on`)把事件写入 `agent_run_event`(seq 单调递增),SSE 按 seq 补读。
|
||
- 预算:`usage` 事件累计,`before_request` hook 里发现超预算就 `abort`,状态记为 `budget_exceeded`,然后进入 attention。
|
||
- 会话存储:v0.1 用 `JsonlSessionRepo`,目录固定为应用数据目录 `data/agent-sessions/`(不放在媒体目录或用户目录),`agent_run` 行记录 session id 和路径。`NodeExecutionEnv` 只当作这个目录的 FileSystem 用,**不把它的 exec/shell 暴露成工具**。PG 存储要等上游或自研 `Storage` 实现(接口约 10 个方法);格式仍是 WIP,所以不在 v0.1 做。
|
||
- 业务真相放在 PG(`agent_run`、操作账本、能力调用记录),JSONL 只是会话转写和恢复点。REQ §10.5/§11.4 说"不能把压缩摘要当唯一状态",这里同理。
|
||
|
||
## 工具白名单机制(三层)
|
||
|
||
1. **注册层**:`AgentHarness.create({ tools })` 只传本次 `capabilities` 对应的工具;也可以用 `activeToolNames` / `lane.setActiveTools` 缩小范围。模型调用未注册的工具时,harness 合成 `Tool "x" is unavailable` 错误结果,不会执行(spike 中已验证 `bash`)。
|
||
2. **授权层**:工具 `execute` 调能力服务。能力服务在服务端校验 `toolContext`(actorId、grants、scope)和持续/单次授权(REQ §10.3),不信任模型参数里的"已同意"。`before_tool` hook 用来记审计和做快速拒绝(`{ block: { reason } }`)。
|
||
3. **命名映射**:能力 ID `download.submit` ↔ 工具名 `download__submit`,因为上游工具名只允许 `[a-zA-Z0-9_-]`。映射表在 `packages/agent` 里唯一定义。
|
||
|
||
每个能力还要声明 `replay`:只读能力(`catalog.lookup`、`inventory.query`、`task.inspect` 等)用 `"safe"`;有副作用的能力(`download.submit`、`organize.execute`、`policy.apply` 等)用 `"never"`。崩溃恢复时,副作用工具会得到"结果未知"。业务侧按服务端生成的幂等键到操作账本对账,模型重新调用时会命中去重,不会产生第二次副作用。
|
||
|
||
## fake provider 用法
|
||
|
||
```ts
|
||
const faux = fauxProvider({ provider: "umt-fake", models: [{ id: "fake", contextWindow: 32000 }] });
|
||
models.setProvider(faux.provider);
|
||
faux.setResponses([
|
||
() => fauxAssistantMessage([fauxText("先查库存"), fauxToolCall("inventory__query", { title: "Dune" })], { stopReason: "toolUse" }),
|
||
(ctx) => /* 读 ctx.messages 里的 toolResult 决定下一步 */ fauxAssistantMessage("完成"),
|
||
]);
|
||
// faux.state.callCount、getPendingResponseCount();队列耗尽时返回 errorMessage "No more faux responses queued"
|
||
```
|
||
|
||
`packages/agent` 提供 `createScriptedModel(steps)`,供 testkit 和 e2e 使用(plan §3.1-13,"把 X 电影 1080p 收进来")。
|
||
|
||
## 设置页 → SDK 映射
|
||
|
||
| 设置项 | SDK |
|
||
| --- | --- |
|
||
| 上游接入(Anthropic / OpenAI 兼容) | `createProvider({ id, auth: { apiKey: { resolve } }, models: [Model], api: anthropicMessagesApi() / openAICompletionsApi() })`;`Model.baseUrl`(Anthropic 不带 `/v1`,OpenAI 兼容带 `/v1`)、`Model.id`、`Model.compat` |
|
||
| 模型选择 | `lane.setModel({ provider, modelId })` |
|
||
| 思考档位 | `thinkingLevel` / `lane.setThinkingLevel`;可选值来自 `getSupportedThinkingLevels(model)` |
|
||
| 上下文预算 | 自定义 `Model.contextWindow` / `maxTokens` |
|
||
| 压缩策略 | `compaction: { enabled, reserveTokens, keepRecentTokens }` + `before_compaction` hook / `customInstructions`(保留目标、约束、证据引用、已完成动作、剩余事项) |
|
||
| 重试 | `retry: { enabled, maxRetries, baseDelayMs }`、`streamOptions.timeoutMs` |
|
||
| 子代理开关 | **SDK 不提供**。由我们实现一个 `agent.delegate` 能力:开新 lane 或新会话,继承父任务的授权上限、预算和取消状态;默认关闭 |
|
||
|
||
## 已知限制
|
||
|
||
- 0.x 版本,存储格式 4 仍是 WIP,可能原地改变。锁精确版本,升级后跑 agent 与 server 的 Pi 相关测试(`deno task test:all`)。
|
||
- 没有子代理,没有 PG 会话存储,`watchSession` 未实现。
|
||
- Pi 不提供权限系统(REQ §10.2)。进程、容器、网络白名单要在部署层落实。
|
||
- 模型流中途崩溃后按"合成 error + 重试"恢复,只读了规范,未实测;v0.1 的 e2e 要补上。
|
||
- 真实上游只验证了请求形状(本地抓包),没有联网调用。
|