//! LLM 调用周期控制模块。 mod retry; pub mod usage; pub use retry::RetryConfig; pub use usage::{CostTracker, Usage}; use std::pin::Pin; use std::sync::Arc; use async_stream::stream; use futures_core::stream::Stream; use serde_json::Value; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::llm::compact::{CompactConfig, CompactState, microcompact, should_compact}; use crate::llm::cycle::retry::should_retry; use crate::llm::error::LlmError; use crate::llm::hooks::{HookContext, HookEvent, HookExecutor}; use crate::llm::provider::LlmProvider; use crate::llm::stream::StreamEvent; use crate::llm::types::message::{ContentBlock, Message}; use crate::llm::types::request_v2::MessageRequest; use crate::llm::types::response_v2::{MessageResponse, PartialMessageResponse, StopReason}; use crate::llm::types::tool::ToolDef; use crate::llm::types::ToolChoice; use crate::tools::ToolRegistry; /// LLM 调用周期配置。 #[derive(Debug, Clone)] pub struct CycleConfig { /// 模型名称。 pub model: String, /// 最大输出 token 数。 pub max_tokens: Option, /// 采样温度。 pub temperature: Option, /// 最大对话轮次。 pub max_turns: Option, /// 重试策略配置。 pub retry: RetryConfig, /// 自动 tool 循环的最大轮次(独立于 `max_turns`,避免影响现有 `submit()` 语义)。 /// 默认 `Some(10)`,防止 LLM 反复调用工具导致无限循环。 pub max_tool_turns: Option, /// 单个工具执行的超时秒数(0 表示不超时)。 /// 默认 60 秒。 pub tool_timeout_secs: u64, /// 单个工具结果的最大字节数(超过此值将被截断)。 /// 默认 65536(64KB),防止大结果导致 token 膨胀。 pub max_tool_result_bytes: usize, } impl Default for CycleConfig { fn default() -> Self { Self { model: String::from("gpt-4o"), max_tokens: None, temperature: None, max_turns: None, retry: RetryConfig::default(), max_tool_turns: Some(10), tool_timeout_secs: 60, max_tool_result_bytes: 65_536, } } } /// LLM 调用周期 —— 管理一次或多次 LLM 请求的生命周期。 /// /// Phase 2 修订: /// - `messages` 字段从 `Vec` 切换为 `Vec`(IR 类型)。 /// - 移除 `system_prompt` 字段(FIX-D)—— 调用方通过 `Message::system_text()` + /// `with_messages()` 自行管理系统提示消息,避免重复维护两条路径。 /// - `with_system_prompt()` 方法标记 `#[deprecated]`,过渡期内仍可用。 pub struct LlmCycle { provider: Arc, config: CycleConfig, usage: CostTracker, /// 消息历史 —— 直接存储 IR `Message` 类型,build_request 不再做转换。 messages: Vec, hook_executor: Option>, compact_config: Option, compact_state: CompactState, } #[allow(deprecated)] impl LlmCycle { /// 创建一个新的 LlmCycle(持有 `Box` 的独占所有权)。 /// /// 内部将 Box 转为 `Arc` 以便 `new_with_arc` 复用句柄。 /// 公共签名保持不变,向后兼容。 pub fn new(provider: Box, config: CycleConfig) -> Self { Self::new_with_arc(Arc::from(provider), config) } /// 创建一个新的 LlmCycle,共享传入的 `Arc` 句柄。 /// /// **新增**(Phase 4a 引入):用于 `AgentSession::submit_turn` 在多 session 间共享 provider。 pub fn new_with_arc(provider: Arc, config: CycleConfig) -> Self { Self { provider, config, usage: CostTracker::default(), messages: Vec::new(), hook_executor: None, compact_config: None, compact_state: CompactState::new(), } } /// 设置系统提示词(**已废弃** —— Phase 2 起使用 `Message::system_text()` + `with_messages()`)。 /// /// 当前实现为过渡期保留:在 messages 头部插入 `Message::System { content: [Text { text }] }`。 #[deprecated( since = "0.2.0", note = "请改用 Message::system_text() + with_messages()" )] pub fn with_system_prompt(mut self, prompt: String) -> Self { self.messages.insert( 0, Message::System { content: vec![ContentBlock::Text { text: prompt }], }, ); self } /// 设置钩子执行器。 pub fn with_hook_executor(mut self, executor: HookExecutor) -> Self { self.hook_executor = Some(Arc::new(executor)); self } /// 设置上下文压缩配置。 pub fn with_compact_config(mut self, config: CompactConfig) -> Self { self.compact_config = Some(config); self } /// 获取用量追踪器引用。 pub fn usage(&self) -> &CostTracker { &self.usage } /// 获取消息历史引用(Phase 2:返回 `&[Message]` IR 类型)。 pub fn messages(&self) -> &[Message] { &self.messages } /// 清空消息历史。 pub fn clear_messages(&mut self) { self.messages.clear(); } /// 重置用量统计。 pub fn reset_usage(&mut self) { self.usage.reset(); } /// 直接设置消息历史(覆盖已有消息),支持 Builder 链式调用。 pub fn with_messages(mut self, messages: Vec) -> Self { self.messages = messages; self } /// 追加消息到历史尾部。 pub fn extend_messages(&mut self, messages: Vec) { self.messages.extend(messages); } /// 追加单条消息到历史尾部。 /// /// 公开给 `submit_stream()` 消费方在收到 `MessageComplete` 事件后调用。 pub fn push_message(&mut self, msg: Message) { self.messages.push(msg); } /// 使用预构建消息提交(跳过自动 push user prompt)。 /// /// 与 `submit()` 不同,不自动添加 `user_text(prompt)`。 /// 调用方完全控制消息序列内容。 pub async fn submit_messages( &mut self, messages: Vec, tools: Vec, ) -> Result { let request = MessageRequest { model: self.config.model.clone(), messages, tools, tool_choice: ToolChoice::Auto, max_tokens: self.config.max_tokens, temperature: self.config.temperature, ..Default::default() }; if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::PreRequest).with_request(&request); let results = executor .execute(crate::llm::hooks::HookEvent::PreRequest, &ctx) .await; if results.iter().any(|r| r.should_block) { let reason = results .iter() .find(|r| r.should_block) .and_then(|r| r.reason.clone()) .unwrap_or_else(|| "Blocked by pre-request hook".to_string()); return Err(LlmError::Other(reason)); } } match self.provider.chat(request).await { Ok(response) => { if let Some(ref executor) = self.hook_executor { let post_request = MessageRequest::default(); let ctx = HookContext::new(crate::llm::hooks::HookEvent::PostRequest) .with_request(&post_request); executor .execute(crate::llm::hooks::HookEvent::PostRequest, &ctx) .await; } self.usage.add(&response.usage); Ok(response) } Err(e) => { if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::OnError).with_error(&e); executor .execute(crate::llm::hooks::HookEvent::OnError, &ctx) .await; } Err(e) } } } /// 提交用户消息并获取 LLM 响应。 pub async fn submit( &mut self, prompt: String, tools: Vec, ) -> Result { self.messages.push(Message::user_text(prompt)); if let Some(ref config) = self.compact_config && should_compact(&self.messages, config, &self.compact_state) { let freed = microcompact(&mut self.messages, config.keep_recent); if freed > 0 { self.compact_state.record_success(); } } let mut attempts = 0; loop { let request = self.build_request(&tools); if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::PreRequest) .with_request(&request); let results = executor .execute(crate::llm::hooks::HookEvent::PreRequest, &ctx) .await; if results.iter().any(|r| r.should_block) { let reason = results .iter() .find(|r| r.should_block) .and_then(|r| r.reason.clone()) .unwrap_or_else(|| "Blocked by pre-request hook".to_string()); return Err(LlmError::Other(reason)); } } match self.provider.chat(request).await { Ok(response) => { if let Some(ref executor) = self.hook_executor { let post_request = self.build_request(&tools); let ctx = HookContext::new(crate::llm::hooks::HookEvent::PostRequest) .with_request(&post_request); executor .execute(crate::llm::hooks::HookEvent::PostRequest, &ctx) .await; } // ponytail: Phase 2 直接存储 IR Message —— 不再转换。 self.messages.push(response.message.clone()); self.usage.add(&response.usage); return Ok(response); } Err(e) if should_retry(&e) && attempts < self.config.retry.max_retries => { attempts += 1; if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::OnRetry) .with_error(&e) .with_attempt(attempts); executor .execute(crate::llm::hooks::HookEvent::OnRetry, &ctx) .await; } let delay = self.config.retry.compute_delay(attempts); tokio::time::sleep(delay).await; } Err(e) => { if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::OnError).with_error(&e); executor .execute(crate::llm::hooks::HookEvent::OnError, &ctx) .await; } return Err(e); } } } } /// 提交用户消息并返回语义事件流(Phase 2 / FIX-E 简化方案)。 /// /// 与 `submit` 不同,该方法返回流式事件而非完整响应。 /// 适用于需要实时处理 LLM 输出的场景。 /// /// **Phase 2 设计决策(FIX-E)**: /// - `Item = StreamEvent`(**不再是 `Result`**) /// - 错误统一以 `StreamEvent::Error { message }` 形式在流中传出 /// - `self.messages` 不会在流结束后自动 push Assistant 响应 —— /// **调用方** 在收到 `StreamEvent::MessageComplete` 后手动调用 /// `cycle.push_message(response.message.clone())` 完成消息历史追加 /// /// 调用模式(FIX-E 推荐): /// ```ignore /// use futures_util::StreamExt; /// let mut stream = cycle.submit_stream(prompt, tools).await?; /// let mut final_response: Option = None; /// while let Some(event) = stream.next().await { /// if let StreamEvent::MessageComplete { full_response } = &event { /// final_response = Some(full_response.clone()); /// } /// } /// if let Some(resp) = final_response { /// cycle.push_message(resp.message.clone()); /// } /// ``` pub async fn submit_stream( &mut self, prompt: String, tools: Vec, ) -> Result + Send>>, LlmError> { self.messages.push(Message::user_text(prompt)); if let Some(ref config) = self.compact_config && should_compact(&self.messages, config, &self.compact_state) { let freed = microcompact(&mut self.messages, config.keep_recent); if freed > 0 { self.compact_state.record_success(); } } let request = self.build_request(&tools); // PreRequest hook if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::PreRequest).with_request(&request); let results = executor .execute(crate::llm::hooks::HookEvent::PreRequest, &ctx) .await; if results.iter().any(|r| r.should_block) { let reason = results .iter() .find(|r| r.should_block) .and_then(|r| r.reason.clone()) .unwrap_or_else(|| "Blocked by pre-request hook".to_string()); return Err(LlmError::Other(reason)); } } let ir_event_stream = self.provider.chat_stream(request).await?; let hook_executor = self.hook_executor.clone(); // ponytail: Phase 2 简化方案(FIX-E)。流是延迟求值的,&mut self 无法进入闭包。 // 调用方在收到 MessageComplete 后手动调 push_message()。 Ok(Box::pin(stream! { use futures_util::StreamExt; let mut ir_event_stream = ir_event_stream; while let Some(result) = ir_event_stream.next().await { match result { Ok(event) => { let is_terminal = matches!(event, StreamEvent::MessageComplete { .. } | StreamEvent::Error { .. }); yield event; if is_terminal { break; } } Err(e) => { // ponytail: 错误不再走 Err 分支,统一以 StreamEvent::Error 传出。 if let Some(ref executor) = hook_executor { let ctx = crate::llm::hooks::HookContext::new( crate::llm::hooks::HookEvent::OnError, ) .with_error(&e); executor .execute(crate::llm::hooks::HookEvent::OnError, &ctx) .await; } yield StreamEvent::Error { message: e.to_string() }; break; } } } // ponytail: post_request hook 收到的消息列表**不包含本次 Assistant 响应** // (因为流是延迟求值,本轮响应还未到达)。调用方如需完整上下文, // 应在收到 MessageComplete 后手动触发 hook。 if let Some(ref executor) = hook_executor { let ctx = crate::llm::hooks::HookContext::new( crate::llm::hooks::HookEvent::PostRequest, ); executor .execute(crate::llm::hooks::HookEvent::PostRequest, &ctx) .await; } })) } fn build_request(&self, tools: &[ToolDef]) -> MessageRequest { // ponytail: Phase 2 简化 —— 直接 clone self.messages,无任何转换 / system prompt 注入。 // 系统消息如需存在,由调用方通过 `with_messages()` 自行管理。 MessageRequest { model: self.config.model.clone(), messages: self.messages.clone(), tools: tools.to_vec(), tool_choice: ToolChoice::Auto, max_tokens: self.config.max_tokens, temperature: self.config.temperature, ..Default::default() } } /// 内部请求方法(与 `submit` 共享重试逻辑,但不 push user message 和 Assistant 响应)。 /// /// 用于 `submit_with_tools()` 的多轮 tool 循环。 async fn submit_request( &mut self, tools: &[ToolDef], ) -> Result { let mut attempts = 0; loop { let request = self.build_request(tools); if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::PreRequest) .with_request(&request); let results = executor .execute(crate::llm::hooks::HookEvent::PreRequest, &ctx) .await; if results.iter().any(|r| r.should_block) { let reason = results .iter() .find(|r| r.should_block) .and_then(|r| r.reason.clone()) .unwrap_or_else(|| "Blocked by pre-request hook".to_string()); return Err(LlmError::Other(reason)); } } match self.provider.chat(request).await { Ok(response) => { if let Some(ref executor) = self.hook_executor { let post_request = self.build_request(tools); let ctx = HookContext::new(crate::llm::hooks::HookEvent::PostRequest) .with_request(&post_request); executor .execute(crate::llm::hooks::HookEvent::PostRequest, &ctx) .await; } self.usage.add(&response.usage); return Ok(response); } Err(e) if should_retry(&e) && attempts < self.config.retry.max_retries => { attempts += 1; if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::OnRetry) .with_error(&e) .with_attempt(attempts); executor .execute(crate::llm::hooks::HookEvent::OnRetry, &ctx) .await; } let delay = self.config.retry.compute_delay(attempts); tokio::time::sleep(delay).await; } Err(e) => { if let Some(ref executor) = self.hook_executor { let ctx = HookContext::new(crate::llm::hooks::HookEvent::OnError).with_error(&e); executor .execute(crate::llm::hooks::HookEvent::OnError, &ctx) .await; } return Err(e); } } } } /// 提交消息并自动处理工具调用循环。 /// /// 流程: /// 1. 发送请求(含工具定义) /// 2. 检查响应中的 finish_reason /// 3. 如果是 ToolCalls → push Assistant 消息 → 执行工具 → 回传结果 → 重复 1 /// 4. 如果是 Stop/Length → push Assistant 消息 → 返回最终响应 /// /// 注意:OpenAI API 要求 tool 消息必须紧跟在对应的 Assistant(tool_calls)消息之后。 /// 因此 push 工具结果前必须先 push Assistant 响应,否则 API 拒绝请求。 pub async fn submit_with_tools( &mut self, prompt: String, registry: &crate::tools::ToolRegistry, ) -> Result { let tools = registry.definitions(); let max_turns = self.config.max_tool_turns.unwrap_or(10); let tool_timeout = self.config.tool_timeout_secs; let max_bytes = self.config.max_tool_result_bytes; self.messages.push(Message::user_text(prompt)); self.maybe_compact(); let mut turn = 0; loop { turn += 1; if turn > max_turns { return Err(LlmError::Other(format!( "达到最大工具循环轮次 ({max_turns})" ))); } let response = self.submit_request(&tools).await?; // 判断是否需要执行工具 let should_execute = matches!(response.stop_reason, StopReason::ToolUse) && has_tool_calls_in_response(&response); // ponytail: Phase 2 直接存储 IR Message —— 不再转换。 self.messages.push(response.message.clone()); if !should_execute { return Ok(response); } // 解析 tool_calls 并执行 let tool_calls = extract_tool_calls_from_response(&response); let calls: Vec<(String, String, serde_json::Value)> = tool_calls .into_iter() .map(|(id, name, args)| { let value: serde_json::Value = serde_json::from_str(&args).unwrap_or(serde_json::Value::Null); (id, name, value) }) .collect(); let results = registry.invoke_all(calls, tool_timeout).await; // 回传工具结果 for result in results { let is_error = result.output.is_err(); let content = match &result.output { Ok(value) => { let serialized = serde_json::to_string(value).unwrap_or_else(|e| { tracing::warn!("工具结果序列化失败: {}", e); "{}".to_string() }); truncate_tool_result(&serialized, max_bytes) } Err(e) if e.is_recoverable() => format!("错误: {}", e), Err(e) => { // 不可恢复错误:终止循环 return Err(LlmError::Other(format!( "工具 '{}' 不可恢复错误: {}", result.tool_name, e ))); } }; // ponytail: 当前 self.messages 仍是 Vec (Phase 2 切换后 // 改为 Message::tool_result 并传入 result.tool_call_id)。当前实现已经使用 // 真实 tool_call_id 而非 tool_name 充当 —— 这条 FIX-A 修复与 Phase 2 消息切换 // 同步生效。 // ponytail: Phase 2 直接存储 Message::ToolResult,is_error 由 ToolInvocation.output 推断。 self.messages .push(Message::tool_result(result.tool_call_id, content, is_error)); } // 每轮工具执行后触发 compaction self.maybe_compact(); } // unreachable: loop returns #[allow(unreachable_code)] { Err(LlmError::Other("unreachable".into())) } } /// 提交消息并自动处理工具调用循环,流式产出所有事件。 /// /// 与 `submit_with_tools` 的区别: /// - LLM 响应是流式的(全程 `chat_stream` 而非 `chat`) /// - 工具执行前后插入 `ToolExecutionStarted` / `ToolExecutionCompleted` 事件 /// - 错误以 `StreamEvent::Error` 形式出现在流中,而非终止 `Result` /// - 消费方需手动 `push_message()` 同步消息历史 /// /// **运行时要求**:内部使用 `tokio::spawn`,需要 tokio 多线程运行时。 /// `#[tokio::test]` 单线程运行时不支持 spawn,测试场景需用 `flavor = "multi_thread"` 或 /// 直接调用模块函数 `run_tool_loop`。 /// /// ponytail: 返回的流是 `Item = StreamEvent`(非 `Result`),所有错误事件化为 `StreamEvent::Error`。 pub async fn submit_with_tools_stream( &mut self, prompt: String, tool_registry: Arc, ) -> Result + Send>>, LlmError> { self.messages.push(Message::user_text(prompt)); self.maybe_compact(); // 提取 self 字段所有 owned 数据 —— spawn 闭包不能捕获 &mut self。 let provider = Arc::clone(&self.provider); let config = self.config.clone(); let hook_executor = self.hook_executor.clone(); let tools = tool_registry.definitions(); let messages = std::mem::take(&mut self.messages); let tool_registry = tool_registry; let (tx, rx) = mpsc::unbounded_channel::(); tokio::spawn(async move { run_tool_loop( messages, provider, config, tool_registry, tools, tx, hook_executor, ) .await; }); Ok(Box::pin(UnboundedReceiverStream::new(rx))) } /// 在接近上下文窗口时压缩历史消息。 fn maybe_compact(&mut self) { if let Some(ref config) = self.compact_config && should_compact(&self.messages, config, &self.compact_state) { let freed = microcompact(&mut self.messages, config.keep_recent); if freed > 0 { self.compact_state.record_success(); } } } } /// 判断 Assistant 消息是否包含 tool_calls。 fn has_tool_calls_in_response(response: &MessageResponse) -> bool { match &response.message { Message::Assistant { content } => content .iter() .any(|b| matches!(b, ContentBlock::ToolUse { .. })), _ => false, } } /// 提取 Assistant 消息中的 tool_calls。 /// /// 返回 `(tool_call_id, tool_name, arguments_json_string)` 列表。 /// /// ponytail: 当前 Phase 0 实现,`arguments_json_string` 内含 JSON 序列化的 input。 /// 消费方在调用 `registry.invoke_all()` 时反序列化一次。该小段冗余序列化 /// 在 Phase 2 切换为 `Vec` 后可整体消除。 fn extract_tool_calls_from_response(response: &MessageResponse) -> Vec<(String, String, String)> { let mut out = Vec::new(); if let Message::Assistant { content } = &response.message { for block in content { if let ContentBlock::ToolUse { id, name, input } = block { let args = serde_json::to_string(input).unwrap_or_else(|_| "null".to_string()); out.push((id.clone(), name.clone(), args)); } } } out } /// 截断工具结果到指定字节数。 fn truncate_tool_result(s: &str, max_bytes: usize) -> String { if s.len() <= max_bytes { return s.to_string(); } let mut end = max_bytes; while end > 0 && !s.is_char_boundary(end) { end -= 1; } format!( "{}\n\n[... truncated, original size: {} bytes ...]", &s[..end], s.len() ) } /// 运行流式工具循环的核心异步状态机(Phase 9)。 /// /// 由 `submit_with_tools_stream` 在 `tokio::spawn` 中调用。 /// /// 流程: /// - loop: 构建请求 → `chat_stream` → 消费流(转 mpsc)→ finalize → 检测 tool_use /// - 如果有 tool_use:插入 `ToolExecutionStarted` → 调 `invoke_all` → 插入 `ToolExecutionCompleted` /// → push 工具结果 → 下一轮 /// - 否则 break(最终轮) /// - 最大轮次超限:发 `StreamEvent::Error` + break /// /// **所有错误事件化**:通过 `tx.send(Error{..})` 表达错误,最终 `return` 结束 task。 /// 不返回 `Result`,因为错误已通过事件流传递。 async fn run_tool_loop( mut messages: Vec, provider: Arc, config: CycleConfig, tool_registry: Arc, tools: Vec, tx: mpsc::UnboundedSender, hook_executor: Option>, ) { use futures_util::StreamExt; let max_turns = config.max_tool_turns.unwrap_or(10); let tool_timeout = config.tool_timeout_secs; let max_bytes = config.max_tool_result_bytes; let mut round = 0u32; loop { round += 1; if round > max_turns { // §3.6 错误表:最大轮次超限 → Error 事件 + 终止 let _ = tx.send(StreamEvent::Error { message: "达到最大工具循环轮次".to_string(), }); break; } // ① 构建请求 let request = MessageRequest { model: config.model.clone(), messages: messages.clone(), tools: tools.clone(), tool_choice: ToolChoice::Auto, max_tokens: config.max_tokens, temperature: config.temperature, ..Default::default() }; // ② PreRequest hook —— 与 `submit_with_tools` / `submit_stream` 行为对齐: // 触发 hook → 检查 should_block → 阻断则事件化 Error + return 结束 task。 // 阻断原因透传,让消费者看到完整的拒绝原因。 if let Some(ref executor) = hook_executor { let ctx = HookContext::new(HookEvent::PreRequest).with_request(&request); let results = executor.execute(HookEvent::PreRequest, &ctx).await; if let Some(blocking) = results.iter().find(|r| r.should_block) { let reason = blocking .reason .clone() .unwrap_or_else(|| "Blocked by pre-request hook".to_string()); let _ = tx.send(StreamEvent::Error { message: reason }); return; } } // ③ chat_stream —— 第一层错误 let mut stream = match provider.chat_stream(request).await { Ok(s) => s, Err(e) => { // ponytail: 不做 retry —— retry 重建 mpsc 通道复杂度与收益不匹配 let _ = tx.send(StreamEvent::Error { message: e.to_string(), }); return; } }; // ④ 消费 LLM 流 let mut partial = PartialMessageResponse::new(); loop { match stream.next().await { Some(Ok(event)) => { partial.apply_to(&event); let is_terminal = matches!( event, StreamEvent::MessageComplete { .. } | StreamEvent::Error { .. } ); if tx.send(event).is_err() { return; // 消费者已 drop rx,task 终止 } if is_terminal { break; } } Some(Err(e)) => { // ponytail: 流内 Err 后 partial 处于损坏状态,直接 return 结束 task let _ = tx.send(StreamEvent::Error { message: e.to_string(), }); return; } None => break, // 流自然结束 } } // ⑤ finalize // ponytail: 防御性检查 —— 若 Provider 在流中产出过 Ok(StreamEvent::Error), // partial 已设为 is_errored=true,finalize() 会基于损坏状态生成不可信响应, // 此时跳过 finalize 直接退出流,让消费者看到 Error 事件后的流自然结束。 if partial.is_errored { return; } let response = match partial.finalize() { Ok(r) => r, Err(e) => { let _ = tx.send(StreamEvent::Error { message: e.to_string(), }); return; } }; messages.push(response.message.clone()); // ⑥ 检测 tool_use —— 没有则 break(最终轮) if !has_tool_calls_in_response(&response) { break; } // ⑦ 提取 tool_calls let tool_calls = extract_tool_calls_from_response(&response); let calls: Vec<(String, String, Value)> = tool_calls .into_iter() .map(|(id, name, args)| { let value: Value = serde_json::from_str(&args).unwrap_or(Value::Null); (id, name, value) }) .collect(); // ⑧ 发送 ToolExecutionStarted for (tool_call_id, tool_name, args_value) in &calls { let args_json = serde_json::to_string(args_value).unwrap_or_default(); if tx .send(StreamEvent::ToolExecutionStarted { tool_name: tool_name.clone(), tool_call_id: tool_call_id.clone(), arguments: args_json, }) .is_err() { return; } } // ⑨ 执行工具 let results = tool_registry.invoke_all(calls, tool_timeout).await; // ⑩ 发送 ToolExecutionCompleted for result in &results { let summary = match &result.output { Ok(v) => serde_json::to_string(v).unwrap_or_default(), Err(e) => e.to_string(), }; let truncated = truncate_tool_result(&summary, max_bytes); if tx .send(StreamEvent::ToolExecutionCompleted { tool_name: result.tool_name.clone(), tool_call_id: result.tool_call_id.clone(), result_summary: truncated, is_error: result.output.is_err(), }) .is_err() { return; } } // ⑪ push 工具结果到 messages(区分可恢复/不可恢复) for result in results { let is_error = result.output.is_err(); let content = match &result.output { Ok(v) => { // ponytail: 与 submit_with_tools 行为对齐 —— 用 truncate_tool_result // 截断结果以防止超大工具输出在 tool 循环中膨胀 messages 上下文窗口 let serialized = serde_json::to_string(v).unwrap_or_else(|e| { tracing::warn!("工具结果序列化失败: {}", e); "{}".to_string() }); truncate_tool_result(&serialized, max_bytes) } Err(e) if e.is_recoverable() => format!("错误: {}", e), Err(e) => { // 不可恢复错误 —— 终止循环 let _ = tx.send(StreamEvent::Error { message: format!("工具 '{}' 不可恢复错误: {}", result.tool_name, e), }); return; } }; messages.push(Message::tool_result(result.tool_call_id, content, is_error)); } } } #[cfg(test)] mod tests { use super::*; use crate::llm::provider::{ProviderCapabilities, ProviderFeatures}; use crate::tools::{BaseTool, ToolRegistry}; use async_trait::async_trait; use futures_core::Stream; use serde_json::{Value, json}; use std::pin::Pin; /// 模拟 Provider —— 预定义响应序列,按调用顺序返回。 struct MockProvider { responses: std::sync::Mutex>, call_count: std::sync::Mutex, } impl MockProvider { fn new(responses: Vec) -> Self { Self { responses: std::sync::Mutex::new(responses), call_count: std::sync::Mutex::new(0), } } } #[async_trait] impl LlmProvider for MockProvider { async fn chat(&self, _request: MessageRequest) -> Result { let mut count = self.call_count.lock().unwrap(); *count += 1; let mut responses = self.responses.lock().unwrap(); if responses.is_empty() { return Err(LlmError::Other("no more mock responses".into())); } Ok(responses.remove(0)) } async fn chat_stream( &self, _request: MessageRequest, ) -> Result> + Send>>, LlmError> { unimplemented!() } fn capabilities(&self) -> ProviderCapabilities { ProviderCapabilities { provider_name: "mock", supported_models: None, features: ProviderFeatures::default(), } } } fn empty_usage() -> crate::llm::types::Usage { crate::llm::types::Usage::default() } fn assistant_text_response(text: &str) -> MessageResponse { MessageResponse { id: String::new(), model: String::new(), message: Message::Assistant { content: vec![ContentBlock::Text { text: text.into() }], }, usage: empty_usage(), stop_reason: StopReason::Stop, extra: std::collections::HashMap::new(), } } fn assistant_tool_call_response(calls: Vec<(&str, &str, &str)>) -> MessageResponse { let tool_blocks: Vec = calls .into_iter() .map(|(id, name, args)| { let input: serde_json::Value = serde_json::from_str(args).unwrap_or(serde_json::Value::Null); ContentBlock::ToolUse { id: id.to_string(), name: name.to_string(), input, } }) .collect(); MessageResponse { id: String::new(), model: String::new(), message: Message::Assistant { content: tool_blocks, }, usage: empty_usage(), stop_reason: StopReason::ToolUse, extra: std::collections::HashMap::new(), } } struct AddTool; #[async_trait] impl BaseTool for AddTool { fn name(&self) -> &str { "add" } fn description(&self) -> &str { "加法" } fn parameters(&self) -> Value { json!({"type":"object","properties":{"a":{"type":"integer"},"b":{"type":"integer"}}}) } async fn execute( &self, args: Value, _ctx: &crate::tools::ToolContext<'_>, ) -> Result { let a = args["a"].as_i64().unwrap_or(0); let b = args["b"].as_i64().unwrap_or(0); Ok(json!({"result": a + b})) } } #[tokio::test] async fn test_submit_with_tools_single_turn() { let responses = vec![ assistant_tool_call_response(vec![("call_1", "add", r#"{"a":1,"b":2}"#)]), assistant_text_response("答案是 3"), ]; let provider = Box::new(MockProvider::new(responses)); let mut cycle = LlmCycle::new(provider, CycleConfig::default()); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let response = cycle .submit_with_tools("1+2=?".to_string(), ®istry) .await .unwrap(); // 最终响应是 Assistant 消息 assert!(matches!(response.message, Message::Assistant { .. })); // 验证消息历史: // user, assistant(含 tool_use), tool_result, assistant(text) let messages = cycle.messages(); assert_eq!(messages.len(), 4); assert!(matches!(messages[0], Message::User { .. })); assert!(matches!(messages[1], Message::Assistant { content: _ })); if let Message::Assistant { content } = &messages[1] { assert!( content .iter() .any(|b| matches!(b, ContentBlock::ToolUse { .. })) ); } assert!(matches!( messages[2], Message::ToolResult { is_error: false, .. } )); assert!(matches!(messages[3], Message::Assistant { .. })); } #[tokio::test] async fn test_submit_with_tools_multi_turn() { let responses = vec![ assistant_tool_call_response(vec![("call_1", "add", r#"{"a":1,"b":2}"#)]), assistant_tool_call_response(vec![("call_2", "add", r#"{"a":3,"b":4}"#)]), assistant_tool_call_response(vec![("call_3", "add", r#"{"a":5,"b":6}"#)]), assistant_text_response("完成"), ]; let provider = Box::new(MockProvider::new(responses)); let mut cycle = LlmCycle::new(provider, CycleConfig::default()); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let response = cycle .submit_with_tools("计算总和".to_string(), ®istry) .await .unwrap(); assert!(matches!(response.message, Message::Assistant { .. })); // user + 3*(assistant + tool) + final assistant = 8 let messages = cycle.messages(); assert_eq!(messages.len(), 8); } #[tokio::test] async fn test_submit_with_tools_max_turns_exceeded() { let config = CycleConfig { max_tool_turns: Some(2), ..Default::default() }; let responses = vec![ assistant_tool_call_response(vec![("c1", "add", r#"{"a":1,"b":1}"#)]), assistant_tool_call_response(vec![("c2", "add", r#"{"a":1,"b":1}"#)]), assistant_tool_call_response(vec![("c3", "add", r#"{"a":1,"b":1}"#)]), assistant_text_response("完成"), ]; let provider = Box::new(MockProvider::new(responses)); let mut cycle = LlmCycle::new(provider, config); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let result = cycle.submit_with_tools("test".to_string(), ®istry).await; assert!( matches!(result, Err(LlmError::Other(msg)) if msg.contains("达到最大工具循环轮次")) ); } #[tokio::test] async fn test_submit_with_tools_no_tool_call_response() { let responses = vec![assistant_text_response("直接回答")]; let provider = Box::new(MockProvider::new(responses)); let mut cycle = LlmCycle::new(provider, CycleConfig::default()); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let response = cycle .submit_with_tools("直接回答".to_string(), ®istry) .await .unwrap(); assert!(matches!(response.message, Message::Assistant { .. })); } #[test] fn test_truncate_tool_result_short() { let s = "short text"; assert_eq!(truncate_tool_result(s, 100), "short text"); } #[test] fn test_truncate_tool_result_long() { let s = "a".repeat(1000); let truncated = truncate_tool_result(&s, 50); assert!(truncated.len() < s.len()); assert!(truncated.contains("[... truncated,")); } #[test] fn test_truncate_tool_result_chinese_chars() { let s = "中".repeat(100); let truncated = truncate_tool_result(&s, 50); assert!(truncated.starts_with("中")); } // ====== Phase 9: submit_with_tools_stream 单元测试 ====== // // ponytail: 由于 in-mod MockProvider 的 chat_stream 是 unimplemented!(), // 这里使用公开的 `crate::llm::mock::MockProvider`(已实现完整 chat_stream + 预设队列)。 // 测试需使用 `tokio::test(flavor = "multi_thread")` 满足 submit_with_tools_stream 内部 // 的 `tokio::spawn` 运行时要求。 use crate::llm::mock::MockProvider as Mock; use futures_util::StreamExt; /// 收集流中所有事件到 Vec。 async fn drain(stream: Pin + Send>>) -> Vec { let mut out = Vec::new(); let mut s = stream; while let Some(ev) = s.next().await { out.push(ev); } out } /// Phase 9 测试 3.1 — 纯文本流(无 tool_use) #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_pure_text() { let provider = Mock::new(vec![assistant_text_response("你好")]); let mut cycle = LlmCycle::new(Box::new(provider), CycleConfig::default()); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let stream = cycle .submit_with_tools_stream("问个问题".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 期望序列:MessageStart → ContentBlockStart(Text) → TextDelta → ContentBlockEnd → CostUpdate → MessageComplete assert!(matches!(events.first(), Some(StreamEvent::MessageStart { .. }))); assert!(events .iter() .any(|e| matches!(e, StreamEvent::TextDelta { text } if text == "你好"))); assert!(events .iter() .any(|e| matches!(e, StreamEvent::MessageComplete { .. }))); // 纯文本流不应有 ToolExecutionStarted/Completed 事件 assert!(!events .iter() .any(|e| matches!(e, StreamEvent::ToolExecutionStarted { .. }))); assert!(!events .iter() .any(|e| matches!(e, StreamEvent::ToolExecutionCompleted { .. }))); assert!(!events .iter() .any(|e| matches!(e, StreamEvent::Error { .. }))); } /// Phase 9 测试 3.2 — 单轮工具调用 #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_single_tool() { let provider = Mock::new(vec![ assistant_tool_call_response(vec![("call_1", "add", r#"{"a":1,"b":2}"#)]), assistant_text_response("答案是 3"), ]); let mut cycle = LlmCycle::new(Box::new(provider), CycleConfig::default()); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let stream = cycle .submit_with_tools_stream("1+2".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 应有 1 对 ToolExecutionStarted / ToolExecutionCompleted let started_count = events .iter() .filter(|e| matches!(e, StreamEvent::ToolExecutionStarted { .. })) .count(); let completed_count = events .iter() .filter(|e| matches!(e, StreamEvent::ToolExecutionCompleted { .. })) .count(); assert_eq!(started_count, 1, "应有 1 个 ToolExecutionStarted"); assert_eq!(completed_count, 1, "应有 1 个 ToolExecutionCompleted"); // 验证 ToolExecutionStarted.arguments 携带有效 JSON let started = events .iter() .find_map(|e| match e { StreamEvent::ToolExecutionStarted { tool_name, tool_call_id, arguments, } => Some((tool_name, tool_call_id, arguments)), _ => None, }) .unwrap(); assert_eq!(started.0, "add"); assert_eq!(started.1, "call_1"); assert!(!started.2.is_empty(), "arguments 应携带实际 JSON 参数"); // 验证 ToolExecutionCompleted 的 result_summary let completed = events .iter() .find_map(|e| match e { StreamEvent::ToolExecutionCompleted { tool_name, tool_call_id, result_summary, is_error, } => Some((tool_name, tool_call_id, result_summary, *is_error)), _ => None, }) .unwrap(); assert_eq!(completed.0, "add"); assert_eq!(completed.1, "call_1"); assert!(!completed.2.is_empty()); assert!(!completed.3); // 应有最终 MessageComplete { stop_reason: Stop } let final_response = events .iter() .rev() .find_map(|e| match e { StreamEvent::MessageComplete { full_response } => Some(full_response.clone()), _ => None, }) .unwrap(); assert_eq!(final_response.stop_reason, StopReason::Stop); } /// Phase 9 测试 3.3 — 多轮工具调用 #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_multi_tool() { let provider = Mock::new(vec![ assistant_tool_call_response(vec![("call_1", "add", r#"{"a":1,"b":2}"#)]), assistant_tool_call_response(vec![("call_2", "add", r#"{"a":3,"b":4}"#)]), assistant_tool_call_response(vec![("call_3", "add", r#"{"a":5,"b":6}"#)]), assistant_text_response("完成"), ]); let mut cycle = LlmCycle::new(Box::new(provider), CycleConfig::default()); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let stream = cycle .submit_with_tools_stream("计算总和".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; let started_count = events .iter() .filter(|e| matches!(e, StreamEvent::ToolExecutionStarted { .. })) .count(); let completed_count = events .iter() .filter(|e| matches!(e, StreamEvent::ToolExecutionCompleted { .. })) .count(); assert_eq!(started_count, 3, "3 轮工具调用"); assert_eq!(completed_count, 3, "3 个 ToolExecutionCompleted"); // 应有 4 个 MessageComplete(每个 LLM 调用独立一个) let complete_count = events .iter() .filter(|e| matches!(e, StreamEvent::MessageComplete { .. })) .count(); assert_eq!(complete_count, 4, "4 个 LLM 调用 → 4 个 MessageComplete"); } /// Phase 9 测试 3.4 — 最大轮次超限 #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_max_turns_exceeded() { let config = CycleConfig { max_tool_turns: Some(2), ..Default::default() }; let provider = Mock::new(vec![ assistant_tool_call_response(vec![("c1", "add", r#"{"a":1,"b":1}"#)]), assistant_tool_call_response(vec![("c2", "add", r#"{"a":1,"b":1}"#)]), assistant_tool_call_response(vec![("c3", "add", r#"{"a":1,"b":1}"#)]), ]); let mut cycle = LlmCycle::new(Box::new(provider), config); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let stream = cycle .submit_with_tools_stream("test".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 流中应有 Error 事件(最大轮次超限) let error_events: Vec<_> = events .iter() .filter_map(|e| match e { StreamEvent::Error { message } => Some(message.clone()), _ => None, }) .collect(); assert!( error_events.iter().any(|m| m.contains("达到最大工具循环轮次")), "应包含最大轮次超限 Error,实际: {:?}", error_events ); // 工具调用次数应 ≤ 2 let started_count = events .iter() .filter(|e| matches!(e, StreamEvent::ToolExecutionStarted { .. })) .count(); assert_eq!(started_count, 2, "工具调用应在第 2 轮后停止"); } /// Phase 9 测试 3.5 — `chat_stream` 返回 Err /// /// 使用自定义 MockProvider 返回 chat_stream Err。 #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_chat_stream_err() { use crate::llm::provider::{ProviderCapabilities, ProviderFeatures}; struct ErrProvider; #[async_trait] impl LlmProvider for ErrProvider { async fn chat(&self, _r: MessageRequest) -> Result { Err(LlmError::Other("网络错误".into())) } async fn chat_stream( &self, _r: MessageRequest, ) -> Result< Pin> + Send>>, LlmError, > { Err(LlmError::Other("网络错误".into())) } fn capabilities(&self) -> ProviderCapabilities { ProviderCapabilities { provider_name: "err", supported_models: None, features: ProviderFeatures::default(), } } } let mut cycle = LlmCycle::new(Box::new(ErrProvider), CycleConfig::default()); let mut registry = ToolRegistry::new(); registry.register(std::sync::Arc::new(AddTool)).unwrap(); let stream = cycle .submit_with_tools_stream("test".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 第一个事件应是 Error(chat_stream Err 立即事件化) assert!( events.first().map(|e| matches!(e, StreamEvent::Error { .. })) == Some(true), "流中首个事件应是 Error,实际: {:?}", events.first() ); assert!(events .iter() .filter_map(|e| match e { StreamEvent::Error { message } => Some(message.as_str()), _ => None, }) .any(|m| m.contains("网络错误"))); } /// Phase 9 测试 3.6 — 空 tool_registry #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_empty_registry() { let provider = Mock::new(vec![assistant_text_response("纯文本回答")]); let mut cycle = LlmCycle::new(Box::new(provider), CycleConfig::default()); let registry = ToolRegistry::new(); let stream = cycle .submit_with_tools_stream("test".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 流退化为纯文本流 —— 无 ToolExecution 事件,无 Error assert!(events .iter() .any(|e| matches!(e, StreamEvent::TextDelta { text } if text == "纯文本回答"))); assert!(!events .iter() .any(|e| matches!(e, StreamEvent::ToolExecutionStarted { .. }))); assert!(!events .iter() .any(|e| matches!(e, StreamEvent::ToolExecutionCompleted { .. }))); assert!(!events .iter() .any(|e| matches!(e, StreamEvent::Error { .. }))); } /// Phase 9 测试 3.7 — 不可恢复工具错误 struct UnrecoverableTool; #[async_trait] impl BaseTool for UnrecoverableTool { fn name(&self) -> &str { "fail_unrecoverable" } fn description(&self) -> &str { "不可恢复地失败" } fn parameters(&self) -> Value { json!({}) } async fn execute( &self, _args: Value, _ctx: &crate::tools::ToolContext<'_>, ) -> Result { // 不可恢复错误 —— NotFound 表示工具在执行时失败且不可恢复 Err(crate::tools::ToolError::NotFound("永久失败".into())) } } #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_unrecoverable_tool_error() { let provider = Mock::new(vec![ assistant_tool_call_response(vec![("call_x", "fail_unrecoverable", "{}")]), assistant_text_response("忽略"), ]); let mut cycle = LlmCycle::new(Box::new(provider), CycleConfig::default()); let mut registry = ToolRegistry::new(); registry .register(std::sync::Arc::new(UnrecoverableTool)) .unwrap(); let stream = cycle .submit_with_tools_stream("test".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 流中应有 Error 事件(不可恢复错误) let error_events: Vec<_> = events .iter() .filter_map(|e| match e { StreamEvent::Error { message } => Some(message.clone()), _ => None, }) .collect(); assert!( error_events.iter().any(|m| m.contains("不可恢复")), "应包含不可恢复错误 Error" ); } /// Phase 9 测试 3.8 — 可恢复工具错误 struct RecoverableTool; #[async_trait] impl BaseTool for RecoverableTool { fn name(&self) -> &str { "fail_recoverable" } fn description(&self) -> &str { "可恢复失败" } fn parameters(&self) -> Value { json!({}) } async fn execute( &self, _args: Value, _ctx: &crate::tools::ToolContext<'_>, ) -> Result { // ExecutionFailed = 可恢复错误(is_recoverable() == true) Err(crate::tools::ToolError::ExecutionFailed( "工具暂时失败".into(), "网络抖动".into(), )) } } #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_recoverable_tool_error() { let provider = Mock::new(vec![ assistant_tool_call_response(vec![("call_y", "fail_recoverable", "{}")]), assistant_text_response("已恢复"), ]); let mut cycle = LlmCycle::new(Box::new(provider), CycleConfig::default()); let mut registry = ToolRegistry::new(); registry .register(std::sync::Arc::new(RecoverableTool)) .unwrap(); let stream = cycle .submit_with_tools_stream("test".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 可恢复错误:tool_result 回传 LLM,最终流正常结束 assert!(!events .iter() .any(|e| matches!(e, StreamEvent::Error { .. }))); // 最终 MessageComplete 应是 Stop(不是 ToolUse) let final_response = events .iter() .rev() .find_map(|e| match e { StreamEvent::MessageComplete { full_response } => Some(full_response.clone()), _ => None, }) .unwrap(); assert_eq!(final_response.stop_reason, StopReason::Stop); // ToolExecutionCompleted.is_error 应为 true let completed = events .iter() .find_map(|e| match e { StreamEvent::ToolExecutionCompleted { is_error, .. } => Some(*is_error), _ => None, }) .unwrap(); assert!(completed, "可恢复错误的 ToolExecutionCompleted.is_error 应为 true"); } /// Phase 9 测试 3.9 — 工具超时 /// /// 使用一个永远 sleep 的工具 + tool_timeout_secs: 1,验证 TimeoutError 路径。 /// /// 注:`tokio::time::timeout` 在 `invoke_all` 中将超时转为 `ToolError::McpTimeout("timeout")`。 /// 当前 `McpTimeout.is_recoverable() == false`(见 `tools/error.rs`),因此工具超时视为不可恢复: /// - 流中应出现 `ToolExecutionCompleted { is_error: true }` /// - 然后出现 `StreamEvent::Error`(不可恢复错误终止循环,§3.6 错误表) /// - 第一轮的 `MessageComplete { stop_reason: ToolUse }` 在 Error 事件之前已发出 /// - 不会再有第二轮 LLM 调用(与方案 §3.6 一致) #[tokio::test(flavor = "multi_thread")] async fn test_submit_with_tools_stream_tool_timeout() { struct SlowTool; #[async_trait] impl BaseTool for SlowTool { fn name(&self) -> &str { "slow_tool" } fn description(&self) -> &str { "慢工具,模拟超时" } fn parameters(&self) -> Value { json!({}) } async fn execute( &self, _args: Value, _ctx: &crate::tools::ToolContext<'_>, ) -> Result { // sleep 超过 5s(tool_timeout = 1s),触发 invoke_all 的 tokio::time::timeout tokio::time::sleep(std::time::Duration::from_secs(5)).await; Ok(json!({"ok": true})) } } let config = CycleConfig { tool_timeout_secs: 1, ..Default::default() }; // ponytail: 预设第二个响应不会消费——超时后流立即终止,不会发起第二轮 LLM 调用 let provider = Mock::new(vec![ assistant_tool_call_response(vec![("call_z", "slow_tool", "{}")]), assistant_text_response("不会到达"), ]); let mut cycle = LlmCycle::new(Box::new(provider), config); let mut registry = ToolRegistry::new(); registry .register(std::sync::Arc::new(SlowTool)) .unwrap(); let stream = cycle .submit_with_tools_stream("test".to_string(), std::sync::Arc::new(registry)) .await .unwrap(); let events = drain(stream).await; // 1. ToolExecutionCompleted 应报告 is_error=true(McpTimeout 视为失败) let tool_completed: Vec<_> = events .iter() .filter_map(|e| match e { StreamEvent::ToolExecutionCompleted { is_error, tool_name, .. } => Some((*is_error, tool_name.clone())), _ => None, }) .collect(); assert!(!tool_completed.is_empty(), "应有 ToolExecutionCompleted 事件"); assert!(tool_completed[0].0, "超时后 ToolExecutionCompleted.is_error 应为 true"); assert_eq!(tool_completed[0].1, "slow_tool"); // 2. 流中应有不可恢复错误终止事件(tool_timeout → McpTimeout → 不可恢复 → Error) let error_events: Vec<_> = events .iter() .filter_map(|e| match e { StreamEvent::Error { message } => Some(message.clone()), _ => None, }) .collect(); assert!( error_events .iter() .any(|m| m.contains("不可恢复错误")), "应有不可恢复错误事件终止流,实际事件: {:?}", error_events ); // 3. 第一轮的 MessageComplete { stop_reason: ToolUse } 在 Error 之前已发出 let first_complete = events .iter() .find_map(|e| match e { StreamEvent::MessageComplete { full_response } => Some(full_response.clone()), _ => None, }) .expect("第一轮 MessageComplete 应存在"); assert_eq!( first_complete.stop_reason, StopReason::ToolUse, "第一轮 LLM 响应 stop_reason 应为 ToolUse" ); // 4. 不会发起第二轮 LLM —— 流中只有 1 个 MessageComplete let complete_count = events .iter() .filter(|e| matches!(e, StreamEvent::MessageComplete { .. })) .count(); assert_eq!( complete_count, 1, "超时后不应有第二轮 LLM 流,应只有 1 个 MessageComplete" ); } }