Files
agcore/design/pdd/19-phase13-cleanup-and-fork-merge.md
T
徐涛 28ca43ccb2 chore(docs): 将设计文档从 docs 移至 design 目录
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
2026-07-23 05:45:53 +08:00

31 KiB
Raw Blame History

Phase 13 — 热身清理 + ContextSlot fork/merge 实施方案

  • 文档编号19
  • 标题Phase 13 — 热身清理 + ContextSlot fork/merge 实施方案
  • 日期2026-07-08
  • 状态:待实施
  • 涉及模块agent/context、agent/session、llm/types、llm/provider/openai、llm/stream
  • 关联文档roadmap.md(§Phase 13)、17-phase10-contextslot.md
  • 对应Roadmap §Phase 13v0.3.0 第一阶段)

1. 背景与目标

v0.3.0 是 agcore 从"LLM 调用工具箱"升级为"多 Agent 基础系统"的关键版本。Phase 13 是 v0.3.0 的第一阶段,定位为"热身",包含两大部分:

  • 技术债清理:删除 Phase 0 遗留的旧 types 文件(request.rsresponse.rsold_stream.rs),以及已标记 #[deprecated]ChatResponse 结构体
  • ContextSlot fork/merge:为 ContextSlot 增加分叉和合并能力,为后续 Phase 17 Checkpointer 和 Phase 18 SubAgent Dispatch 打基础

依赖关系:无(独立交付)

优先级P0

预估规模:净减 ~200 行代码(新增 ~505 行,删除 ~704 行)


2. 需求分析

2.1 功能需求

  1. 技术债清理:删除 src/llm/types/request.rs187 行)、response.rs177 行)、old_stream.rs45 行),将其中的 OpenAI wire-format 类型移入 src/llm/provider/openai.rs;删除 types/mod.rs 中的 ChatResponse 废弃结构体
  2. ContextSlot::fork:从现有 context slot 分支出独立的子 slot
  3. ContextSlot::merge:将子 slot 的消息合并回父 slot
  4. MergeStrategy 枚举:Append(追加)/ Replace(替换),#[non_exhaustive] 预留 Phase 16 Summarize 扩展

2.2 非功能需求

  • 每步可编译:5 个 Step 按物理文件切割,每步 cargo build --all-targets + cargo test 验证
  • 指定公共 API 路径保持向后兼容agcore::llm::types::ToolChoicere-export 不变)、crate::llm::stream::StreamEvent(重导出保留);其余 wire-format 类型(OpenaiChatRequestOpenaiChatResponse/ChunkStreamOptions 等)移入 provider/openai.rs 后属 Breaking Change,详见 §4.3 CHANGELOG
  • 向后兼容的 StreamEvent 路径crate::llm::stream::StreamEvent 重导出保留,不修改 cycle.rssession.rs 的 import

3. 方案设计

3.1 整体架构

Phase 13 分为 5 个 Step,按执行顺序排列:

Step 13.5 (fork/merge) → Step 13.4 (ToolChoice) → Step 13.1 (request types) → Step 13.2 (response types) → Step 13.3 (cleanup)

这种顺序的好处:

  • 先交付价值:13.5 是唯一有用户功能交付的 Step,先做建立节奏
  • 排序约束13.4 必须先于 13.1ToolChoice 不搬走,request.rs 不能删)
  • 13.3 收尾:删除旧文件和 ChatResponse 是 breaking change,放在最后

3.2 Step 13.5 — ContextSlot fork/merge

MergeStrategy 枚举

定义在 src/agent/context.rs

#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MergeStrategy {
    /// 子 slot 消息追加到父 slot 末尾。
    Append,
    /// 用子 slot 消息替换父 slot 内容。
    Replace,
}
  • #[non_exhaustive] 保证 Phase 16 加入 Summarize 变体时不破坏现有代码
  • 不预埋 Summarize 占位变体(YAGNI 原则)

ContextSlot::fork

