Skip to main content

ai_agent/tools/agent/
run_agent.rs

1// Source: ~/claudecode/openclaudecode/src/tools/AgentTool/runAgent.ts
2#![allow(dead_code)]
3use std::collections::HashMap;
4use std::sync::Arc;
5use tokio::fs;
6
7use super::agent_tool_utils::{
8    AgentToolResult, extract_partial_result, finalize_agent_tool, resolve_agent_tools,
9};
10use super::load_agents_dir::AgentDefinition;
11
12/// Context for tool execution passed to the agent.
13pub struct ToolContext {
14    pub available_tools: Vec<String>,
15    pub mcp_clients: Vec<String>,
16    pub commands: Vec<(String, String)>, // (name, description)
17    pub agent_definitions: Vec<AgentDefinition>,
18    pub main_loop_model: String,
19    pub custom_system_prompt: Option<String>,
20    pub append_system_prompt: Option<String>,
21    pub tool_use_id: Option<String>,
22}
23
24/// Agent configuration overrides.
25pub struct AgentOverrides {
26    pub user_context: Option<HashMap<String, String>>,
27    pub system_context: Option<HashMap<String, String>>,
28    pub system_prompt: Option<String>,
29    pub agent_id: Option<String>,
30}
31
32/// Result from running an agent.
33pub struct RunAgentResult {
34    pub messages: Vec<serde_json::Value>,
35    pub result: AgentToolResult,
36}
37
38/// Resolve the model for the agent.
39pub fn resolve_agent_model(
40    agent_model: Option<&str>,
41    main_loop_model: &str,
42    override_model: Option<&str>,
43) -> String {
44    if let Some(m) = override_model {
45        return m.to_string();
46    }
47    if let Some(m) = agent_model {
48        if m == "inherit" {
49            return main_loop_model.to_string();
50        }
51        return m.to_string();
52    }
53    main_loop_model.to_string()
54}
55
56/// Filter out assistant messages with incomplete tool calls (tool uses without results).
57pub fn filter_incomplete_tool_calls(messages: &[serde_json::Value]) -> Vec<serde_json::Value> {
58    // Build a set of tool use IDs that have results
59    let mut tool_use_ids_with_results = std::collections::HashSet::new();
60
61    for message in messages {
62        if message.get("type").and_then(|t| t.as_str()) == Some("user") {
63            if let Some(content) = message.get("message").and_then(|m| m.get("content")) {
64                if let Some(arr) = content.as_array() {
65                    for block in arr {
66                        if block.get("type").and_then(|t| t.as_str()) == Some("tool_result") {
67                            if let Some(id) = block.get("tool_use_id").and_then(|v| v.as_str()) {
68                                tool_use_ids_with_results.insert(id.to_string());
69                            }
70                        }
71                    }
72                }
73            }
74        }
75    }
76
77    // Filter out assistant messages that contain tool uses without results
78    messages
79        .iter()
80        .filter(|message| {
81            if message.get("type").and_then(|t| t.as_str()) != Some("assistant") {
82                return true;
83            }
84            if let Some(content) = message.get("message").and_then(|m| m.get("content")) {
85                if let Some(arr) = content.as_array() {
86                    let has_incomplete = arr.iter().any(|block| {
87                        block.get("type").and_then(|t| t.as_str()) == Some("tool_use")
88                            && block
89                                .get("id")
90                                .and_then(|v| v.as_str())
91                                .is_some_and(|id| !tool_use_ids_with_results.contains(id))
92                    });
93                    return !has_incomplete;
94                }
95            }
96            true
97        })
98        .cloned()
99        .collect()
100}
101
102/// Parameters for running an agent.
103pub struct RunAgentParams {
104    pub agent_definition: AgentDefinition,
105    pub prompt_messages: Vec<serde_json::Value>,
106    pub tool_context: ToolContext,
107    pub is_async: bool,
108    pub override_params: Option<AgentOverrides>,
109    pub model: Option<String>,
110    pub max_turns: Option<usize>,
111    pub fork_context_messages: Option<Vec<serde_json::Value>>,
112    pub allowed_tools: Option<Vec<String>>,
113    pub worktree_path: Option<String>,
114    pub description: Option<String>,
115}
116
117/// Run an agent synchronously and return the result.
118/// In a full implementation, this would use an async generator pattern to yield messages.
119/// Here we provide a simplified sync interface that runs the agent and returns the result.
120pub async fn run_agent(params: RunAgentParams) -> Result<RunAgentResult, String> {
121    let start_time = std::time::SystemTime::now()
122        .duration_since(std::time::UNIX_EPOCH)
123        .unwrap_or_default()
124        .as_millis() as u64;
125
126    let resolved_model = resolve_agent_model(
127        params.agent_definition.model.as_deref(),
128        &params.tool_context.main_loop_model,
129        params.model.as_deref(),
130    );
131
132    let agent_id = params
133        .override_params
134        .as_ref()
135        .and_then(|o| o.agent_id.clone())
136        .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
137
138    // Handle message forking for context sharing
139    let context_messages = params
140        .fork_context_messages
141        .as_ref()
142        .map(|msgs| filter_incomplete_tool_calls(msgs))
143        .unwrap_or_default();
144
145    let mut initial_messages: Vec<serde_json::Value> = context_messages;
146    initial_messages.extend(params.prompt_messages);
147
148    // Resolve tools
149    let resolved = resolve_agent_tools(
150        &params.agent_definition,
151        &params.tool_context.available_tools,
152        params.is_async,
153    );
154
155    // Build agent-specific options
156    let _agent_system_prompt = params
157        .override_params
158        .as_ref()
159        .and_then(|o| o.system_prompt.clone())
160        .unwrap_or_else(|| params.agent_definition.system_prompt());
161
162    // In a full implementation, the query() loop would:
163    // 1. Send messages + system prompt to the API
164    // 2. Parse the response
165    // 3. Execute tool calls
166    // 4. Build new messages with tool results
167    // 5. Repeat until max_turns or stop sequence
168    //
169    // Here we provide a simplified implementation that returns the initial messages
170    // and a placeholder result. A full port would integrate with the actual API client.
171
172    log::debug!(
173        "Running agent '{}' (type: {}, model: {}, async: {})",
174        agent_id,
175        params.agent_definition.agent_type,
176        resolved_model,
177        params.is_async
178    );
179
180    // Record metadata
181    let _ = write_agent_metadata(
182        &agent_id,
183        &params.agent_definition,
184        &params.worktree_path,
185        &params.description,
186    )
187    .await;
188
189    // Build the result
190    let result = AgentToolResult {
191        agent_id: agent_id.clone(),
192        agent_type: Some(params.agent_definition.agent_type.clone()),
193        content: "Agent completed".to_string(),
194        total_tool_use_count: 0,
195        total_duration_ms: 0,
196        total_tokens: 0,
197        usage: super::agent_tool_utils::TokenUsage::default(),
198    };
199
200    Ok(RunAgentResult {
201        messages: initial_messages,
202        result,
203    })
204}
205
206/// Write agent metadata for persistence.
207async fn write_agent_metadata(
208    agent_id: &str,
209    agent_definition: &AgentDefinition,
210    worktree_path: &Option<String>,
211    description: &Option<String>,
212) -> std::io::Result<()> {
213    let metadata_dir = std::env::current_dir()?
214        .join(".claude")
215        .join("subagents")
216        .join(agent_id);
217    fs::create_dir_all(&metadata_dir).await?;
218
219    let meta = serde_json::json!({
220        "agentType": agent_definition.agent_type,
221        "worktreePath": worktree_path,
222        "description": description,
223    });
224
225    fs::write(
226        metadata_dir.join("metadata.json"),
227        serde_json::to_string_pretty(&meta)?,
228    )
229    .await
230}
231
232/// Clean up resources after agent completion.
233pub fn cleanup_agent(agent_id: &str) {
234    // Release any resources associated with this agent
235    // In a full implementation, this would:
236    // - Clear MCP server connections specific to this agent
237    // - Clear session hooks
238    // - Release file state cache memory
239    // - Kill any background tasks spawned by this agent
240    // - Release todo entries
241    log::debug!("Cleaning up agent: {}", agent_id);
242}
243
244/// Extract a summary from agent messages (for async agent notifications).
245pub fn extract_agent_summary(messages: &[serde_json::Value]) -> String {
246    // Get the last assistant message with text content
247    for msg in messages.iter().rev() {
248        if msg.get("type").and_then(|t| t.as_str()) != Some("assistant") {
249            continue;
250        }
251        if let Some(content) = msg.get("message").and_then(|m| m.get("content")) {
252            if let Some(arr) = content.as_array() {
253                let text = super::agent_tool_utils::extract_text_content(arr, "\n");
254                if !text.is_empty() {
255                    // Truncate to reasonable notification length
256                    if text.len() > 500 {
257                        return format!("{}...", &text[..497]);
258                    }
259                    return text;
260                }
261            }
262        }
263    }
264
265    // Fall back to partial result extraction
266    extract_partial_result(messages).unwrap_or_else(|| "Agent completed".to_string())
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    fn make_agent_def() -> AgentDefinition {
274        AgentDefinition {
275            agent_type: "test".to_string(),
276            when_to_use: "test".to_string(),
277            tools: vec!["*".to_string()],
278            disallowed_tools: vec![],
279            source: "built-in".to_string(),
280            base_dir: "built-in".to_string(),
281            get_system_prompt: Arc::new(|| String::new()),
282            model: None,
283            max_turns: None,
284            permission_mode: None,
285            effort: None,
286            color: None,
287            mcp_servers: vec![],
288            hooks: None,
289            skills: vec![],
290            background: false,
291            initial_prompt: None,
292            memory: None,
293            isolation: None,
294            required_mcp_servers: vec![],
295            omit_claude_md: false,
296            critical_system_reminder_experimental: None,
297        }
298    }
299
300    #[test]
301    fn test_resolve_agent_model_override() {
302        assert_eq!(
303            resolve_agent_model(Some("haiku"), "sonnet", Some("opus")),
304            "opus"
305        );
306    }
307
308    #[test]
309    fn test_resolve_agent_model_inherit() {
310        assert_eq!(
311            resolve_agent_model(Some("inherit"), "sonnet", None),
312            "sonnet"
313        );
314    }
315
316    #[test]
317    fn test_filter_incomplete_tool_calls_keeps_complete() {
318        let messages = vec![
319            serde_json::json!({
320                "type": "assistant",
321                "message": {
322                    "content": [{"type": "tool_use", "id": "1", "name": "Bash"}]
323                }
324            }),
325            serde_json::json!({
326                "type": "user",
327                "message": {
328                    "content": [{"type": "tool_result", "tool_use_id": "1", "content": "done"}]
329                }
330            }),
331        ];
332        let filtered = filter_incomplete_tool_calls(&messages);
333        assert_eq!(filtered.len(), 2);
334    }
335
336    #[test]
337    fn test_filter_incomplete_tool_calls_removes_incomplete() {
338        let messages = vec![serde_json::json!({
339            "type": "assistant",
340            "message": {
341                "content": [{"type": "tool_use", "id": "1", "name": "Bash"}]
342            }
343        })];
344        let filtered = filter_incomplete_tool_calls(&messages);
345        assert_eq!(filtered.len(), 0);
346    }
347
348    #[test]
349    fn test_extract_agent_summary_from_messages() {
350        let messages = vec![serde_json::json!({
351            "type": "assistant",
352            "message": {
353                "content": [{"type": "text", "text": "Task completed successfully"}]
354            }
355        })];
356        let summary = extract_agent_summary(&messages);
357        assert_eq!(summary, "Task completed successfully");
358    }
359}