Files
UHD_Media_Tool/docs/adr/ADR-003-pi-integration.md
T
rootandClaude Opus 5.5 b343dddbf6 docs: 清理过程文档,CHANGELOG 补齐 0.3.8–0.3.14
- 删除 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]>
2026-10-02 16:08:09 +08:00

94 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# 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 要补上。
- 真实上游只验证了请求形状(本地抓包),没有联网调用。