impl ContextSlot {
    pub fn fork(&self, child_id: String, strategy: DeriveStrategy) -> ContextSlot {
        let messages = match &strategy {
            DeriveStrategy::Full => self.messages.clone(),
            DeriveStrategy::Focused(cfg) => Self::filter_focused(&self.messages, cfg),
        };
        tracing::debug!(
            parent_id = %self.id,
            child_id = %child_id,
            ?strategy,
            "ContextSlot::fork"
        );
        ContextSlot {
            id: child_id,
            session_id: self.session_id.clone(),
            config: SlotConfig {
                mode: match &strategy {
                    DeriveStrategy::Full => SlotMode::Full,
                    DeriveStrategy::Focused(cfg) => SlotMode::Focused(cfg.clone()),
                },
                source: SlotSource::Derived {
                    parent_id: self.id.clone(),
                    strategy,
                },
                budget: self.config.budget.clone(),
                compact: self.config.compact,
            },
            messages,
            meta: SlotMeta::new(),
        }
    }
}

设计要点:

  • 纯数据层操作,不持久化
  • 子 slot 的 meta 全新创建(SlotMeta::new()),不继承父 slot 的 message_count
  • 子 slot 的 source 记录 parent_id,血缘可追溯
  • 添加 tracing::debug! 日志,支持多 slot 交互场景的审计追踪

ContextSlot::merge

impl ContextSlot {
    /// 将子 slot 的消息合并到当前 slot。
    ///
    /// **注意**:本方法仅操作内存数据,不自动持久化。
    /// 调用方需在 merge 后自行调用 `self.save(&store)` 将结果写入后端存储。
    pub fn merge(&mut self, child: ContextSlot, strategy: MergeStrategy) -> Result<(), AgentError> {
        // 防御性检查
        if self.id == child.id {
            return Err(AgentError::Config("不能将 slot 合并到自身".into()));
        }
        if self.session_id != child.session_id {
            return Err(AgentError::Config("不能合并不同 session 的 slot".into()));
        }
        if matches!(self.config.mode, SlotMode::Readonly) {
            return Err(AgentError::SlotReadonly("Readonly slot 不允许合并".into()));
        }

        tracing::debug!(
            self_id = %self.id,
            child_id = %child.id,
            ?strategy,
            "ContextSlot::merge"
        );

        match strategy {
            MergeStrategy::Append => {
                let count = child.messages.len();
                self.messages.extend(child.messages);
                self.meta.message_count += count;
            }
            MergeStrategy::Replace => {
                self.messages = child.messages;
                self.meta.message_count = self.messages.len();
            }
        }
        Ok(())
    }
}

AgentSession::derive_slot 重构

现有 derive_slotsession.rs:213-260)的手工复制代码改为调用 parent.fork()

pub async fn derive_slot(
    &mut self,
    id: impl Into<String>,
    parent_id: &str,
    strategy: DeriveStrategy,
) -> Result<(), AgentError> {
    let slot_id = id.into();
    if self.slots.contains_key(&slot_id) {
        return Err(AgentError::SlotAlreadyExists(slot_id));
    }
    let parent = self
        .slots
        .get(parent_id)
        .ok_or_else(|| AgentError::SlotNotFound(parent_id.to_string()))?;
    let child = parent.fork(slot_id.clone(), strategy); // ← 用 fork
    child.save(&*self.resolve_store()).await?;
    self.slots.insert(slot_id, child);
    Ok(())
}

重复检查、查找父 slot 的代码不变;消息复制逻辑委托给 fork()

测试计划(新增 9 个)

测试名 验证点
fork_full_copies_messages fork Full 策略复制父 slot 全部消息
fork_focused_filters_messages fork Focused 策略按 config 过滤
fork_preserves_independence 父 slot 追加消息不影响子 slot
fork_sets_derived_source 子 slot source 正确记录 parent_id
merge_append_appends_messages Append 追加到父 slot 末尾,message_count 正确
merge_replace_replaces_messages Replace 替换父 slot 消息,message_count 正确
merge_self_rejected self-merge 返回 Err
merge_readonly_rejected 合并到 Readonly slot 返回 Err
merge_cross_session_rejected 跨 session 合并返回 Err

3.3 Step 13.4 — ToolChoice 移入 tool.rs

变更文件

