Add fast-slow O2 route contract EOF design and plan for review
Why: user chose O2 in the completion signal evaluation; stage 4/5 docs precede any code. Evidence: read-only code location in Agent-Native runtime/llm and Backend runtime (SELF). Impact: docs only; no product code, no paid requests. Refs: docs/superpowers/specs/2026-10-19-fast-slow-completion-signal-evaluation.md Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
# 快慢通道 O2:限路由等价终态 阶段 5 实施与测试计划
|
||||||
|
|
||||||
|
- 状态:**阶段 5 计划稿,待人审**(与[阶段 4 方案](../specs/2026-10-19-fast-slow-o2-route-contract-eof-design.md)同批评审)。人审通过前不派 worker、不改产品代码。
|
||||||
|
- 日期:2026-10-19;作者:host;来源等级:SELF。
|
||||||
|
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md);worker 不提交、不推送、不开 PR;host 负责范围审阅、权威验证、PR/CI/合入。
|
||||||
|
- 付费与真实模型:**本计划零付费部分不需要任何付费请求**;P1 付费验证另提资源方案并单独批准。
|
||||||
|
|
||||||
|
## 1. 交付切分与顺序
|
||||||
|
|
||||||
|
| 增量 | 仓 | 内容 | 依赖 | 合入方式 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| I1 | Agent-Native | `runtime/argschema` 提炼 + 解析器延后判定 + `RouteContractEOF` 传输层判定 + 夹具 | 无 | 组件仓 PR,CI 通过后合入(组件仓无需 #518 窗口) |
|
||||||
|
| I2 | Backend | 系统层设置键、`openLLMClient` 按身份挂合同、运行记录透传 `CompletionBasis` | I1 合入且 Backend `deps/agent-native` 改钉 | 组件仓 PR |
|
||||||
|
| I3 | Dev | 改钉 Agent-Native 与 Backend,组合验证 `make test` | I1、I2 | #518 窗口合入 |
|
||||||
|
| P1 | — | 1 次短工具探针 + 1 次 A01(付费) | I3 合入、资源方案获批、系统层配置写入 | 另行授权 |
|
||||||
|
|
||||||
|
各增量阶段 6 评审包写入 `docs/superpowers/specs/`;I1–I3 每个增量完成后先交用户阶段 6 再合入(与快慢通道既往 manual 口径一致)。
|
||||||
|
|
||||||
|
## 2. I1 Agent-Native(单元 `o2-i1-agent-native-route-contract`)
|
||||||
|
|
||||||
|
- **允许路径**:`runtime/llm/**`、`runtime/argschema/**`(新)、`runtime/tools/registry.go`、`runtime/tools/registry_test.go`。
|
||||||
|
- **动作**:
|
||||||
|
1. 新包 `runtime/argschema`:`Validate(schema, args json.RawMessage) error`,语义与现 `validateArgs` 逐项相同;`registry.validateArgs` 改为委托,现有 registry 测试不改断言。
|
||||||
|
2. 解析器:内部选项 `deferMissingDone`;新增计数 `finishChunks`、标志 `toolAfterFinish`。只有在“除缺 `[DONE]` 外全部现有检查通过”时才返回候选终态,否则行为同今。公开 `ParseSSE` 签名与零值行为不变。
|
||||||
|
3. 传输层:`Client.RouteContract`;在 `bodyObserver` 快照后按方案 §4.2 的 C1–C9 判定;放行发 `message_end`(`CompletionBasis=route_contract_eof`,`StreamComplete=false`),拒绝发与今天相同文案的终态失败并附 `ContractCheck`。
|
||||||
|
4. `Event`/`TerminalObservation` 新增 `CompletionBasis`、`ContractCheck`(JSON 字段名 `completionBasis`、`contractCheck`)。
|
||||||
|
- **测试**(`httptest`;HTTP/2 用 `httptest.NewUnstartedServer` + `EnableHTTP2` + TLS):
|
||||||
|
|
||||||
|
| 用例 | 期望 |
|
||||||
|
|---|---|
|
||||||
|
| 有 `[DONE]`(合同开/关) | 放行,`CompletionBasis=done_marker`,与现状一致 |
|
||||||
|
| 无 `[DONE]`、合同关 | 拒绝,文案不变 |
|
||||||
|
| 无 `[DONE]`、合同开、HTTP/2 正常结束、全部条件成立 | **放行**,`route_contract_eof`,工具调用逐字节等于输入 |
|
||||||
|
| 同上但 `known_length`+`match` / `mismatch` | 依人审 §6.1 结论放行 / 拒绝(C2) |
|
||||||
|
| `close_delimited`、`chunked`、`unknown` | 拒绝(C2) |
|
||||||
|
| 缺 Usage | 拒绝(C5) |
|
||||||
|
| Usage 在 `finish_reason` 块之前 / 之后 | 均放行(顺序不作要求,评估稿 §3) |
|
||||||
|
| 两个非空 `finish_reason` 块;`finish_reason=stop` 却有工具 | 拒绝(C4) |
|
||||||
|
| `finish_reason` 之后追加工具片段 | 拒绝(C6) |
|
||||||
|
| 半帧 / 无效帧 / 供应商错误帧 | 拒绝(C3/C9) |
|
||||||
|
| ID 或名称缺失、参数非 JSON 对象 | 拒绝(C7) |
|
||||||
|
| 工具名不在请求 `tools`;缺 required;属性类型不符 | 拒绝(C8) |
|
||||||
|
| 模型名不符、基址不符、摘要不符、`APIBase` 覆盖到他址、重定向到他主机 | 拒绝(C1) |
|
||||||
|
| 读取中取消 | 中止事件,无放行 |
|
||||||
|
|
||||||
|
每个用例同时断言 Usage 保留与“放行次数/工具调用数”。
|
||||||
|
- **验证**:`gofmt -l runtime`;`go vet ./runtime/...`;`go test ./runtime/llm/... ./runtime/argschema/... ./runtime/tools/... -count=1`;`git diff --check`。
|
||||||
|
- **停止条件**:若延后判定需要改变公开 `ParseSSE` 的零值行为或既有测试断言,停止并回到方案评审。
|
||||||
|
|
||||||
|
## 3. I2 Backend(单元 `o2-i2-backend-route-contract-config`)
|
||||||
|
|
||||||
|
- **允许路径**:`internal/runtime/**`、`internal/settings/**`、`internal/runmgr/**`(仅透传字段)、Backend `deps/agent-native` gitlink(host 改钉,不由 worker 改)。
|
||||||
|
- **动作**:系统层设置键 `models.chat.routeContractEOF`(缺省关闭;用户/项目层与画像覆盖一律忽略);`openLLMClient` 增参 `ResolvedModel` 与配置,三元组与摘要全部精确一致才挂合同;轻量档、视觉改模、子代理独立判断;运行记录与快慢通道收据透传 `completionBasis`。
|
||||||
|
- **测试**:缺省关闭;用户层写入被忽略;模型/基址/摘要任一不符不挂合同;视觉改模后不继承;挂合同后假流放行一次工具并记录 `route_contract_eof`;删除配置后恢复拒绝。
|
||||||
|
- **验证**:`gofmt`;`go vet ./internal/runtime ./internal/settings ./internal/runmgr`;`go test ./internal/runtime ./internal/settings ./internal/runmgr -count=1`。
|
||||||
|
|
||||||
|
## 4. I3 Dev 组合
|
||||||
|
|
||||||
|
- host 改钉 `deps/agent-native`(I1 合入 SHA)与 `modules/backend`(I2 合入 SHA);`make test` + `make build` 由 PR CI(GATE)确认;无产品代码改动。
|
||||||
|
- 注意与同期 IAM 链路 initiative(Dev #628)对 Backend gitlink 的合入顺序:以合入时最新 main 为准重新组合验证,冲突由 host 选择包含双方的 Backend SHA,不回退对方提交。
|
||||||
|
|
||||||
|
## 5. P1 付费验证(不在本计划授权内)
|
||||||
|
|
||||||
|
- 前提:I3 合入;系统层配置写入目标路由三元组;核对 r0.6 证据身份(受保护路由文件 SHA-256 `2a92569d…`)与生产三元组指向同一模型/基址。
|
||||||
|
- 内容:1 次短工具探针(预期约 500 tokens,参考 r0.5/r0.6),期望 `route_contract_eof` 放行且工具执行恰好一次;通过后 1 次 A01,规模参考 r0.4。
|
||||||
|
- 另提资源方案(预算上限、停止条件、收据路径),用户批准后执行;失败即停、不补跑。
|
||||||
|
|
||||||
|
## 6. 完成与停止
|
||||||
|
|
||||||
|
- 零付费完成:I1–I3 合入,夹具全绿,配置缺省关闭时线上行为不变(以既有 `[DONE]` 与拒绝用例为回归证据)。
|
||||||
|
- 停止并回到方案评审:需要改变 `StreamComplete` 语义、泛化到其他路由、或任何条件无法在本地确定性观察。
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# 快慢通道 O2:限路由等价终态(`route_contract_eof`)方案
|
||||||
|
|
||||||
|
- 状态:**阶段 4 方案稿,待人审。** 本文不授权产品代码改动、付费请求、A01 复验或部署;阶段 4/5 人审通过后才按配套计划派 worker 实施零付费部分,付费探针另行提资源方案。
|
||||||
|
- 日期:2026-10-19;作者:host(只读调查与文档撰写);来源等级:SELF。
|
||||||
|
- 决策来源:用户 2026-10-19 在[完成信号口径评估](2026-10-19-fast-slow-completion-signal-evaluation.md)中选定 **O2**(另起任务)。
|
||||||
|
- 配套计划:[阶段 5 实施与测试计划](../plans/2026-10-19-fast-slow-o2-route-contract-eof-plan.md)。
|
||||||
|
- 上游约束:[r0.6 传输完整性设计](../../../modules/documents/docs/proposals/2026-10-14-fast-slow-u3-r06-transport-completeness-design.md)的四个前提(评估稿第 4 节逐项对照);本方案是该设计要求的“另一轮方案评审”,不改变 r0.6 已批准的观察字段。
|
||||||
|
|
||||||
|
## 1. 目标与非目标
|
||||||
|
|
||||||
|
**目标**:对**一条经核验的 MiniMax 路由**,在流缺少字面 `[DONE]`、但能证明“按协议结束且工具调用完整”时,放行该轮工具调用;其余一切情形保持现行拒绝。
|
||||||
|
|
||||||
|
**非目标**:
|
||||||
|
|
||||||
|
- 不改变有 `[DONE]` 的现有放行路径与 `StreamComplete` 语义。
|
||||||
|
- 不对其他 OpenAI 兼容服务、其他 MiniMax 模型或其他主机泛化;不提供“全局放宽”开关。
|
||||||
|
- 不在本轮跑真实 A01 或任何付费请求;不改 Anthropic 协议路径。
|
||||||
|
- 不引入完整 JSON Schema 引擎(见 §4.3 的子集约定)。
|
||||||
|
|
||||||
|
## 2. 现状(代码定位,Agent-Native `origin/main` 9c24e22 / Dev 钉 28f3ea7 同文件)
|
||||||
|
|
||||||
|
| 位置 | 现行行为 |
|
||||||
|
|---|---|
|
||||||
|
| `runtime/llm/sse.go` `emitParsed` 第 260 行起 | 有工具调用且 `!sawDone` → `sendTerminalFailure("tool call stream ended before [DONE]")`,工具全部丢弃,Usage 保留 |
|
||||||
|
| `runtime/llm/sse.go` 第 300 行 | 正常结束发 `message_end`,`StreamComplete = sawDone` |
|
||||||
|
| `runtime/llm/client.go` `run` 第 129–139 行 | 用 `bodyObserver` 包住响应体;`ParseSSE` 的终态事件发出后才补 `TerminalObservation.BodyBoundary` |
|
||||||
|
| `runtime/llm/body_observation.go` | 只按 `net/http` 可见信息分类:`http2_stream` / `known_length` / `chunked` / `close_delimited` / `unknown`;`LengthRelation` 仅在已知长度、未解压、未饱和且见 EOF 时为 `match`/`mismatch` |
|
||||||
|
| `runtime/tools/registry.go` `validateArgs` | 工具**执行前**按 schema 子集(`type`、`required`、属性类型)校验参数;失败返回错误结果、不执行 |
|
||||||
|
| Backend `internal/runtime/model.go` `ResolvedModel` | 路由身份 = `Kind` + `Model` + `Base`(设置层 → 画像覆盖);生产无独立“路由文件” |
|
||||||
|
| Backend `internal/runtime/llm.go` `openLLMClient` | 构造 `agentllm.Client`;`MiniMax-M*` 置 `ReasoningSplit` |
|
||||||
|
|
||||||
|
关键约束:**判定所需的传输边界只在 `client.go` 可见**,`ParseSSE` 看不到;因此终态判定不能只放在解析器里。
|
||||||
|
|
||||||
|
## 3. 方案总览
|
||||||
|
|
||||||
|
```
|
||||||
|
ParseSSE(解析器) Client.run(传输层)
|
||||||
|
────────────────── ────────────────────
|
||||||
|
既有检查全部照旧 bodyObserver 快照
|
||||||
|
仅当“唯一缺陷 = 缺 [DONE]”且 ↓
|
||||||
|
调用方声明了路由合同时 → RouteContractEOF.Evaluate(candidate, boundary, tools)
|
||||||
|
返回 候选终态(不外发) ────────────▶ 全部条件成立 → message_end(CompletionBasis=route_contract_eof)
|
||||||
|
任一不成立 → 与今天相同的终态失败(Usage 保留)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 合同未配置(零值)时,解析器与客户端的行为**逐字节等同现状**。
|
||||||
|
- 解析器只做“延后判定”,不做放行;放行只在传输层、且只对绑定身份。
|
||||||
|
|
||||||
|
## 4. 设计细节
|
||||||
|
|
||||||
|
### 4.1 路由合同与身份绑定(Agent-Native `runtime/llm`)
|
||||||
|
|
||||||
|
新增导出类型(命名可在计划阶段微调,语义固定):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// RouteContractEOF 只对一条经核验路由生效;零值/nil 表示关闭。
|
||||||
|
type RouteContractEOF struct {
|
||||||
|
Model string // 精确模型名,例如 MiniMax-M3
|
||||||
|
BaseURL string // 规范化后的精确基址,例如 https://api.minimaxi.com/v1
|
||||||
|
IdentitySHA256 string // sha256("openai-chat\n" + Model + "\n" + BaseURL)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Client` 增加可选字段 `RouteContract *RouteContractEOF`。生效前提(任一不满足即视为关闭,回到现行拒绝):
|
||||||
|
|
||||||
|
1. `Protocol` 为 openai-chat;
|
||||||
|
2. 本次请求实际使用的 `model` 与 `Model` 精确相等;
|
||||||
|
3. 本次请求实际使用的基址(含 `StreamOptions.APIBase` 覆盖)规范化后与 `BaseURL` 精确相等;
|
||||||
|
4. 由 1–3 重算的摘要等于 `IdentitySHA256`(防止宿主误配一半字段);
|
||||||
|
5. 请求没有被重定向到其他主机(最终响应 URL 主机与基址主机一致)。
|
||||||
|
|
||||||
|
r0.6 探针所记的受保护路由文件 SHA-256(`2a92569d…`)是**证据身份**,用于核对“被观察的就是这条路由”;生产绑定用上面的三元组摘要,二者在计划的付费探针前做一次映射核对。
|
||||||
|
|
||||||
|
### 4.2 放行条件(全部成立才放行)
|
||||||
|
|
||||||
|
| # | 条件 | 数据来源 |
|
||||||
|
|---|---|---|
|
||||||
|
| C1 | 路由合同生效(§4.1 的 1–5) | `Client` + 请求 + 响应 |
|
||||||
|
| C2 | 传输按协议结束:`FramingClass=http2_stream` 且 `EOFSeen`,或 `known_length` 且 `LengthRelation=match` | `BodyBoundaryObservation` |
|
||||||
|
| C3 | `ReadOutcome=clean_eof`;无取消、读错、无效帧、供应商错误 | `TerminalObservation` |
|
||||||
|
| C4 | 恰好一个带非空 `finish_reason` 的块,且其值为 `tool_calls` | 解析器新增计数 |
|
||||||
|
| C5 | 请求开启 `include_usage`(客户端恒开)且收到 Usage | `UsagePresent` |
|
||||||
|
| C6 | `finish_reason` 块之后**没有**任何工具片段 | 解析器新增标志 |
|
||||||
|
| C7 | 每个工具调用 ID、名称齐全,参数为 JSON 对象 | 现有检查 |
|
||||||
|
| C8 | 每个调用的名称在本次请求声明的 `tools` 中,参数通过该工具 schema 子集校验(§4.3) | 请求 `tools` |
|
||||||
|
| C9 | 最后一帧 JSON 完整,`TailEventKind=json_frame` | `TerminalObservation` |
|
||||||
|
|
||||||
|
`chunked`、`close_delimited`、`unknown`、`known_length` 但 `mismatch`/`unknown` 一律不满足 C2。
|
||||||
|
|
||||||
|
### 4.3 参数 schema 子集
|
||||||
|
|
||||||
|
C8 采用与 `tools.registry.validateArgs` **相同**的子集:顶层 `type: object`、`required` 必填项、已声明属性的基本类型(string/number/integer/boolean/array/object)。实现上把该校验提炼到新的无依赖小包 `runtime/argschema` 中的单一函数,`runtime/llm` 与 `runtime/tools` 都调用它(两包当前互不导入,放进任一方都会引入新的包依赖方向),避免两套语义漂移。超出子集的约束(枚举、格式、嵌套)不在本轮,registry 执行前校验仍保留作为第二道门。
|
||||||
|
|
||||||
|
### 4.4 输出与可观测性
|
||||||
|
|
||||||
|
- 放行时:`message_end` 正常携带工具调用;`StreamComplete` **保持 false**(字面含义仍是“未见 `[DONE]`”);新增 `Event.CompletionBasis`:`done_marker`(现有路径)或 `route_contract_eof`。
|
||||||
|
- `TerminalObservation` 新增 `CompletionBasis` 与 `ContractCheck`(失败时记录第一个不满足的条件编号,例如 `C2`),不保存正文、参数或密钥。
|
||||||
|
- 拒绝时:错误文案保持 `tool call stream ended before [DONE]`,附 `ContractCheck`;Usage 照旧保留以便计费与停止。
|
||||||
|
- 下游(Backend 运行记录、快慢通道收据)只把 `CompletionBasis` 作为新增字段透传,不把 `route_contract_eof` 计作 `StreamComplete=true`。
|
||||||
|
|
||||||
|
### 4.5 宿主配置(Backend)
|
||||||
|
|
||||||
|
- 新设置键 `models.chat.routeContractEOF`(JSON:`model`、`baseUrl`、`identitySha256`),**默认缺省 = 关闭**;只允许系统层写入,不接受用户/项目层与画像覆盖。
|
||||||
|
- `openLLMClient` 在 `ResolvedModel` 与该配置三元组全部精确一致时才挂合同;轻量档、视觉改模、子代理按各自解析结果独立判断,不继承。
|
||||||
|
- 回滚:删除该设置键即恢复现行拒绝,无需发版;身份漂移(换模型、换基址)自动失效。
|
||||||
|
|
||||||
|
## 5. 风险与取舍
|
||||||
|
|
||||||
|
| 风险 | 处置 |
|
||||||
|
|---|---|
|
||||||
|
| 截断被误判为完成 | C4 要求 `finish_reason` 已到达;C2 要求传输按协议结束;C6 拒绝 `finish_reason` 之后再出现片段 |
|
||||||
|
| 供应商行为漂移、无版本承诺 | 合同只绑定精确身份;默认关闭;`ContractCheck` 使漂移在收据中可见;必要时直接删配置回滚 |
|
||||||
|
| 参数语义错误进入执行 | C8 在放行前校验;registry 执行前再校验一次 |
|
||||||
|
| `known_length` 是否纳入 | 方案纳入(仅 `match`);r0.6 观察到的是 `http2_stream`。**请人审确认**是否只保留 `http2_stream` |
|
||||||
|
| 两套 schema 校验漂移 | §4.3 提炼为单一函数 |
|
||||||
|
|
||||||
|
## 6. 需人审确认的取舍
|
||||||
|
|
||||||
|
1. C2 是否同时接受 `known_length`+`match`(建议接受),还是只接受 `http2_stream`。
|
||||||
|
2. 宿主配置键仅系统层可写、默认关闭(建议如此)。
|
||||||
|
3. 付费验证(1 次短工具探针 + 1 次 A01)在零付费部分合入后另提资源方案,不随本方案授权。
|
||||||
Reference in New Issue
Block a user