docs: 更新方案文档,补充 StreamEvent 迁移和 OpenaiProvider 桥接方案

This commit is contained in:
徐涛
2026-06-30 22:59:05 +08:00
parent 832ebf2665
commit 925c8f9729
2 changed files with 291 additions and 42 deletions
+38 -8
View File
@@ -216,8 +216,10 @@ while let Some(event) = anthropic_stream.next().await {
// Anthropic 的 message_delta 携带 thinking.signature
// → Provider 直接写入 PartialMessageResponse 内部状态
AnthropicEvent::MessageDelta { delta, usage } => {
if let Some(sig) = delta.thinking?.signature {
partial.set_thinking_signature(sig);
if let Some(thinking) = &delta.thinking {
if let Some(sig) = &thinking.signature {
partial.set_thinking_signature(sig.clone());
}
}
yield StreamEvent::CostUpdate { usage: map_usage(usage) };
}
@@ -331,11 +333,21 @@ pub fn create_provider(
**原则**
- 新类型定义放入**新文件**`message.rs``request_v2.rs``response_v2.rs`),不堆积到已有类型文件
- 已有的 `request.rs``OpenaiChatRequest`)、`response.rs``OpenaiChatResponse``stream.rs`(旧 `StreamEvent`**保留原样**,后续 Provider 实现可能作为内部转换目标继续引用
- 已有的 `request.rs``OpenaiChatRequest`)、`response.rs``OpenaiChatResponse`**保留原样**,后续 Provider 实现可能作为内部转换目标继续引用
- `LlmProvider` trait 签名由 `chat(ChatRequest) → ChatResponse` 切换为 `chat(MessageRequest) → MessageResponse`,**在同一个 Phase 内完成**(见下方任务 6‒8)
- trait 签名变更导致的编译错误(`StubProvider``LlmCycle` 调用点)**在 Phase 0 内全部修复**,不留到 Phase 1
- `AgentSession` 等上游中对 `LlmCycle.submit()` 返回值的引用同步适配
**`StreamEvent` 命名冲突处理**
`StreamEvent`(高精度版)定义在 `src/llm/types/response_v2.rs` 中。
`StreamEvent``src/llm/stream.rs` 中定义)的变体(`AssistantTextDelta``ToolExecutionStarted``TurnComplete` 等)与新类型冲突。
处理方式(任务 9 执行):
1. `response_v2.rs` 中的新 `StreamEvent` 是唯一的 `StreamEvent` 定义
2. `src/llm/stream.rs` 中的旧 `StreamEvent` 枚举**替换为**重新导出语句:`pub use super::types::response_v2::StreamEvent;`
3.`StreamEvent` 的变体(`AssistantTextDelta``ToolExecutionStarted``TurnComplete`)**暂时保留**为一个独立的枚举(命名为 `LegacyStreamEvent`)放在 `src/llm/types/old_stream.rs` 新文件中,供 `stream.rs` 中的 `parse_chunk_stream()` 内部使用
4. 这样 `stream.rs` 的辅助函数继续编译,`LlmCycle``AgentSession` 看到的是新 `StreamEvent`
**涉及文件**
| 类型 | 文件 | 操作 |
@@ -343,16 +355,20 @@ pub fn create_provider(
| 新增 | `src/llm/types/message.rs` | 新文件 |
| 新增 | `src/llm/types/request_v2.rs` | 新文件 |
| 新增 | `src/llm/types/response_v2.rs` | 新文件 |
| 新增 | `src/llm/types/old_stream.rs` | 新文件(从 `stream.rs` 迁移旧 `StreamEvent` 变体) |
| 追加 | `src/llm/types/mod.rs` | 追加 `pub mod` 声明 |
| 修改 | `src/llm/provider.rs` | 改 `LlmProvider` trait 签名 |
| 修改 | `src/agent/builder.rs` | 更新 `StubProvider` 实现 |
| 修改 | `src/llm/stream.rs` | 将旧 `StreamEvent` 枚举替换为对 `response_v2::StreamEvent` 的重新导出 |
| 修改 | `src/llm/provider/openai.rs` | 修改:添加临时桥接实现(`MessageRequest → ChatRequest` 转换 + `ChatResponse → MessageResponse` 转换),Phase 1 重写时移除 |
| 修改 | `src/llm/hooks.rs` | 更新 `HookContext.request` 类型为 `&'a MessageRequest` |
| 修改 | `src/llm/cycle.rs` | 更新调用点(`build_request``submit``submit_stream``submit_messages``submit_request` 的类型引用和返回值) |
| 修改 | `src/llm/cycle/retry.rs` | 如有对新 `LlmError` 类型的引用,同步适配 |
| 修改 | `src/agent/error.rs``src/agent/runtime.rs``src/agent/session.rs` 等 | 如有对 `LlmCycle` 返回值或 `ChatResponse` 的引用,同步适配(具体文件由编译错误定位) |
**具体任务**
1. 新增 `src/llm/types/message.rs`,定义 `Message` 扁平大枚举 + `ContentBlock` + `ContentBlockType`
2. 将 9b 中的 `ContentBlock` 变体(`Text`, `Image`, `ToolUse`, `ToolResult`, `Thinking`, `Extension`)及其辅助类型(`ImageSource``AudioSource``FileSource`)定义到 `message.rs`
2. 将 9b 中的 `ContentBlock` 变体(`Text`, `Image`, `Audio`, `File`, `ToolUse`, `ToolResult`, `Thinking`, `Extension`)及其辅助类型(`ImageSource``AudioSource``FileSource`)定义到 `message.rs`
3. 新增 `src/llm/types/request_v2.rs`,定义 `MessageRequest`(从 9b 移植)+ `ExtraError` + extra 访问方法(`get_extra``get_extra_opt``get_extra_as``set_extra`
4. 新增 `src/llm/types/response_v2.rs`,定义 `MessageResponse` + `StreamEvent`(高精度版,`MessageComplete` 只含 `full_response: MessageResponse`+ `PartialUsage` + `PartialMessageResponse` + `apply_to` + `finalize`
5. 新类型侧单元测试:构造、序列化/反序列化(JSON roundtrip)、match 穷举性验证、`PartialMessageResponse.apply_to + finalize` 汇聚一致性测试
@@ -362,10 +378,20 @@ pub fn create_provider(
- `build_request()`:将已有的 `Vec<OpenaiChatMessage>` 转换为 `Vec<Message>`(通过 `chat_message → message` 映射函数),构造 `MessageRequest`
- `submit()` / `submit_messages()`:返回 `Result<MessageResponse, LlmError>`
- `submit_stream()`:返回 `Result<Pin<Box<dyn Stream<Item = StreamEvent> + Send>>, LlmError>`
- 流处理循环:由消费 `OpenaiChatChunk` 改为消费 `StreamEvent`。流结束处的 `full_response` 暂不使用(Phase 2 才启用简化逻辑),先提取 `stop_reason``message` 构建传统返回
9. 编译驱动适配:对上游(`agent/session.rs``agent/runtime.rs``agent/error.rs` 等)中引用旧类型的地方,逐一按编译错误修复
- 流处理循环:由消费 `OpenaiChatChunk` 改为消费 `StreamEvent`。流结束处的 `full_response` 暂不使用(Phase 2 才启用简化逻辑),先提取 `stop_reason``message` 构建传统返回
9. 新增文件 `src/llm/types/old_stream.rs`(从 `src/llm/stream.rs` 迁移旧 `StreamEvent` 定义),同时将 `src/llm/stream.rs` 中的旧 `StreamEvent` 枚举替换为对 `response_v2.rs` 中新 `StreamEvent` 的重新导出(`pub use super::types::response_v2::StreamEvent;`),确保 `stream.rs``parse_chunk_stream()``ChunkToEventStream` 继续编译通过
10. 修改 `src/llm/provider/openai.rs`:添加 `LlmProvider` trait 临时桥接实现——
- `chat()``MessageRequest → ChatRequest`(利用现有 `OpenaiChatMessage` 转换)→ 调用已有 `chat_inner()``ChatResponse → MessageResponse`(使用 `finalize()` 算法或直接映射)
- `chat_stream()``MessageRequest → ChatRequest` → 调用已有 `chat_stream_inner()` → 将 `OpenaiChatChunk` 流映射为 `StreamEvent` 流(利用已有的 `parse_chunk_stream`
- 桥接实现标记 `// ponytail: Phase 0 临时桥接,Phase 1 重写时移除`
11. 测试适配(编译驱动,涉及文件不限于以下列表,由编译器报错定位):
- `src/llm/cycle.rs` 测试模块(`MockProvider``assistant_text_response()``assistant_tool_call_response()`、各测试用例中的断言类型)
- `src/agent/session.rs` 测试模块(`MockProvider`、响应构造 helper 等)
- `src/agent/builder.rs` 测试模块(`StubProvider` 已单独由任务 7 处理)
- `src/agent/session_memory.rs``src/agent/runtime.rs`
12. 编译驱动适配:对上游(`agent/session.rs``agent/runtime.rs``agent/error.rs` 等)中引用旧类型的地方,逐一按编译错误修复
**验证**`cargo test` 全部通过。`git diff` 确认新增和修改文件范围符合预期。
**验证**`cargo test` 全部通过。`git diff` 确认新增和修改文件范围符合预期。确认 `OpenaiProvider` 的临时桥接代码带有 `// ponytail: Phase 0 临时桥接` 注释,Phase 1 移除时易于定位。
### Phase 1Provider 适配
@@ -469,7 +495,11 @@ pub fn create_provider(
**风险储备**
- 如果 `OpenaiProvider` 的重写复杂度过高,可以保留旧的 `OpenaiProvider` 不变,在旁边新增一个 `OpenaiProviderV2` 并行开发
- `ChatRequest` / `ChatResponse` 等类型别名不主动移除——旧别名在 Phase 2 切换后通过 `#[deprecated]` 标记引导迁移,确认无外部使用者后再删除
- `ChatRequest` / `ChatResponse` / `Message` / `ContentBlock` / `ToolDefinition` / `StopReason` 等类型别名和旧类型结构体的弃用路径:
- **Phase 0 完成时**:旧别名**保留**(作为编译桥接),新类型通过不同路径(`request_v2::MessageRequest``response_v2::MessageResponse`)访问,两者同时存在于类型模块中
- **Phase 1 完成时**Provider 实现切换到新类型,旧 `OpenaiProvider` 的临时桥接代码被 Phase 1 的真实实现替换。旧别名仍由 `src/llm/types/mod.rs` 导出,不影响其他模块
- **Phase 2 完成时**`LlmCycle` 内部消息存储从 `Vec<OpenaiChatMessage>` 切换到 `Vec<Message>`,所有 `ChatRequest`/`ChatResponse` 引用被替换。此时对 `ChatRequest``ChatResponse``Message``ContentBlock``ToolDefinition``StopReason` 等旧别名和 `ChatResponse` 结构体加 `#[deprecated]` 标记
- **下一个版本(v0.2.0 或 v1.0.0**:运行 `cargo check` 确认无外部引用后,删除所有 deprecated 别名和 `ChatResponse` 结构体
---