文件 变更
src/llm/types/request.rs 删除 ToolChoice 枚举 + serde impl~28-99 行)
src/llm/types/tool.rs 新增 ToolChoice 枚举 + serde impl(原样搬入)
src/llm/types/mod.rs pub use request::{..., ToolChoice}pub use tool::ToolChoice
src/llm/types/request_v2.rs import 路径 request::ToolChoicetool::ToolChoice

import 路径变化

当前 移动后
crate::llm::types::request::ToolChoice crate::llm::types::tool::ToolChoice
crate::llm::types::ToolChoice(通过 re-export crate::llm::types::ToolChoice(通过 tool.rs re-export,保持不变)

验证cargo build --all-targets + cargo test + cargo clippy

3.4 Step 13.1 — request.rs 类型移入 openai.rs

变更文件

文件 变更
src/llm/types/request.rs 整文件删除187 行)
src/llm/provider/openai.rs 新增 StreamOptionsOpenaiToolAudioParamPredictionContentUserLocationApproximateWebSearchOptionsOpenaiChatRequest 等类型定义
src/llm/types/mod.rs 删除 pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions};删除 pub mod request;
src/llm/provider/openai.rs import 调整 use crate::llm::types::request::{...} 改为从同级 use super::super::types::... 或直接使用本文件内类型

注意OpenaiTool 引用 OpenaiToolDefinition(定义在 tool.rs),移入 openai.rs 后需通过 crate::llm::types::tool::OpenaiToolDefinition 引用。OpenaiChatRequest.messages 字段引用 OpenaiChatMessage(定义在 openai_message.rs),路径不变。

设计决策:搬入 openai.rs 后的类型可见性可降级为 pub(crate)。它们是与 OpenAI wire-format 绑定的内部序列化类型,公共 API 消费者不应直接接触。

验证cargo build --all-targets + cargo test + cargo clippy

3.5 Step 13.2 — response.rs 类型移入 openai.rs

变更文件

文件 变更
src/llm/types/response.rs 整文件删除177 行)
src/llm/provider/openai.rs 新增 TokenLogprobTopLogprobLogprobsURLCitationAnnotationOpenaiAudioChoiceOpenaiChatResponseDeltaChunkChoiceOpenaiChatChunk + From<OpenaiChatMessage> for Delta + From<OpenaiChatResponse> for OpenaiChatChunk
src/llm/types/mod.rs 删除 pub use response::{...};删除 pub mod response;
src/llm/stream.rs:26 use crate::llm::types::{OpenaiChatChunk, OpenaiToolCall} 中的 OpenaiChatChunk 路径改为 crate::llm::provider::openai::OpenaiChatChunkOpenaiToolCall 保持从 tool.rs

验证cargo build --all-targets + cargo test + cargo clippy

3.6 Step 13.3 — 旧文件清理 + ChatResponse 删除

13.3a — 删除 old_stream.rs

前置验证:实施前执行 grep -rn 'parse_chunk_stream\|map_legacy_to_ir\|LegacyToIrEventStream\|ChunkToLegacyEventStream' src/ 确认零外部调用方,记录结果到实施 commit。

文件 变更
src/llm/types/old_stream.rs 整文件删除45 行,LegacyStreamEvent
src/llm/types/mod.rs 删除 pub mod old_stream;
src/llm/stream.rs 删除 use crate::llm::types::old_stream::LegacyStreamEvent;删除 parse_chunk_streamparse_chunk_stream_legacyChunkToLegacyEventStreamLegacyToIrEventStreammap_legacy_to_irempty_message_response~160 行死代码)

stream.rs 最终形态

//! 流式事件系统 —— 重导出 StreamEvent 供向后兼容。
pub use crate::llm::types::response_v2::StreamEvent;

为什么不全删 stream.rscycle.rssession.rsuse crate::llm::stream::StreamEvent 路径保持不变。全删 + 改所有 import 路径的改动量 > 收益。保留 1 行重导出就够。

13.3b — 删除 ChatResponse

文件 变更
src/llm/types/mod.rs 删除 ChatResponse 结构体定义 + 两个 #[allow(deprecated)] From implFrom<OpenaiChatResponse> for ChatResponseFrom<ChatResponse> for OpenaiChatChunk

