docs: 更新方案文档,补充 StreamEvent 迁移和 OpenaiProvider 桥接方案
This commit is contained in:
@@ -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 1:Provider 适配
|
||||
|
||||
@@ -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` 结构体
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -15,12 +15,18 @@
|
||||
## 原则
|
||||
|
||||
1. **新类型定义放入新文件**(`message.rs`、`request_v2.rs`、`response_v2.rs`),不堆积到已有类型文件
|
||||
2. 已有的 `request.rs`(`OpenaiChatRequest`)、`response.rs`(`OpenaiChatResponse`)、`stream.rs`(旧 `StreamEvent`)**保留原样**,后续 Provider 实现可能作为内部转换目标继续引用
|
||||
2. 已有的 `request.rs`(`OpenaiChatRequest`)、`response.rs`(`OpenaiChatResponse`)**保留原样**,后续 Provider 实现可能作为内部转换目标继续引用
|
||||
3. `LlmProvider` trait 签名由 `chat(ChatRequest) → ChatResponse` 切换为 `chat(MessageRequest) → MessageResponse`,**在同一个 Phase 内完成**
|
||||
4. trait 签名变更导致的编译错误(`StubProvider`、`LlmCycle` 调用点)**在 Phase 0 内全部修复**,不留到 Phase 1
|
||||
5. `AgentSession` 等上游中对 `LlmCycle.submit()` 返回值的引用同步适配
|
||||
6. 不使用任何新依赖,只在现有 crate 范围内完成
|
||||
|
||||
**`StreamEvent` 命名冲突处理**(参考 10 号文档 §4 Phase 0):
|
||||
- 新 `StreamEvent`(高精度版)定义在 `src/llm/types/response_v2.rs` 中,是**唯一的 `StreamEvent` 定义**
|
||||
- `src/llm/stream.rs` 中的旧 `StreamEvent` 枚举**替换为**重新导出:`pub use super::types::response_v2::StreamEvent;`
|
||||
- 旧 `StreamEvent` 的变体(`AssistantTextDelta`、`ToolExecutionStarted`、`TurnComplete` 等)迁移到一个独立的 `LegacyStreamEvent` 枚举中,放在 `src/llm/types/old_stream.rs` 新文件
|
||||
- `stream.rs` 中的 `parse_chunk_stream()` 和 `ChunkToEventStream` 内部使用 `LegacyStreamEvent`,对外暴露的是新 `StreamEvent`
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件
|
||||
@@ -30,9 +36,13 @@
|
||||
| 新增 | `src/llm/types/message.rs` | Message 扁平大枚举 + ContentBlock + 辅助类型 |
|
||||
| 新增 | `src/llm/types/request_v2.rs` | MessageRequest + ExtraError + extra 访问方法 |
|
||||
| 新增 | `src/llm/types/response_v2.rs` | MessageResponse + StreamEvent(新) + PartialMessageResponse |
|
||||
| 新增 | `src/llm/types/old_stream.rs` | 旧 StreamEvent 变体迁移为 LegacyStreamEvent |
|
||||
| 追加 | `src/llm/types/mod.rs` | 追加 `pub mod` 声明和 `pub use` 重导出 |
|
||||
| 修改 | `src/llm/provider.rs` | LlmProvider trait 签名切换 |
|
||||
| 修改 | `src/agent/builder.rs` | StubProvider 适配新 trait 签名 |
|
||||
| 修改 | `src/llm/stream.rs` | 旧 StreamEvent 枚举替换为对 response_v2::StreamEvent 的重新导出 |
|
||||
| 修改 | `src/llm/provider/openai.rs` | 添加临时桥接实现(MessageRequest → ChatRequest → 旧逻辑 → MessageResponse) |
|
||||
| 修改 | `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/session.rs` | ChatResponse → MessageResponse 引用适配 |
|
||||
@@ -53,21 +63,30 @@
|
||||
## 任务依赖关系
|
||||
|
||||
```
|
||||
任务 1-4(新类型定义) ← 可并行
|
||||
任务 1-4(新类型定义) ← 可并行
|
||||
│
|
||||
├──→ 任务 5(新类型单元测试)← 可并行,不阻塞下游
|
||||
│
|
||||
├──→ 任务 9(old_stream.rs 迁移)← 仅需 response_v2.rs 就绪
|
||||
│
|
||||
└──→ 任务 6(trait 签名切换)
|
||||
│
|
||||
└──→ 任务 7(StubProvider 适配)
|
||||
│
|
||||
└──→ 任务 8(LlmCycle 适配)
|
||||
│
|
||||
└──→ 任务 9(上游适配)
|
||||
├──→ 任务 7(StubProvider 适配)
|
||||
│ │
|
||||
│ └──→ 任务 8(LlmCycle 适配)
|
||||
│ │
|
||||
│ ├──→ 任务 11(测试适配)← 也依赖任务 10
|
||||
│ └──→ 任务 12(编译驱动适配上游)
|
||||
│
|
||||
└──→ 任务 10(OpenaiProvider 桥接)← 可并行 7/8
|
||||
```
|
||||
|
||||
**关键路径**:任务 1 → 6 → 7 → 8 → 9
|
||||
**可并行**:任务 5 可与任务 6-9 并行编写,但在执行任务 6 前需确认类型定义已就绪
|
||||
**关键路径**:任务 1 → 6 → 7 → 8 → 11/12
|
||||
**可并行**:
|
||||
- 任务 5 与任务 6-12 可并行编写
|
||||
- 任务 9 与任务 6-8 无依赖关系
|
||||
- 任务 10 与任务 7-8 无依赖关系
|
||||
- 任务 11 和 12 可并行适配
|
||||
|
||||
---
|
||||
|
||||
@@ -645,19 +664,19 @@ impl LlmProvider for StubProvider {
|
||||
// 新类型导入
|
||||
use crate::llm::types::message::Message;
|
||||
use crate::llm::types::request_v2::MessageRequest;
|
||||
use crate::llm::types::response_v2::{MessageResponse, StreamEvent as NewStreamEvent};
|
||||
use crate::llm::types::response_v2::{MessageResponse, StreamEvent};
|
||||
use crate::llm::provider::ProviderCapabilities;
|
||||
|
||||
// 旧类型导入(仍用于内部消息存储和 submit_stream 处理)
|
||||
use crate::llm::types::{OpenaiChatMessage, OpenaiChatChunk};
|
||||
use crate::llm::stream::StreamEvent as OldStreamEvent;
|
||||
use crate::llm::types::old_stream::LegacyStreamEvent;
|
||||
|
||||
// ⚠️ StreamEvent 命名消歧:
|
||||
// - 旧 StreamEvent(src/llm/stream.rs)对应 OpenaiChatChunk 解析事件
|
||||
// - 新 StreamEvent(src/llm/types/response_v2.rs)对应高精度 IR 流式事件
|
||||
// - 本文件中,通过 as 别名区分:NewStreamEvent / OldStreamEvent
|
||||
// - 实际使用 `submit_stream` 调用 provider.chat_stream 返回的是 NewStreamEvent
|
||||
// - Phase 2 移除旧 StreamEvent 后,可去掉别名直接使用 StreamEvent
|
||||
// ⚠️ StreamEvent 命名说明:
|
||||
// - 旧 StreamEvent 已迁移至 src/llm/types/old_stream.rs(LegacyStreamEvent)
|
||||
// - src/llm/stream.rs 已改为重新导出 response_v2::StreamEvent
|
||||
// - 本文件中,StreamEvent 指的就是新高精度 IR 事件类型
|
||||
// - submit_stream 返回的流包含的是 StreamEvent
|
||||
// - parse_chunk_stream 内部使用 LegacyStreamEvent,对外也映射为新 StreamEvent
|
||||
```
|
||||
|
||||
### 8b:修改 `build_request(&self, tools) → MessageRequest`
|
||||
@@ -762,14 +781,13 @@ pub async fn submit_stream(&mut self, prompt: String, tools: Vec<ToolDefinition>
|
||||
-> Result<Pin<Box<dyn Stream<Item = StreamEvent> + Send>>, LlmError>;
|
||||
|
||||
// 目标签名(返回新 StreamEvent 流)
|
||||
// 注意:此处使用 NewStreamEvent 与 8a 中 import 别名保持一致
|
||||
pub async fn submit_stream(&mut self, prompt: String, tools: Vec<ToolDefinition>)
|
||||
-> Result<Pin<Box<dyn Stream<Item = NewStreamEvent> + Send>>, LlmError>;
|
||||
-> Result<Pin<Box<dyn Stream<Item = StreamEvent> + Send>>, LlmError>;
|
||||
```
|
||||
|
||||
**内部逻辑变更**:
|
||||
- 调用 `self.provider.chat_stream(request)` 直接获得 `Result<NewStreamEvent>` 流
|
||||
- 流处理:从消费 `OpenaiChatChunk` 改为消费 `NewStreamEvent`
|
||||
- 调用 `self.provider.chat_stream(request)` 直接获得 `Result<StreamEvent>` 流
|
||||
- 流处理:从消费 `OpenaiChatChunk` 改为消费 `StreamEvent`
|
||||
- 流结束处提取 `full_response`(Phase 2 才真正使用,当前先解构 MessageComplete 拿到 MessageResponse 用于消息历史追加和 usage 统计)
|
||||
|
||||
### 8f:修改 `submit_request()` 返回类型
|
||||
@@ -914,9 +932,204 @@ fn message_to_chat_message(msg: &Message) -> OpenaiChatMessage {
|
||||
|
||||
---
|
||||
|
||||
## 任务 9:编译驱动适配上游
|
||||
## 任务 9:新增 `src/llm/types/old_stream.rs` —— 旧 StreamEvent 变体迁移
|
||||
|
||||
### 9a:`src/agent/session.rs`
|
||||
**对应**:10 号文档 §4 Phase 0 任务 9
|
||||
|
||||
### 背景
|
||||
|
||||
`src/llm/stream.rs` 中当前的 `StreamEvent` 枚举(包含 `AssistantTextDelta`、`ToolExecutionStarted`、`TurnComplete`、`CostUpdate`、`Error` 等变体)与新 `StreamEvent`(`response_v2.rs` 中定义的高精度 IR 事件)同名。
|
||||
|
||||
### 操作步骤
|
||||
|
||||
1. 新建 `src/llm/types/old_stream.rs`
|
||||
2. 从 `src/llm/stream.rs` 中复制旧 `StreamEvent` 枚举定义,重命名为 `LegacyStreamEvent`:
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LegacyStreamEvent {
|
||||
AssistantTextDelta { text: String },
|
||||
ToolExecutionStarted { tool_name: String, input: serde_json::Value, tool_call_id: String },
|
||||
TurnComplete { reason: FinishReason },
|
||||
CostUpdate { usage: Usage },
|
||||
Error { message: String },
|
||||
}
|
||||
```
|
||||
3. 将 `src/llm/stream.rs` 中的旧 `StreamEvent` 枚举定义**替换为**重新导出:
|
||||
```rust
|
||||
// 替换旧 StreamEvent 定义
|
||||
pub use crate::llm::types::response_v2::StreamEvent;
|
||||
```
|
||||
4. 更新 `src/llm/stream.rs` 中 `parse_chunk_stream()` 和 `ChunkToEventStream` 的引用——它们内部使用 `LegacyStreamEvent`(从 `super::types::old_stream` 引入),外部返回值改为新 `StreamEvent`
|
||||
5. 在 `src/llm/types/mod.rs` 中追加 `pub mod old_stream;`
|
||||
6. 更新 `src/llm/cycle.rs` 中 `use crate::llm::stream::StreamEvent` 的导入(如果是直接引用 `stream::StreamEvent`,由于 `stream.rs` 现在重导出了新 `StreamEvent`,导入路径不变,拿到的是新类型)
|
||||
|
||||
### 验证点
|
||||
|
||||
- `cargo build` 通过,无 `StreamEvent` 重定义错误
|
||||
- `stream.rs` 中 `parse_chunk_stream()` 返回类型正确(新 `StreamEvent`)
|
||||
|
||||
---
|
||||
|
||||
## 任务 10:修改 `src/llm/provider/openai.rs` —— 临时桥接实现
|
||||
|
||||
**对应**:10 号文档 §4 Phase 0 任务 10
|
||||
|
||||
### 背景
|
||||
|
||||
`OpenaiProvider` 实现了 `LlmProvider` trait,Phase 0 切换 trait 签名后其 `chat()` / `chat_stream()` 签名不匹配,需要添加临时桥接。
|
||||
|
||||
### 目标实现
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
impl LlmProvider for OpenaiProvider {
|
||||
// ponytail: Phase 0 临时桥接,Phase 1 重写时移除
|
||||
async fn chat(&self, request: MessageRequest) -> Result<MessageResponse, LlmError> {
|
||||
// MessageRequest → ChatRequest
|
||||
let chat_req = self.convert_to_chat_request(request);
|
||||
// 调用已有逻辑(需要提取为内部方法)
|
||||
let chat_resp = self.chat_inner(chat_req).await?;
|
||||
// ChatResponse → MessageResponse
|
||||
Ok(self.convert_to_message_response(chat_resp))
|
||||
}
|
||||
|
||||
// ponytail: Phase 0 临时桥接,Phase 1 重写时移除
|
||||
async fn chat_stream(
|
||||
&self,
|
||||
request: MessageRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError> {
|
||||
let chat_req = self.convert_to_chat_request(request);
|
||||
let chunk_stream = self.chat_stream_inner(chat_req).await?;
|
||||
// 利用已有的 parse_chunk_stream 映射为 StreamEvent 流
|
||||
Ok(crate::llm::stream::parse_chunk_stream(chunk_stream))
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
ProviderCapabilities {
|
||||
provider_name: "openai",
|
||||
supported_models: None,
|
||||
features: ProviderFeatures::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 辅助方法
|
||||
|
||||
```rust
|
||||
impl OpenaiProvider {
|
||||
/// Phase 0 临时:MessageRequest → ChatRequest
|
||||
fn convert_to_chat_request(&self, request: MessageRequest) -> ChatRequest {
|
||||
ChatRequest {
|
||||
model: request.model,
|
||||
// 将 Vec<Message> 转回 Vec<OpenaiChatMessage>
|
||||
messages: request.messages.iter()
|
||||
.map(|m| message_to_chat_message(m)) // 复用任务 8k 的转换函数
|
||||
.collect(),
|
||||
max_tokens: request.max_tokens,
|
||||
temperature: request.temperature,
|
||||
tools: request.tools.into(),
|
||||
tool_choice: Some(ToolChoice::Auto),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 0 临时:ChatResponse → MessageResponse
|
||||
fn convert_to_message_response(&self, response: ChatResponse) -> MessageResponse {
|
||||
MessageResponse {
|
||||
id: String::new(), // Phase 0 简化为空
|
||||
model: self.model.clone(),
|
||||
message: chat_message_to_message(&response.message), // 复用任务 8c
|
||||
usage: response.usage,
|
||||
stop_reason: map_stop_reason(response.stop_reason),
|
||||
extra: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
- 桥接代码全部标记 `// ponytail: Phase 0 临时桥接,Phase 1 重写时移除`
|
||||
- 如果 `OpenaiProvider` 当前没有 `chat_inner()` / `chat_stream_inner()` 分离,需要先重构提取内部方法
|
||||
- `ChatRequest` / `ChatResponse` 等旧类型仍存在且可用(未删除),桥接代码直接引用
|
||||
|
||||
### 验证点
|
||||
|
||||
- `OpenaiProvider` 实现 `LlmProvider` trait 后编译通过
|
||||
- `create_provider(ProviderType::OpenAI, config)` 返回的 `Box<dyn LlmProvider>` 可正常调用
|
||||
- `cargo test` 中 `OpenaiProvider` 相关测试通过
|
||||
|
||||
---
|
||||
|
||||
## 任务 11:测试适配
|
||||
|
||||
**对应**:10 号文档 §4 Phase 0 任务 11
|
||||
|
||||
### 范围
|
||||
|
||||
trait 签名变更后,以下测试文件中的 mock provider 和辅助函数需要适配:
|
||||
|
||||
### 11a:`src/llm/cycle.rs` 测试模块
|
||||
|
||||
现有 `MockProvider` 需要更新为新 trait 签名:
|
||||
|
||||
```rust
|
||||
struct MockProvider {
|
||||
response: Arc<Mutex<Option<MessageResponse>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmProvider for MockProvider {
|
||||
async fn chat(&self, _request: MessageRequest) -> Result<MessageResponse, LlmError> {
|
||||
self.response.lock().unwrap().take()
|
||||
.ok_or_else(|| LlmError::Other("no response".into()))
|
||||
}
|
||||
async fn chat_stream(
|
||||
&self, _request: MessageRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
ProviderCapabilities {
|
||||
provider_name: "mock",
|
||||
supported_models: None,
|
||||
features: ProviderFeatures::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
辅助函数更新:
|
||||
- `assistant_text_response(text)` → 返回 `MessageResponse` 而非 `ChatResponse`
|
||||
- `assistant_tool_call_response(...)` → 返回 `MessageResponse` 而非 `ChatResponse`
|
||||
- 测试用例断言从 `ChatResponse` 字段解构改为 `MessageResponse` 字段解构
|
||||
|
||||
### 11b:`src/agent/session.rs` 测试模块
|
||||
|
||||
- `MockProvider` 适配新 trait 签名(同上模式)
|
||||
- 响应构造 helper 改为构造 `MessageResponse`
|
||||
- 断言中 `response.message` 字段类型从 `OpenaiChatMessage` 变为 `Message`
|
||||
|
||||
### 11c:`src/agent/builder.rs` 测试模块
|
||||
|
||||
`StubProvider` 已在任务 7 中更新,无需额外修改。
|
||||
|
||||
### 11d:其他测试模块
|
||||
|
||||
按编译错误定位其余测试文件中的适配点。
|
||||
|
||||
### 验证点
|
||||
|
||||
- `cargo test` 所有测试通过(包括 cycle、session、builder 测试)
|
||||
|
||||
---
|
||||
|
||||
## 任务 12:编译驱动适配上游
|
||||
|
||||
**对应**:10 号文档 §4 Phase 0 任务 12
|
||||
|
||||
### 12a:`src/agent/session.rs`
|
||||
|
||||
- `submit_turn()` 返回类型从 `Result<ChatResponse, AgentError>` 改为 `Result<MessageResponse, AgentError>`
|
||||
- `response.usage` 引用适配(`MessageResponse.usage` 字段与 `ChatResponse.usage` 类型同为 `Usage`,无需修改)
|
||||
@@ -924,43 +1137,43 @@ fn message_to_chat_message(msg: &Message) -> OpenaiChatMessage {
|
||||
- 测试模块(`src/agent/session.rs` 中的 `#[cfg(test)]`)中的 `MockProvider` 需要适配新 trait 签名(返回 `MessageResponse` 而非 `ChatResponse`),并新增 `chat_stream` 和 `capabilities` 方法实现
|
||||
- `assistant_text()` 辅助函数改为构造 `MessageResponse` 而非 `ChatResponse`
|
||||
|
||||
### 9b:`src/agent/runtime.rs`
|
||||
### 12b:`src/agent/runtime.rs`
|
||||
|
||||
- 无直接修改(只引用 `Arc<dyn LlmProvider>`,trait 定义变更不影响持有者)
|
||||
- 但需验证 `RuntimeBundle` 中的 `provider: Arc<dyn LlmProvider>` 在 trait 签名变更后编译通过
|
||||
|
||||
### 9c:`src/agent/error.rs`
|
||||
### 12c:`src/agent/error.rs`
|
||||
|
||||
- 无预期变更(`AgentError` 不直接引用 `ChatResponse` 或相关类型)
|
||||
|
||||
### 9d:`src/memory/conversation.rs`
|
||||
### 12d:`src/memory/conversation.rs`
|
||||
|
||||
- `messages: Vec<OpenaiChatMessage>` 保持不动(Phase 2 才切换为 `Vec<Message>`)
|
||||
- 但 `ConversationMemory` 的 `add_message()` 和 `get_history()` 方法签名需要确认是否需要适配
|
||||
- `serde_json::from_str::<OpenaiChatMessage>` 反序列化保持使用旧类型
|
||||
|
||||
### 9e:`src/prompt/composer.rs`
|
||||
### 12e:`src/prompt/composer.rs`
|
||||
|
||||
- `build_request()` 返回 `OpenaiChatRequest` 保持不变(旧类型仍存在)
|
||||
- `validate_messages()` 继续使用 `&[OpenaiChatMessage]` 保持不变
|
||||
|
||||
### 9f:`src/llm/cycle/retry.rs`
|
||||
### 12f:`src/llm/cycle/retry.rs`
|
||||
|
||||
- 检查是否有对 `ChatRequest` / `ChatResponse` 的引用,如有则更新为 `MessageRequest` / `MessageResponse`
|
||||
- 具体检查点:`retry_with_backoff` 函数的请求参数类型、`should_retry` 函数的响应/错误类型、以及 `retry_policy` 配置中涉及的类型引用
|
||||
|
||||
### 9g:`src/llm/hooks.rs`
|
||||
### 12g:`src/llm/hooks.rs`
|
||||
|
||||
- `HookContext::with_request` 参数类型从 `&ChatRequest` 改为 `&MessageRequest`
|
||||
- **决策锁定**:直接修改 `with_request` 签名,不做双方法兼容。理由:Phase 0 完成后旧 `ChatRequest` 只在旧 Provider 内部使用(`src/llm/provider/openai.rs`),hook 层不应引用旧 request 类型。
|
||||
- 同步更新 `HookContext` 中所有引用 `ChatRequest` 的字段和方法
|
||||
|
||||
### 9h:`src/llm/compact.rs`
|
||||
### 12h:`src/llm/compact.rs`
|
||||
|
||||
- 暂时不修改(`estimate_message_tokens` / `microcompact` 继续使用 `OpenaiChatMessage`)
|
||||
- Phase 2 才做切换
|
||||
|
||||
### 9i:`examples/` 目录
|
||||
### 12i:`examples/` 目录
|
||||
|
||||
- 运行 `grep -r "ChatResponse\|ChatRequest" examples/` 预检
|
||||
- 如现有 example 直接引用旧类型,同步适配为 `MessageResponse` / `MessageRequest`
|
||||
@@ -974,6 +1187,7 @@ fn message_to_chat_message(msg: &Message) -> OpenaiChatMessage {
|
||||
2. **单元测试**:`cargo test` 全部通过
|
||||
3. **clippy 检查**:`cargo clippy` 无新增警告
|
||||
4. **diff 确认**:`git diff --stat` 确认修改范围符合预期
|
||||
5. **桥接标记确认**:`grep "ponytail: Phase 0 临时桥接" src/llm/provider/openai.rs` 返回结果,确保 Phase 1 移除时易定位
|
||||
|
||||
## 回滚方案
|
||||
|
||||
@@ -981,12 +1195,17 @@ fn message_to_chat_message(msg: &Message) -> OpenaiChatMessage {
|
||||
|---------|------|
|
||||
| 新类型设计发现重大缺陷(如 Message 枚举扁平度不足) | 回退 git 到 Phase 0 开始前的 commit,保留 9 系文档作为参照,重启设计评审 |
|
||||
| 临时转换层(`chat_message_to_message` / `message_to_chat_message`)逻辑错误 | 修复转换函数逻辑,不修改类型定义 |
|
||||
| 单个 Provider 的适配不合理 | 将该 Provider 回退为 `unimplemented!()`(当前状态),不影响其他组件 |
|
||||
| `StreamEvent` 重导出或 `old_stream.rs` 迁移引入兼容性问题 | 恢复 `stream.rs` 中的旧 `StreamEvent` 定义,改为 `as` 别名消歧方案 |
|
||||
| `OpenaiProvider` 桥接实现不合理 | 将该 Provider 回退为 `unimplemented!()`,不影响其他组件 |
|
||||
| Phase 0 完成时发现未预见的设计问题 | **不打乱整体进度**:在不动已有文件的前提下,直接原地修改新类型文件重新迭代,不需要整个回退到 Phase 0 之前。Phase 0 结束时打 tag `types-v2-prototype` 作为 checkpoint |
|
||||
|
||||
**风险储备**:
|
||||
- `ChatRequest` / `ChatResponse` 等旧类型保留不动,不主动删除。Phase 2 切换后通过 `#[deprecated]` 标记引导迁移
|
||||
- 如果 `OpenaiProvider` 保留旧实现,旧 trait 方法名不变、只是签名变,不影响共存
|
||||
- `ChatRequest` / `ChatResponse` / `Message` / `ContentBlock` / `ToolDefinition` / `StopReason` 等类型别名和旧类型结构体的弃用路径(对应 10 号文档 §6):
|
||||
- **Phase 0 完成时**:旧别名**保留**(作为编译桥接),新类型通过不同路径访问
|
||||
- **Phase 1 完成时**:Provider 实现切换到新类型,旧别名字面保留
|
||||
- **Phase 2 完成时**:对旧别名加 `#[deprecated]` 标记
|
||||
- **下个版本**:确认无外部引用后删除
|
||||
- `StreamEvent` 通过 `old_stream.rs` + 重导出方案处理,若遇到不可预见的编译问题,备选方案为改回 `as` 别名消歧
|
||||
|
||||
## 开放事项
|
||||
|
||||
|
||||
Reference in New Issue
Block a user