//! 端到端集成:Agent + AnthropicProvider + mock transport 跑完整工具回合。 //! End-to-end integration: Agent + AnthropicProvider + mock transport running //! a full tool-call turn. mod common; use focus_core::event::VecSink; use focus_core::model::*; use focus_core::tool::{Tool, ToolEffects, ToolRegistry, ToolResult, ToolUpdateSink}; use focus_core::{Agent, AgentConfig, CoreError}; use focus_json::JsonValue; use focus_providers::anthropic::AnthropicProvider; use focus_providers::config::ProviderConfig; use std::sync::Arc; /// 记录调用参数的 echo 工具。 /// An echo tool that records its arguments. #[derive(Debug, Default)] struct EchoTool { calls: Arc>>, } impl Clone for EchoTool { fn clone(&self) -> Self { Self { calls: self.calls.clone(), } } } impl Tool for EchoTool { fn name(&self) -> &str { "echo" } fn description(&self) -> &str { "echoes the arguments back" } fn parameters(&self) -> JsonValue { let mut o = JsonValue::obj(); o.insert("type", "object".into()).ok(); let mut props = JsonValue::obj(); props.insert("text", JsonValue::Str("the text".into())).ok(); o.insert("properties", props).ok(); o } fn effects(&self) -> ToolEffects { ToolEffects::READ } fn execute( &self, _id: &str, args: &JsonValue, _u: Option<&ToolUpdateSink<'_>>, ) -> Result { self.calls.lock().unwrap().push(args.clone()); Ok(ToolResult::text(format!( "echo: {}", args.get_str("text").unwrap_or("") ))) } } /// 回合 1:模型调用 echo 工具;回合 2:模型给出最终文本。 /// Turn 1: the model calls the echo tool; turn 2: the model gives final text. #[test] fn agent_runs_full_tool_turn_against_anthropic() { let mut mock = common::MockTransport::new(); // 回合 1:tool_use → 回合 2:文本回答。 // Turn 1: tool_use → turn 2: text answer. mock.push_body( "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":20}}}\n\n\ event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_1\",\"name\":\"echo\",\"input\":{}}}\n\n\ event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"text\\\":\\\"world\\\"}\"}}\n\n\ event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n\ event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":8}}\n\n\ event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n", ); mock.push_body( "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":30}}}\n\n\ event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n\ event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"all done\"}}\n\n\ event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n\ event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":4}}\n\n\ event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n", ); let provider = AnthropicProvider::with_transport(ProviderConfig::new("sk-test"), Arc::new(mock.clone())); let config = AgentConfig { model: "claude-sonnet-4".into(), system_prompt: "You are helpful.".into(), max_tokens: Some(1024), ..Default::default() }; let echo = EchoTool::default(); let calls = echo.calls.clone(); let tools = ToolRegistry::with(Box::new(echo)); let mut agent = Agent::new(config, Box::new(provider), tools); let mut sink = VecSink::new(); agent .prompt("please echo world", &mut sink) .expect("run failed"); // 两次回合、一次工具执行。 // Two turns, one tool execution. assert_eq!(sink.count("turn_end"), 2); assert_eq!(sink.count("tool_execution_end"), 1); let msgs = agent.messages(); assert_eq!(msgs.len(), 4); assert!(matches!(msgs[0], Message::User(_))); assert!(matches!( &msgs[1], Message::Assistant(a) if a.content.iter().any(|c| c.as_tool_call().is_some()) )); assert!(matches!( &msgs[2], Message::ToolResult(tr) if tr.tool_name == "echo" && !tr.is_error )); let final_text = match &msgs[3] { Message::Assistant(a) => a .content .iter() .find_map(|c| c.as_text()) .map(|t| t.text.clone()), _ => None, }; assert_eq!(final_text.as_deref(), Some("all done")); // 工具确实收到了模型传入的参数。 // The tool actually received the model's arguments. let recorded = calls.lock().unwrap(); assert_eq!(recorded.len(), 1); assert_eq!(recorded[0].get_str("text"), Some("world")); // 两个回合都发出了 HTTP 请求。 // Both turns issued HTTP requests. assert_eq!(mock.request_count(), 2); }