ChatResponse 自 v0.1.0 起标记 #[deprecated]v0.2.0-rc.1 阶段直接删除即可。删除前运行 cargo doc --no-deps 2>&1 | grep -i 'ChatResponse' 确认零文档引用。

验证cargo build --all-targets + cargo test + cargo clippy + cargo doc --no-deps


4. 实现计划

4.1 实施顺序总览

Step 13.5 ──→ Step 13.4 ──→ Step 13.1 ──→ Step 13.2 ──→ Step 13.3
 (fork/merge)  (ToolChoice)  (request)      (response)     (cleanup)
      │              │             │              │              │
      ▼              ▼             ▼              ▼              ▼
  +60 行净增       -0 净增       -0 净增        -0 净增        -260 删除
  +9 个测试       import 路径  纯类型搬移     纯类型搬移     +1 行重导出
                  变更

4.2 各 Step 文件变更清单

Step 13.5 — ContextSlot fork/merge

操作 文件 变更说明
新增 src/agent/context.rs MergeStrategy 枚举 + ContextSlot::fork() + ContextSlot::merge()
重构 src/agent/session.rs derive_slot 改为调用 parent.fork()
新增 内联测试 9 个新测试(fork, merge, 边界)

Step 13.4 — ToolChoice 移动

操作 文件 变更说明
删除 src/llm/types/request.rs 移除 ToolChoice 枚举 + serde impl
新增 src/llm/types/tool.rs 增加 ToolChoice 枚举 + serde impl
修改 src/llm/types/mod.rs 更新 re-export 路径
修改 src/llm/types/request_v2.rs 更新 import 路径

Step 13.1 — request 类型搬移

操作 文件 变更说明
删除 src/llm/types/request.rs 整文件删除(187 行)
新增 src/llm/provider/openai.rs 增加所有 OpenAI wire-format 类型
修改 src/llm/types/mod.rs 删除 re-export + mod 声明

Step 13.2 — response 类型搬移

操作 文件 变更说明
删除 src/llm/types/response.rs 整文件删除(177 行)
新增 src/llm/provider/openai.rs 增加所有 OpenAI wire-format 类型 + From impl
修改 src/llm/types/mod.rs 删除 re-export + mod 声明
修改 src/llm/stream.rs 更新 OpenaiChatChunk import 路径

Step 13.3 — 旧文件清理

操作 文件 变更说明
删除 src/llm/types/old_stream.rs 整文件删除(45 行)
修改 src/llm/types/mod.rs 删除 pub mod old_stream; + 删除 ChatResponse 结构体 + From impl
修改 src/llm/stream.rs 删除所有死代码,仅保留 pub use 重导出

4.3 回滚策略

所有 Step 通过 git commit 管理,回退时 git revert <commit> 即可。每个 Step 独立编译,回滚不会级联依赖。若 Step 13.3(ChatResponse 删除)导致外部编译失败,单独 revert 该 commit 即可恢复 ChatResponse + old_stream.rs

4.4 CHANGELOG 条目

## [0.3.0] - 未发布

### Breaking Changes

**类型路径变更(0.3.0):**
- `agcore::llm::types::request::ToolChoice``agcore::llm::types::tool::ToolChoice`(公共 re-export 路径 `agcore::llm::types::ToolChoice` 保持不变)
- `agcore::llm::types::request::StreamOptions``agcore::llm::provider::openai::StreamOptions`
- `agcore::llm::types::request::OpenaiChatRequest``agcore::llm::provider::openai::OpenaiChatRequest`
- `agcore::llm::types::response::OpenaiChatResponse``agcore::llm::provider::openai::OpenaiChatResponse`
- `agcore::llm::types::response::OpenaiChatChunk``agcore::llm::provider::openai::OpenaiChatChunk`
- 其余 `request.rs`/`response.rs` 中的 wire-format 类型(`OpenaiTool``AudioParam``Choice``Delta` 等)同步移入 `agcore::llm::provider::openai` 模块

