Propose the N2 implementation plan: context, protocol, and tools #42

Merged
weichen merged 1 commits from feature/agent-stack-n2-plan into main 2026-10-07 12:38:28 +00:00
@@ -0,0 +1,148 @@
# 方案:N2 上下文、协议与工具——现状核验与增量实施
日期:2026-10-07。上游:[2026-10-02-agent-stack-completion.md](2026-10-02-agent-stack-completion.md)「N2 上下文、协议与工具」节(范围与三条验收的唯一权威)、[2026-10-02-agent-stack-audit.md](../records/2026-10-02-agent-stack-audit.md)(审计项逐条对当前代码复核后引用)。
基线 pin:agent-native `ead690b`、backend `68addfd`、agent-squad `029edc0`、agent-intent `343b1e6`(均为已合入 main)。本文所有 file:line 以这些 SHA 为准;实施时先复核再引用(与 N1/Q1/I1 同一纪律:审计与本文都不是证据,当前代码才是)。
依赖状态:N1/Q1/I1/I2 全部闭卡(任务卡归档)。总提案依赖序「N1/Q1/I1 → N2/Q2/I2;N3 在 N2 之后」满足,N2 开工。
## 一、现状核验(六路侦察 + 关键处亲验)
### 1. 上下文与压缩
库层三件已就位且配对安全:
- **会话压缩**:`runtime/session/compact.go:78-146`;`alignToolTail`(compact.go:148-154)回退 tailStart 至工具结果组之外,头段整体换摘要,不留孤立调用。回归:`TestR2CompactKeepsToolPair`、`TestCompactLeavesNoOrphanToolCall`(runtime/session/r2_regression_test.go:12-46,105-162)。
- **微压缩**:`runtime/session/microcompact.go:44-70` 只改写旧工具结果正文为转录指针占位,不增删消息、不动配对(loop.go:759-776 接入)。
- **适配层自动压缩**:`adapters/native/backend.go:95-103,412-470`。
- **提醒**:`runtime/reminders/reminders.go:70-97` 并入最后一条消息,不新建 user 消息(审计 N22 已修)。
仍开放:
- **产品侧裁切破坏配对(验收②的真缺口)**:`modules/backend/internal/ctxasm/finish.go:200-242` `trimOldest→oldestDrop` 逐条丢弃「系统前缀后第一条」,无工具配对意识——丢掉带 tool_calls 的 assistant 会留下孤立结果,丢掉结果会留下悬空调用。渲染阶段(ctxasm.go:174-277)本是配对安全的,裁切在其后发生,把安全性破坏掉。
- **旧工具结果没有落盘路径**:微压缩占位指向转录调用 ID(microcompact.go:58-68),不是文件路径;总提案 N2 行「运行内把旧工具结果换成路径」(ZCode compact 模式)未实现。
- **缓存友好无契约**:全仓无 `cache_control`;系统文本+工具清单的稳定性是事实性而非设计(registry.go:136-148 注册序、loop.go:380-401 拼接),无测试钉住;backend 有 PrefixHash 观测(finish.go:252-277)但无稳定契约测试。
- **`alignToolTail` 无直接单测**(仅经 Store.Compact 端到端覆盖,边界形态靠偶然)。
- filestate 读后才能写只在 agent-native 路径(backend.go:210);产品直连路径不过 `WorkspaceToolsTracked`(llm_direct.go:191-193)——划归 Q3 收敛范畴(见边界节)。
### 2. Anthropic 原生协议(审计 N4 仍开放)
- 线上 Client 只讲 OpenAI chat:`runtime/llm/client.go:14,71,76` 固定 POST `…/chat/completions` + Bearer。
- `Protocol` 枚举五方言(providers.go:123-129),`ClientFor` 只实现 `openai-chat`,其余 fail-closed(providers.go:201-208)。审计 N4 的「选了不能用」已被缓解:anthropic/google 预设改指 OpenAI 兼容端点(providers.go:143,146)——但**原生方言不存在**。
- 离线组包 `anthropicMessages`(providers.go:320-352)丢 tool_use/tool_result、不挂 tools(providers.go:270-289);`Message`(types.go:48-57)无 thinking/signature 存储,签名块无法往返。
- 无录制 HTTP 夹具机制(无 testdata/;现有测试全是内联 httptest 脚本)。
- 审计 N4 验证结论:**部分缓解、主体仍开放**。
### 3. 延迟工具 / tool_search / 代码编排(基座已在,剩收口)
已就位(agent-native):`ExposureDeferred` 两级暴露(runtime/tools/registry.go:56-73)、`tool_search`/`call_tool` 元工具(deferred.go:13-68)、激活持久化(deferred.go:94-128 → `.agentx/activated-tools.json`)、MCP 延迟连接 `StartBackground`(manager.go:124-135)与 >8 工具自动 deferred(manager.go:197-207)、代码编排 `run_tool_script`(code_tool.go:14;codeorch/host.go 进程外执行、中间结果不进上下文、50 次上限、禁名单)。backend 双路径接线:`runtime.codeOrchestration` 默认关(settings.go:95;app.go:241 agent-native 路径;codeorch_tool.go:15-78 + runmgr.go:348-355 直连路径)。
仍开放/风险:
- 激活恢复只记名字,激活工具永不进 `Decls()`(deferred.go:100 注释为有意设计,call_tool 间接层即机制)——**保持,不扩**。
- 代码编排在两处各有一份实现(库 code_tool.go vs 产品 codeorch_tool.go),开关两点判定,可漂移;N2 只做「同一语义」契约测试,不做合并(合并属 Q3 退役直连路径时自然发生)。
- 审计 N30(native.Options 单件)仍在;本阶段不做大重构,只在新端口上不再加字段。
### 4. MCP(审计 N3/N18/N19 已修;2026-07-28 未对齐)
- **验收③已满足**:stdio 单读循环按 id 分发(stdio.go:192-231),取消只 dropPending;回归 `TestStdioSlowCancelThenEcho`(mcp_test.go:78-105)断言 `Readers()==1`。审计 N3 关闭;N18(重连一次,manager.go:243-250)、N19(HTTP 会话 DELETE,http.go:95-119)同闭。
- **未对齐 2026-07-28**:`ProtocolVersion="2024-11-05"`(protocol.go:16)且服务端协商版本从不检查;`callToolResult` 无 `resultType`(protocol.go:138-141);`input_required` 只是成功结果里的内容块特例(ask.go:20-27),应答直接当终态返回(manager.go:254-264),不**重试原请求**;ask 端口(ask.go:7-18)接的是自由输入而非审批面;`tasks/get|update|cancel` 客户端面无;工具 schema 透传无 2020-12 声明(manager.go:174-177 合成默认 schema 时也未声明)。
### 5. skills 校验(开放规范未对齐)
- 现校验仅:目录名字符集 `[a-zA-Z0-9_-]`、64KiB 截断、描述 200 字截断、每根 100 个(runtime/skills/skills.go:22-26,140-186)。frontmatter 只按行前缀扫 `description:`(有意不引入 YAML 依赖,skills.go:4);`name:` 被忽略(目录名权威);未知键静默通过;不合规范式静默跳过(skills.go:79-81)。
- 开放规范差距:name 必填且须 kebab-case 并与目录同名、description 必填(规范上限 1024)、已知键白名单(license/compatibility/metadata/allowed-tools)、字段形态校验、**错误上浮**而非静默。
- backend 侧零校验:种子(internal/skills/skills.go:52-127)与插件技能落盘(plugins/host_v2.go:56-74)都不验;内置 recipes 只有 `description:`,缺 `name:`(dogfood 欠账)。
### 6. 子代理后台/转录/恢复
- **后台机制产品层已有**:`spawnBackground`(runmgr.go:1447-1474,上限 3、detached ctx、完成入 notice 队列、空闲合并投递);模型经 delegate 工具 `background:true` 触发(llm_direct.go:459-492)。但 `AgentSpec.Background`(agentspec.go:28,156-161)**除测试外无消费者**;notice 队列是内存 map(runmgr.go:1487-1494),重启即丢。
- **无独立转录**:子 run 共享父会话 messages 表、仅 `run_id` 分区(runmgr.go:1352,1426;store/messages.go:101-116 排除);SubLLMRunner 直连 `nruntime.Run`(sub_loop.go:98),不建 workDir、不写 .agentx。
- **零恢复**:`ReconcileInterrupted`(runmgr.go:1606-1634)对子 run 只 seal 成 interrupted,无续跑/重跑路径;恢复测试只覆盖会话 run。
- `WorktreeIsolation/OutputSchema`(agentspec.go:29-30)declared-only——本阶段保持,记为边界。
### 7. 审计 N2(并行工具事件竞争)——已修,仅引用
`serializeHostCallbacks`(loop.go:468-480,Run 入口 :170 调用)用一把锁串行 OnEvent/BeforeTool/AfterTool;`GOWORK=off go test -race ./runtime/` 全绿(本方案亲验,2026-10-07)。不再安排工作,作为既有证据引用。
## 二、工作包划分
| WP | 内容 | 主要仓 | 对应验收 |
|---|---|---|---|
| WP1 | 上下文配对收口 + 工具结果落盘 + 前缀稳定契约 | backend + agent-native | ② |
| WP2 | Anthropic 原生客户端 + 录制夹具端到端 | agent-native | ① |
| WP3 | MCP 2026-07-28 对齐(版本协商 / resultType / input_required 重试 / approval 接线) | agent-native + backend | ③(回归引用) |
| WP4 | skills 开放规范校验 + dogfood + 插件路径复用 | agent-native + backend | — |
| WP5 | 子代理:Background 消费、独立转录、恢复与通知持久化 | backend(+agent-native 小面) | — |
| WP6 | 装配收口 + A/B 验收证据 + 级联闭链 | backend + Dev | ①②③ 总验 |
依赖:WP1 先行(验收②、风险最小、产品直接受益);WP2 与 WP3/WP4 可并行;WP5 独立;WP6 收全部。每个 WP 走「库 PR → backend PR → 级联 → Dev gitlink PR」既有链式,逐 WP 合入,不做长分支。
### WP1 上下文配对收口
1. **ctxasm 裁切配对对齐**:`oldestDrop` 改为工具组对齐——候选索引落在 tool 结果组内或带 tool_calls 的 assistant 上时,整组同退(对齐 `alignToolTail` 语义,但方向是丢弃侧)。测试:构造带多轮工具调用的装配输出,裁切后断言(a)无孤立 tool_call(每个 call 有结果)、(b)无悬空结果(每个结果有 call)、(c)`Report.Truncations` 仍记录。**这是验收② 的机器证据。**
2. **旧工具结果落盘**:微压缩清理时把旧结果写入 `<workDir>/.agentx/results/<toolCallID>.txt`,占位改为路径指引(「完整内容见 <路径>」);保留 keepLastN 语义。库内实现 + 单测(写盘、占位、配对不变、文件可被 fs 工具读回)。
3. **前缀稳定契约测试**:钉住系统文本与 `Decls()` 顺序在同一注册序列下逐字节确定(含 tool_search/call_tool 注入位置);backend 侧钉 `PrefixHash` 在同输入下不变。只测不改行为。
4. **`alignToolTail` 直接单测**:tail 恰落多调用 assistant、keepLastN 大于工具组、全为 tool 角色三种边界。
### WP2 Anthropic 原生客户端
1. **客户端**:`ClientFor` 增 `ProtocolAnthropic` 分支——POST `{base}/messages`,头 `x-api-key` + `anthropic-version`,SSE 解析 Anthropic 事件族(`message_start`/`content_block_start|delta|stop`/`message_delta`;`text_delta`/`input_json_delta`/`thinking_delta`/`signature_delta`),映射到既有 `Event` 类型;stop_reason `tool_use`→StopToolCalls、`end_turn`→StopEnd。
2. **消息形态**:`anthropicMessages` 重写——assistant `tool_use` 块(id/name/input)、user `tool_result` 块(tool_use_id/content)、`tools` 数组(name/description/input_schema);`Message` 增 thinking 块与 `signature`(含 `redacted_thinking`)存储与往返;system 与 tools 挂 `cache_control`(原生路径上的缓存友好,呼应 WP1-3)。
3. **预设**:`anthropic` 预设切到 `ProtocolAnthropic`(用户预期即原生);compat shim 经 `models.json` 自定义仍可配。`BuildBody` 的 Anthropic 分支与线上客户端统一(同一组包函数,消灭「离线/线上两张皮」的 N4 根因)。
4. **录制夹具(验收①)**:新增 `runtime/llm/testdata/anthropic/` 录制字节流(非真实密钥、非线上依赖):一轮完整工具调用——带 tools 请求 → SSE 含 `tool_use` → `tool_result` 请求 → 终文本。端到端断言:`ClientFor(Lookup("anthropic"))` 走通全程;第二次请求体中 thinking 块带原签名(签名往返);请求头含 `anthropic-version`。
5. 审计 N4 随本 WP 关闭(预设即真实客户端;未实现协议仍不出现在预设)。
### WP3 MCP 2026-07-28 对齐
1. **版本协商**:通告 `2026-07-28`;读取服务端 `initializeResult.protocolVersion`,按协商版本分流(低版本回落现行行为,不留半新半旧)。
2. **resultType**:`callToolResult` 增判别字段;`input_required` 升格为协议级结果类型——经 ask 端口取得应答后**用原参数重试 `tools/call`**(不再把应答当终态内容返回);重试次数有界(防循环)。
3. **approval 接线**:ask 端口在 backend 接到审批/确认卡面(复用 N1 的 HITL 卡与 PermissionAsk 通路),自由输入与批准两类语义分清。
4. **tasks 客户端面**:`Client` 增 `tasks/get|update|cancel` 方法 + 双传输实现 + 假服务器用例;产品侧映射只做最小集(run 中断时对进行中的 MCP task 发 `tasks/cancel`),完整后台任务映射记录为后续项(N3 持久化阶段)。
5. **schema 2020-12**:合成默认 schema 时声明 `$schema: 2020-12`;透传保持不透明(不重写方言,注释记为有意)。
6. 回归保持:`TestStdioSlowCancelThenEcho`(验收③)全程原样通过,作为 WP3 不得破坏的既有证据。
### WP4 skills 开放规范校验
1. **严格子集解析器**:坚持不引入 YAML 依赖(skills.go:4 的既有约束),手写支持规范字段形态的子集(标量、`metadata` 字符串映射、`allowed-tools` 空格分隔列表);形态不合法即错误,不猜测。
2. **校验规则**:`name` 必填 + kebab-case(小写、1–64、无连续/首尾连字符)+ 与目录同名;`description` 必填、≤1024(上限对齐规范,替换现 200 截断);已知键白名单 `{name,description,license,compatibility,metadata,allowed-tools}`,未知键带字段定位拒绝;`compatibility` ≤500;不合规**错误上浮**(List 返回逐技能诊断;load_skill/declared-skill fail-closed 带错误正文,替换静默跳过)。
3. **dogfood**:内置 recipes 全部补 `name:`;不合规目录改名随本 WP 同 PR(不另起 PR)。
4. **backend 复用**:插件技能落盘(host_v2.go:56-74)过同一校验器,失败带字段定位拒绝装载(与 N1 插件 AgentSpec 拒禁键同款形态);种子路径在校验失败时拒写并日志上浮。
### WP5 子代理后台/转录/恢复
1. **Background 消费**:`AgentSpec.Background=true` 时 factory 选择后台派生(squadadapter.SubagentFactory 与 delegate 工具同样语义:声明驱动,不再只靠模型参数)。`WorktreeIsolation/OutputSchema` 保持 declared-only,文档记为边界。
2. **独立转录**:每个子 run 落独立转录文件(`<data>/workspaces/<owner>/<conv>/subagents/<runID>/transcript.jsonl`,由 finishChild 从 messages 表按 run_id 导出 + 后续增量直写);父会话分区保持系统记录不变。SubLLMRunner 迁 exec.Backend 属 Q3,本 WP 不做(边界记明)。
3. **恢复**:`ReconcileInterrupted` 对子 run 不再只 seal——有存储请求与转录的子 run 标记 resumable,父会话下一轮空闲时提示/自动续跑(续跑=以原请求+转录起新子 run,接续语义写清);恢复测试补子代理场景。
4. **通知持久化**:notice 队列落盘(SQLite 表或 JSONL,随 runmgr 既有存储面),重启后重投未投递的后台完成通知;内存 map 只做热缓存。
### WP6 装配收口与 A/B 验收
1. app 装配复核:新端口/开关全部有默认关或行为不变证明;需要设置项的(如 Anthropic 原生端点 key 环境约定)进 settings Schema。
2. A/B 验收证据(写任务卡):① anthropic 录制夹具端到端(WP2-4);② 配对断言测试 + 既有 `TestCompactLeavesNoOrphanToolCall` 原样通过;③ `TestStdioSlowCancelThenEcho` 原样通过。三条全部 GATE(各仓 CI)+ Dev `make test` 收据。
3. 级联 pin 收齐、Dev gitlink PR、任务卡闭链。
## 三、验收总表
| 总提案验收 | 机器证据 | 所在 WP | 现状 |
|---|---|---|---|
| ① 选 anthropic 预设能完成一轮工具调用(录制 HTTP 夹具,不要求真实密钥) | `runtime/llm/testdata/anthropic/*` 夹具 + `TestAnthropicPresetToolRound`(端到端:tools 请求→tool_use→tool_result→终文本→签名往返) | WP2 | 未实现 |
| ② 压缩后的历史不出现孤立 tool_call | 库:`TestCompactLeavesNoOrphanToolCall`(既有,原样保持);产品:`TestTrimOldestKeepsToolPairs`(新,WP1-1) | WP1 | 库已修 / 产品侧仍开放 |
| ③ MCP 取消不再留下第二个读者 | `TestStdioSlowCancelThenEcho`(既有,`Readers()==1`,WP3 不得破坏) | WP3(回归引用) | 已满足 |
## 四、边界与非目标
- **不做** backend 直连路径的延迟工具层与 filestate 接线:该路径属 Q3 退役对象,避免双线建设;N2 的延迟工具成果经 agent-native 路径消费(已通)。
- **不做** `SubLLMRunner` 迁 exec.Backend(Q3 范围);WP5 的独立转录用产品侧导出实现。
- **不做** native.Options 单件重构(审计 N30);新端口不再往里加字段。
- **不做** MCP tasks/* 的完整产品映射(最小集 only:客户端面 + 中断取消);完整后台任务子系统归 N3。
- **不做** token 计费口径拆分(审计 N20 残余)、RunState/OTel/模型回退链/流重试——全部是 N3 范围。
- 激活工具不进 `Decls()` 保持现状(call_tool 间接层即设计)。
- 审计 N2(事件竞争)、N3/N18/N19(MCP)、N22(提醒角色)、N7/N8(压缩配对/并发)已修,仅引用不返工;审计 N4 随 WP2 关闭。
## 五、风险与回退
- **Anthropic 方言漂移**:以录制夹具锁定请求/响应字节形态,预设切换属行为变化——任务卡明示;回落手段:models.json 自定义指回 compat 端点(无需改码)。
- **skills 严格化拒旧内容**:bundled recipes 同 PR 修齐;用户/插件目录里的不合规技能从「静默可用」变「报错可见」——这是本 WP 的有意行为变化,验收时以插件演示技能全绿为门槛。
- **ctxasm 裁切语义变化**:整组同退可能多丢一条 assistant——以配对安全为优先;`Report.Truncations` 文案同步更新。
- **MCP 版本协商**:服务端低版本时必须逐字节回落现行行为(WP3-1 的回落用例为放行门槛)。