1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use anyhow::{anyhow, Result};
5use tracing::{debug, info, warn};
6
7use crate::agents::agent::ActiveAgent;
8use crate::ai::provider::AiProvider;
9use crate::ai::types::{Content, ContentBlock, Message, MessageRole, ToolResultData};
10use crate::chat::request::prepare_request;
11use crate::chat::tool_extraction::extract_all_tool_calls;
12use crate::module::ContextBuilder;
13use crate::module::Module;
14use crate::module::PromptBuilder;
15use crate::settings::SettingsManager;
16use crate::steering::SteeringDocuments;
17use crate::tools::r#trait::{ToolExecutor, ToolOutput, ToolRequest};
18
19pub struct AgentRunner {
27 ai_provider: Arc<dyn AiProvider>,
28 settings: SettingsManager,
29 tools: BTreeMap<String, Arc<dyn ToolExecutor + Send + Sync>>,
30 modules: Vec<Arc<dyn Module>>,
31 steering: SteeringDocuments,
32 prompt_builder: PromptBuilder,
33 context_builder: ContextBuilder,
34}
35
36impl AgentRunner {
37 pub fn new(
38 ai_provider: Arc<dyn AiProvider>,
39 settings: SettingsManager,
40 tools: BTreeMap<String, Arc<dyn ToolExecutor + Send + Sync>>,
41 modules: Vec<Arc<dyn Module>>,
42 steering: SteeringDocuments,
43 prompt_builder: PromptBuilder,
44 context_builder: ContextBuilder,
45 ) -> Self {
46 Self {
47 ai_provider,
48 settings,
49 tools,
50 modules,
51 steering,
52 prompt_builder,
53 context_builder,
54 }
55 }
56
57 pub async fn run(
61 &self,
62 mut active_agent: ActiveAgent,
63 max_iterations: usize,
64 ) -> Result<String> {
65 for iteration in 0..max_iterations {
66 debug!(iteration, "AgentRunner iteration");
67
68 let (request, _model_settings) = prepare_request(
69 active_agent.agent.as_ref(),
70 &active_agent.conversation,
71 self.ai_provider.as_ref(),
72 self.settings.clone(),
73 &self.steering,
74 Vec::new(),
75 &self.prompt_builder,
76 &self.context_builder,
77 &self.modules,
78 )
79 .await?;
80
81 let response = self.ai_provider.converse(request).await?;
82 log_response_text(&response.content);
83
84 let extraction = extract_all_tool_calls(&response.content);
85 let tool_uses = extraction.tool_calls;
86
87 info!(
88 tool_count = tool_uses.len(),
89 tools = ?tool_uses.iter().map(|t| &t.name).collect::<Vec<_>>(),
90 "Extracted tools from response"
91 );
92
93 if let Some(parse_error) = extraction.xml_parse_error {
95 warn!("XML tool call parse error: {parse_error}");
96 active_agent.conversation.push(Message {
97 role: MessageRole::User,
98 content: Content::text_only(format!(
99 "Error parsing XML tool calls: {}. Please check your XML format and retry.",
100 parse_error
101 )),
102 });
103 }
104 if let Some(parse_error) = extraction.json_parse_error {
105 warn!("JSON tool call parse error: {parse_error}");
106 active_agent.conversation.push(Message {
107 role: MessageRole::User,
108 content: Content::text_only(format!(
109 "Error parsing JSON tool calls: {}. Please check your JSON format and retry.",
110 parse_error
111 )),
112 });
113 }
114
115 active_agent
116 .conversation
117 .push(Message::assistant(response.content.clone()));
118
119 if tool_uses.is_empty() {
120 warn!("AgentRunner completed - no more tool calls, but never got complete_task. This is likely a model error.");
121 break;
122 }
123
124 let mut tool_results = Vec::new();
125 let mut completion_result: Option<(bool, String)> = None;
126 for tool_use in &tool_uses {
127 info!(tool = %tool_use.name, args = %tool_use.arguments, "Runner calling tool");
128 let result = self
129 .execute_tool(&tool_use.name, &tool_use.id, &tool_use.arguments)
130 .await;
131
132 let (content, is_error) = match &result {
133 Ok((output, _)) => (output.clone(), false),
134 Err(e) => (format!("Error: {e:?}"), true),
135 };
136
137 if let Ok((_, tool_output)) = &result {
138 completion_result = completion_result.or(Self::extract_completion(tool_output));
139 }
140
141 let result_preview: String = content.chars().take(300).collect();
142 info!(tool = %tool_use.name, result = %result_preview, is_error, "Tool result");
143
144 tool_results.push(ContentBlock::ToolResult(ToolResultData {
145 tool_use_id: tool_use.id.to_string(),
146 content,
147 is_error,
148 }));
149 }
150
151 active_agent
152 .conversation
153 .push(Message::user(Content::new(tool_results)));
154
155 if let Some((success, result)) = completion_result {
156 if success {
157 debug!("AgentRunner completed via complete_task");
158 return Ok(result);
159 }
160 return Err(anyhow!("Task failed: {}", result));
161 }
162 }
163
164 Err(anyhow!(
165 "Agent did not complete task within {} iterations",
166 max_iterations
167 ))
168 }
169
170 fn extract_completion(output: &ToolOutput) -> Option<(bool, String)> {
171 if let ToolOutput::PopAgent { success, result } = output {
172 Some((*success, result.clone()))
173 } else {
174 None
175 }
176 }
177
178 async fn execute_tool(
179 &self,
180 name: &str,
181 tool_use_id: &str,
182 arguments: &serde_json::Value,
183 ) -> Result<(String, ToolOutput)> {
184 debug!(name, "Executing tool");
185
186 let executor = self
187 .tools
188 .get(name)
189 .ok_or_else(|| anyhow!("No executor for tool: {}", name))?;
190
191 let schema = executor.input_schema();
192 let coerced_arguments = crate::tools::fuzzy_json::coerce_to_schema(arguments, &schema)
193 .map_err(|e| anyhow!("Failed to coerce tool arguments: {e:?}"))?;
194 let request = ToolRequest::new(coerced_arguments, tool_use_id.to_string());
195 let handle = executor.process(&request).await?;
196
197 let tool_output = handle.execute().await;
198
199 let output_string = match &tool_output {
200 ToolOutput::Result {
201 content, is_error, ..
202 } => {
203 if *is_error {
204 return Err(anyhow!("{}", content));
205 }
206 content.clone()
207 }
208 ToolOutput::PopAgent { success, result } => {
209 format!("Task completed (success={}): {}", success, result)
210 }
211 ToolOutput::PushAgent { .. } | ToolOutput::PromptUser { .. } => {
212 return Err(anyhow!(
213 "Tool '{}' returned unsupported action for AgentRunner context",
214 name
215 ))
216 }
217 };
218
219 Ok((output_string, tool_output))
220 }
221}
222
223fn log_response_text(content: &Content) {
224 for block in content.blocks() {
225 let ContentBlock::Text(text) = block else {
226 continue;
227 };
228 let preview: String = text.chars().take(500).collect();
229 info!(response = %preview, "Agent reasoning");
230 }
231}