**类型删除:**
- `agcore::llm::types::ChatResponse` 已删除(自 v0.1.0 标记 `#[deprecated]`,请改用 `MessageResponse`
- `agcore::llm::types::old_stream::LegacyStreamEvent` 已删除(内部死代码)

### Features
- `ContextSlot::fork(child_id, strategy)` — 从父槽派生独立的子槽(数据层操作)
- `ContextSlot::merge(child, strategy)` — 将子槽消息合并回父槽(支持 Append/Replace
- `MergeStrategy` 枚举(`#[non_exhaustive]`Phase 16 可扩展 Summarize

5. 风险评估

风险 影响 概率 缓解措施
ChatResponse 被外部 crate 引用 编译 break 中 — #[deprecated] 仅产生编译警告,外部 crate 可能通过 #[allow(deprecated)] 静默依赖 CHANGELOG 明确标注语义版本(0.3.0)和迁移指引;Step 13.3 验收加入 cargo doc --no-deps | grep ChatResponse 确认零引用
StreamOptions 等 wire-format 类型路径变更影响直接引用消费者 编译 break 低(v0.2.0-rc.1,极少外部消费者使用内部类型) CHANGELOG 完整列出所有路径变更;编译错误立即可发现
parse_chunk_stream 有隐藏调用方 编译 break 极低(实施前执行 grep -rn 'parse_chunk_stream|map_legacy_to_ir|LegacyToIrEventStream' src/ 前置验证) Step 13.3 前运行 grep 验证并记录结果;cargo build --all-targets 可 100% 捕获
#[allow(deprecated)] 遗漏 clippy 警告 cargo clippy --all-targets -- -D warnings 验证
Step 顺序错误导致编译中间态 开发者体验差 严格按 13.5→13.4→13.1→13.2→13.3 执行;每步 cargo build 验证
stream.rs 简化后 import 断链 编译 break 极低 保留 pub use 重导出路径,cycle.rs/session.rs import 不变

6. 验收标准

M9 里程碑(Phase 13 完成条件)

# 条件 验证方法
1 request.rsresponse.rsold_stream.rs 三个旧文件不存在 ls src/llm/types/ 确认
2 ChatResponse 结构体不存在 全局搜索 ChatResponse 仅保留 openai.rsOpenaiChatResponse 引用
3 ToolChoicetool.rs 中定义,公共路径 agcore::llm::types::ToolChoice 保持不变 cargo doc --no-deps 确认类型文档
4 OpenaiChatRequest/Response/Chunkprovider/openai.rs 中定义 编译通过
5 ContextSlot::fork() 单元测试通过(P0 条件全部满足) cargo test
6 ContextSlot::merge() 单元测试通过(P0 条件全部满足) cargo test
7 stream.rs 只保留 pub use 重导出 文件内容确认
8 cargo build --all-targets 编译通过 编译验证
9 cargo test --all-targets 全绿(预期 283~285 测试) 测试验证
10 cargo clippy --all-targets -- -D warnings 0 警告 clippy 验证
11 CHANGELOG 包含 Phase 13 的 Breaking Changes 和 Features 条目 文件确认

fork/merge 详细验收 P0 项

fork 的 5 项 P0 条件:

# 条件 优先级
1 fork("child", Full) 创建新 slot,消息在 fork 时刻 == 父 slot P0
2 子 slot 获得独立消息列表——父 slot 后续追加不影响子 slot P0
3 子 slot 的 source 标记为 Derived { parent_id, strategy } P0
4 子 slot 可独立持久化(fork + save + load roundtrip P0
5 fork 不允许重复 id(返回 SlotAlreadyExists)(由 derive_slot 编排层保证) P0

merge 的 5 项 P0 条件:

# 条件 优先级
1 parent.merge(child, Append) 子消息追加到父末尾 P0
2 parent.merge(child, Replace) 子消息替换父全量消息 P0
3 merge 后父 slot 的 meta.message_count 正确更新 P0
4 merge 不允许合并到 Readonly 目标 slot P0
5 merge 不允许 self-mergechild.id == parent.id P0

参考来源

  • Roadmapdocs/roadmap.md §Phase 13
  • ContextSlot 设计:docs/17-phase10-contextslot.md
  • 旧 StreamEvent 设计:src/llm/stream.rs 文件注释
  • 当前代码库:src/llm/types/request.rssrc/llm/types/response.rssrc/llm/types/old_stream.rssrc/llm/types/mod.rssrc/llm/provider/openai.rssrc/agent/context.rssrc/agent/session.rs

7. 实施计划

全局说明

commit 策略:每个 Step 一个独立 commit。commit message 格式:

<type>(<scope>): <中文描述>
  • Step 13.5 → feat(agent): 实现 ContextSlot fork/merge
  • Step 13.4 → refactor(types): ToolChoice 移入 tool.rs
  • Step 13.1 → refactor(types): request.rs 类型移入 provider/openai.rs
  • Step 13.2 → refactor(types): response.rs 类型移入 provider/openai.rs
  • Step 13.3 → refactor(types): 删除旧类型文件和 ChatResponse

验证命令(每步通用)

cargo build --all-targets && cargo test && cargo clippy --all-targets -- -D warnings

预计测试数量变化

  • 当前基线:277 测试(每个 Step 开始时 cargo test 确认)
  • Step 13.5 后:286+9
  • Step 13.4-13.2 后:286(无变化)
  • Step 13.3 后:285-1ChatResponseFrom impl 无测试直接引用,删除后仅 types/mod.rs 中的 deprecated 注释行减少,不影响测试计数。实施前执行 grep -rn 'ChatResponse' src/ --include='*test*' --include='*tests*' 确认零测试引用)
  • 最终范围:285 测试

Step 13.5 — ContextSlot fork/merge

前置依赖:无(纯新增,不依赖前序 Step

任务描述:在 agent/context.rs 中新增 MergeStrategy 枚举、ContextSlot::fork() 方法和 ContextSlot::merge() 方法;重构 agent/session.rs 中的 derive_slot 改为调用 parent.fork();新增 9 个内联测试覆盖 fork/merge 的 happy path 和 error path。

涉及文件

  • src/agent/context.rs — 新增枚举和方法
  • src/agent/session.rs — 重构 derive_slot
  • src/agent.rs — 追加 MergeStrategy re-export

具体操作

  1. context.rs 中新增 MergeStrategy 枚举(Append / Replace#[non_exhaustive]
  2. context.rsimpl ContextSlot 块内新增 fork(&self, child_id: String, strategy: DeriveStrategy) -> ContextSlot 方法
  3. context.rsimpl ContextSlot 块内新增 merge(&mut self, child: ContextSlot, strategy: MergeStrategy) -> Result<(), AgentError> 方法(含 self-merge/cross-session/Readonly 三项防御检查 + tracing::debug! 日志)
  4. session.rsderive_slot 方法中将手工消息复制代码替换为 parent.fork(slot_id, strategy)
  5. agent.rspub use context::{...} 列表中追加 MergeStrategy
  6. context.rs#[cfg(test)] mod tests 中新增 9 个测试用例

注意:重构后 derive_slot 的子 slot budgetContextBudget::default() 变为继承父 slotcompacttrue 变为继承父 slot。由于 ContextBudget 在 v0.2 无消费逻辑且父 slot 的 compact 默认也为 true,此变化无实际影响。验收条件中"行为不变"指对外功能行为不变(slot 消息内容、血缘关系不变)。

预估工作量M1-4h

风险等级:低(纯新增,不修改已有逻辑路径)

验收条件

  • MergeStrategy 枚举存在,AppendReplace 两个变体可用,且通过 agcore::agent::MergeStrategy 路径可访问
  • ContextSlot::fork 返回的 child 在 fork 时刻消息等于父 slot
  • fork Focused 策略按 FocusedConfig 过滤消息
  • 父 slot 后续追加消息不影响子 slot
  • 子 slot 的 source 正确记录 Derived { parent_id, strategy }
  • parent.merge(child, Append) 追加到父末尾,message_count 正确
  • parent.merge(child, Replace) 替换父全量消息,message_count 正确
  • self-merge 返回 Err(AgentError::Config)
  • merge 到 Readonly slot 返回 Err(AgentError::SlotReadonly)
  • 跨 session merge 返回 Err(AgentError::Config)
  • derive_slot 对外行为不变(slot 消息内容、血缘关系、持久化行为均不变;内部 budget/compact 继承差异无实际影响),测试全绿
  • cargo doc --no-deps 无 warning(验证新增公开 API 的文档注释完整)

回退方式git revert 该 commit

Step 13.4 — ToolChoice 移入 tool.rs

前置依赖:Step 13.5(顺序约束:必须早于 Step 13.1——若 Step 13.1 先执行会将 ToolChoicerequest.rs 一同删除,导致本 Step 无可搬移的源)

任务描述:将 ToolChoice 枚举及其 serde 实现从 types/request.rs 搬移到 types/tool.rs,更新所有 import/path 引用。公共 re-export 路径 agcore::llm::types::ToolChoice 保持不变。

涉及文件

  • src/llm/types/request.rs — 删除 ToolChoice~28-99 行)
  • src/llm/types/tool.rs — 新增 ToolChoice 枚举 + serde impl
  • src/llm/types/mod.rs — re-export 路径从 request 改为 tool
  • src/llm/types/request_v2.rs — import 路径从 request:: 改为 tool::

具体操作

  1. request.rs 复制 ToolChoice 枚举 + Serialize/Deserialize impl 到 tool.rs
  2. request.rs 中删除 ToolChoice 定义
  3. mod.rs 中将 pub use request::{..., ToolChoice} 改为 pub use tool::ToolChoice
  4. request_v2.rs 中将 use crate::llm::types::request::ToolChoice 改为 use crate::llm::types::tool::ToolChoice
  5. 验证 cycle.rsuse crate::llm::types::ToolChoice(通过 re-export)路径不变

预估工作量S<1h

风险等级:低(有限的 import 路径变更,编译立即可发现)

验收条件

  • ToolChoicetool.rs 中定义
  • pub use tool::ToolChoicemod.rs
  • request_v2.rs 编译通过
  • cycle.rs 路径不变
  • cargo build --all-targets + cargo test + cargo clippy 全绿

回退方式git revert 该 commit

Step 13.1 — request.rs 类型移入 openai.rs

前置依赖Step 13.4ToolChoice 已移走,request.rs 剩余内容全是 OpenAI wire-format 专有类型)

任务描述:删除 types/request.rs 整文件,将所有剩余类型(OpenaiChatRequestStreamOptionsOpenaiToolAudioParamPredictionContentUserLocationApproximateWebSearchOptions)搬入 provider/openai.rs,更新 mod.rs re-export。

涉及文件

  • src/llm/types/request.rs — 整文件删除
  • src/llm/provider/openai.rs — 新增所有类型定义
  • src/llm/types/mod.rs — 删除 re-export + mod 声明

具体操作

  1. request.rs 复制所有剩余类型定义到 openai.rs,可见性设为 pub(crate)
  2. OpenaiTool 内引用 OpenaiToolDefinition(定义在 tool.rs),路径改为 crate::llm::types::tool::OpenaiToolDefinition
  3. 删除 openai.rs 中原 use crate::llm::types::request::{...} import
  4. mod.rs 删除 pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions}pub mod request;
  5. 删除 types/request.rs 文件

预估工作量M1-4h

风险等级:低(纯搬移 + 删除,文件内无逻辑变更)

验收条件

  • request.rs 文件不存在
  • OpenaiChatRequest 等类型在 openai.rs 中定义,编译通过
  • OpenaiTool 通过 crate::llm::types::tool::OpenaiToolDefinition 正确引用
  • cargo build --all-targets + cargo test + cargo clippy 全绿

回退方式git revert 该 commit。若 Step 13.2 也已提交,单独 revert 本 Step 可能因 provider/openai.rs 并发修改产生合并冲突。安全回退顺序为逆序:先 revert 13.2,再 revert 13.1。

Step 13.2 — response.rs 类型移入 openai.rs

前置依赖:无(与 Step 13.1 共享 provider/openai.rstypes/mod.rs,但本 Step 仅追加类型定义,无覆盖操作;建议在 13.1 之后顺序执行以避免并行时的合并冲突)

任务描述:删除 types/response.rs 整文件,将所有类型(OpenaiChatResponseOpenaiChatChunkChoiceDeltaChunkChoice 等 + 两个 From impl)搬入 provider/openai.rs,更新 mod.rsstream.rs 的 import 路径。

涉及文件

  • src/llm/types/response.rs — 整文件删除
  • src/llm/provider/openai.rs — 新增所有类型定义 + From impl
  • src/llm/types/mod.rs — 删除 re-export + mod 声明
  • src/llm/stream.rsOpenaiChatChunk import 路径改为 provider::openai

具体操作

  1. response.rs 复制所有类型定义(含 From impl)到 openai.rs,可见性设为 pub(crate)
  2. 删除 openai.rs 中原 use crate::llm::types::response::{...} import
  3. mod.rs 删除 pub use response::{...}pub mod response;
  4. stream.rs:26OpenaiChatChunk 的 import 路径改为 crate::llm::provider::openai::OpenaiChatChunkOpenaiToolCall 路径不变)
  5. 删除 types/response.rs 文件

预估工作量M1-4h

风险等级:低(与 Step 13.1 模式完全相同)

验收条件

  • response.rs 文件不存在
  • OpenaiChatResponse/Chunk 等类型在 openai.rs 中定义,编译通过
  • stream.rs import 路径正确
  • cargo build --all-targets + cargo test + cargo clippy 全绿

回退方式git revert 该 commit。若 Step 13.1 和本 Step 均已提交,安全回退顺序为逆序:先 revert 本 Step,再 revert 13.1。

Step 13.3 — 旧文件清理 + ChatResponse 删除

前置依赖Step 13.1request.rs 已删)、Step 13.2response.rs 已删)

