将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
31 KiB
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 13(v0.3.0 第一阶段)
1. 背景与目标
v0.3.0 是 agcore 从"LLM 调用工具箱"升级为"多 Agent 基础系统"的关键版本。Phase 13 是 v0.3.0 的第一阶段,定位为"热身",包含两大部分:
- 技术债清理:删除 Phase 0 遗留的旧 types 文件(
request.rs、response.rs、old_stream.rs),以及已标记#[deprecated]的ChatResponse结构体 - ContextSlot fork/merge:为 ContextSlot 增加分叉和合并能力,为后续 Phase 17 Checkpointer 和 Phase 18 SubAgent Dispatch 打基础
依赖关系:无(独立交付)
优先级:P0
预估规模:净减 ~200 行代码(新增 ~505 行,删除 ~704 行)
2. 需求分析
2.1 功能需求
- 技术债清理:删除
src/llm/types/request.rs(187 行)、response.rs(177 行)、old_stream.rs(45 行),将其中的 OpenAI wire-format 类型移入src/llm/provider/openai.rs;删除types/mod.rs中的ChatResponse废弃结构体 ContextSlot::fork:从现有 context slot 分支出独立的子 slotContextSlot::merge:将子 slot 的消息合并回父 slotMergeStrategy枚举:Append(追加)/ Replace(替换),#[non_exhaustive]预留 Phase 16 Summarize 扩展
2.2 非功能需求
- 每步可编译:5 个 Step 按物理文件切割,每步
cargo build --all-targets + cargo test验证 - 指定公共 API 路径保持向后兼容:
agcore::llm::types::ToolChoice(re-export 不变)、crate::llm::stream::StreamEvent(重导出保留);其余 wire-format 类型(OpenaiChatRequest、OpenaiChatResponse/Chunk、StreamOptions等)移入provider/openai.rs后属 Breaking Change,详见 §4.3 CHANGELOG - 向后兼容的 StreamEvent 路径:
crate::llm::stream::StreamEvent重导出保留,不修改cycle.rs和session.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.1(ToolChoice 不搬走,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_slot(session.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::ToolChoice → tool::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 |
新增 StreamOptions、OpenaiTool、AudioParam、PredictionContent、UserLocation、Approximate、WebSearchOptions、OpenaiChatRequest 等类型定义 |
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 |
新增 TokenLogprob、TopLogprob、Logprobs、URLCitation、Annotation、OpenaiAudio、Choice、OpenaiChatResponse、Delta、ChunkChoice、OpenaiChatChunk + 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::OpenaiChatChunk(OpenaiToolCall 保持从 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_stream、parse_chunk_stream_legacy、ChunkToLegacyEventStream、LegacyToIrEventStream、map_legacy_to_ir、empty_message_response(~160 行死代码) |
stream.rs 最终形态:
//! 流式事件系统 —— 重导出 StreamEvent 供向后兼容。
pub use crate::llm::types::response_v2::StreamEvent;
为什么不全删 stream.rs:cycle.rs 和 session.rs 的 use crate::llm::stream::StreamEvent 路径保持不变。全删 + 改所有 import 路径的改动量 > 收益。保留 1 行重导出就够。
13.3b — 删除 ChatResponse
| 文件 | 变更 |
|---|---|
src/llm/types/mod.rs |
删除 ChatResponse 结构体定义 + 两个 #[allow(deprecated)] From impl(From<OpenaiChatResponse> for ChatResponse 和 From<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.rs、response.rs、old_stream.rs 三个旧文件不存在 |
ls src/llm/types/ 确认 |
| 2 | ChatResponse 结构体不存在 |
全局搜索 ChatResponse 仅保留 openai.rs 中 OpenaiChatResponse 引用 |
| 3 | ToolChoice 在 tool.rs 中定义,公共路径 agcore::llm::types::ToolChoice 保持不变 |
cargo doc --no-deps 确认类型文档 |
| 4 | OpenaiChatRequest/Response/Chunk 在 provider/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-merge(child.id == parent.id) | P0 |
参考来源
- Roadmap:
docs/roadmap.md§Phase 13 - ContextSlot 设计:
docs/17-phase10-contextslot.md - 旧 StreamEvent 设计:
src/llm/stream.rs文件注释 - 当前代码库:
src/llm/types/request.rs、src/llm/types/response.rs、src/llm/types/old_stream.rs、src/llm/types/mod.rs、src/llm/provider/openai.rs、src/agent/context.rs、src/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(-1,
ChatResponse的Fromimpl 无测试直接引用,删除后仅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_slotsrc/agent.rs— 追加MergeStrategyre-export
具体操作:
- 在
context.rs中新增MergeStrategy枚举(Append / Replace,#[non_exhaustive]) - 在
context.rs中impl ContextSlot块内新增fork(&self, child_id: String, strategy: DeriveStrategy) -> ContextSlot方法 - 在
context.rs中impl ContextSlot块内新增merge(&mut self, child: ContextSlot, strategy: MergeStrategy) -> Result<(), AgentError>方法(含 self-merge/cross-session/Readonly 三项防御检查 +tracing::debug!日志) - 在
session.rs的derive_slot方法中将手工消息复制代码替换为parent.fork(slot_id, strategy) - 在
agent.rs的pub use context::{...}列表中追加MergeStrategy - 在
context.rs的#[cfg(test)] mod tests中新增 9 个测试用例
注意:重构后 derive_slot 的子 slot budget 从 ContextBudget::default() 变为继承父 slot,compact 从 true 变为继承父 slot。由于 ContextBudget 在 v0.2 无消费逻辑且父 slot 的 compact 默认也为 true,此变化无实际影响。验收条件中"行为不变"指对外功能行为不变(slot 消息内容、血缘关系不变)。
预估工作量:M(1-4h)
风险等级:低(纯新增,不修改已有逻辑路径)
验收条件:
MergeStrategy枚举存在,Append和Replace两个变体可用,且通过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 先执行会将 ToolChoice 与 request.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 implsrc/llm/types/mod.rs— re-export 路径从request改为toolsrc/llm/types/request_v2.rs— import 路径从request::改为tool::
具体操作:
- 从
request.rs复制ToolChoice枚举 +Serialize/Deserializeimpl 到tool.rs - 从
request.rs中删除ToolChoice定义 - 在
mod.rs中将pub use request::{..., ToolChoice}改为pub use tool::ToolChoice - 在
request_v2.rs中将use crate::llm::types::request::ToolChoice改为use crate::llm::types::tool::ToolChoice - 验证
cycle.rs的use crate::llm::types::ToolChoice(通过 re-export)路径不变
预估工作量:S(<1h)
风险等级:低(有限的 import 路径变更,编译立即可发现)
验收条件:
ToolChoice在tool.rs中定义pub use tool::ToolChoice在mod.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.4(ToolChoice 已移走,request.rs 剩余内容全是 OpenAI wire-format 专有类型)
任务描述:删除 types/request.rs 整文件,将所有剩余类型(OpenaiChatRequest、StreamOptions、OpenaiTool、AudioParam、PredictionContent、UserLocation、Approximate、WebSearchOptions)搬入 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 声明
具体操作:
- 从
request.rs复制所有剩余类型定义到openai.rs,可见性设为pub(crate) OpenaiTool内引用OpenaiToolDefinition(定义在tool.rs),路径改为crate::llm::types::tool::OpenaiToolDefinition- 删除
openai.rs中原use crate::llm::types::request::{...}import - 从
mod.rs删除pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions}和pub mod request; - 删除
types/request.rs文件
预估工作量:M(1-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.rs 和 types/mod.rs,但本 Step 仅追加类型定义,无覆盖操作;建议在 13.1 之后顺序执行以避免并行时的合并冲突)
任务描述:删除 types/response.rs 整文件,将所有类型(OpenaiChatResponse、OpenaiChatChunk、Choice、Delta、ChunkChoice 等 + 两个 From impl)搬入 provider/openai.rs,更新 mod.rs 和 stream.rs 的 import 路径。
涉及文件:
src/llm/types/response.rs— 整文件删除src/llm/provider/openai.rs— 新增所有类型定义 + From implsrc/llm/types/mod.rs— 删除 re-export + mod 声明src/llm/stream.rs—OpenaiChatChunkimport 路径改为provider::openai
具体操作:
- 从
response.rs复制所有类型定义(含Fromimpl)到openai.rs,可见性设为pub(crate) - 删除
openai.rs中原use crate::llm::types::response::{...}import - 从
mod.rs删除pub use response::{...}和pub mod response; - 在
stream.rs:26将OpenaiChatChunk的 import 路径改为crate::llm::provider::openai::OpenaiChatChunk(OpenaiToolCall路径不变) - 删除
types/response.rs文件
预估工作量:M(1-4h)
风险等级:低(与 Step 13.1 模式完全相同)
验收条件:
response.rs文件不存在OpenaiChatResponse/Chunk等类型在openai.rs中定义,编译通过stream.rsimport 路径正确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.1(request.rs 已删)、Step 13.2(response.rs 已删)
任务描述:删除 old_stream.rs 和 ChatResponse,简化 stream.rs 为仅保留 pub use 重导出。这是 Phase 13 技术风险最高的 Step。
涉及文件:
src/llm/types/old_stream.rs— 整文件删除src/llm/types/mod.rs— 删除pub mod old_stream;+ 删除ChatResponse结构体和两个Fromimplsrc/llm/stream.rs— 删除死代码(约 160 行),仅保留pub use重导出
具体操作:
- 前置验证 A:执行
grep -rn 'parse_chunk_stream\|map_legacy_to_ir\|LegacyToIrEventStream\|ChunkToLegacyEventStream' src/确认零外部调用方,记录结果到 commit message - 前置验证 B:执行
cargo doc --no-deps 2>&1 | grep -i 'ChatResponse'确认零文档引用,记录结果 - 从
mod.rs删除pub mod old_stream; - 从
mod.rs删除ChatResponse结构体定义 +#[allow(deprecated)]From<OpenaiChatResponse> for ChatResponse+From<ChatResponse> for OpenaiChatChunk - 删除
old_stream.rs文件 - 从
stream.rs删除:use crate::llm::types::old_stream::LegacyStreamEvent、parse_chunk_stream、parse_chunk_stream_legacy、ChunkToLegacyEventStream、LegacyToIrEventStream、map_legacy_to_ir、empty_message_response stream.rs最终只保留 module doc comment +pub use crate::llm::types::response_v2::StreamEvent;- 检查
cycle.rs:88的#[allow(deprecated)]属性是否仍与ChatResponse相关——若不相关则无需改动;若因ChatResponse删除而变脏,清理该属性
预估工作量:S(<1h,cleanup)+ 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 warnings0 警告cargo doc --no-deps无 warning
回退方式:git revert 该 commit(单独 revert 即可恢复 ChatResponse + old_stream.rs)