任务描述:删除 old_stream.rsChatResponse,简化 stream.rs 为仅保留 pub use 重导出。这是 Phase 13 技术风险最高的 Step。

涉及文件

  • src/llm/types/old_stream.rs — 整文件删除
  • src/llm/types/mod.rs — 删除 pub mod old_stream; + 删除 ChatResponse 结构体和两个 From impl
  • src/llm/stream.rs — 删除死代码(约 160 行),仅保留 pub use 重导出

具体操作

  1. 前置验证 A:执行 grep -rn 'parse_chunk_stream\|map_legacy_to_ir\|LegacyToIrEventStream\|ChunkToLegacyEventStream' src/ 确认零外部调用方,记录结果到 commit message
  2. 前置验证 B:执行 cargo doc --no-deps 2>&1 | grep -i 'ChatResponse' 确认零文档引用,记录结果
  3. mod.rs 删除 pub mod old_stream;
  4. mod.rs 删除 ChatResponse 结构体定义 + #[allow(deprecated)] From<OpenaiChatResponse> for ChatResponse + From<ChatResponse> for OpenaiChatChunk
  5. 删除 old_stream.rs 文件
  6. stream.rs 删除:use crate::llm::types::old_stream::LegacyStreamEventparse_chunk_streamparse_chunk_stream_legacyChunkToLegacyEventStreamLegacyToIrEventStreammap_legacy_to_irempty_message_response
  7. stream.rs 最终只保留 module doc comment + pub use crate::llm::types::response_v2::StreamEvent;
  8. 检查 cycle.rs:88#[allow(deprecated)] 属性是否仍与 ChatResponse 相关——若不相关则无需改动;若因 ChatResponse 删除而变脏,清理该属性

预估工作量S<1hcleanup+ M(需验证过程)

风险等级:中(ChatResponse 删除是 Breaking Change,外部可能静默依赖)

验收条件

  • old_stream.rs 文件不存在
  • ChatResponse 结构体不存在(全局搜索仅保留 OpenaiChatResponse 引用)
  • stream.rs 只保留 pub use 重导出
  • cargo build --all-targets 编译通过
  • cargo test --all-targets 全绿(预期 285 测试)
  • cargo clippy --all-targets -- -D warnings 0 警告
  • cargo doc --no-deps 无 warning

回退方式git revert 该 commit(单独 revert 即可恢复 ChatResponse + old_stream.rs