Skip to main content

agent_base/tool/
subagent.rs

1use async_trait::async_trait;
2use serde_json::{json, Value};
3use tokio::sync::Mutex;
4
5use crate::engine::AgentRuntime;
6use crate::types::{AgentError, AgentEvent, AgentResult, SessionId};
7use super::{Tool, ToolContext, ToolControlFlow, ToolOutput};
8
9/// Sub-Agent session policy
10#[derive(Clone, Debug)]
11pub enum SubAgentSessionPolicy {
12    /// Create a new session per call (default)
13    Ephemeral,
14    /// Reuse the same session; sub-agent accumulates history
15    Persistent,
16}
17
18pub struct SubAgentTool {
19    name: &'static str,
20    description: &'static str,
21    sub_runtime: Mutex<AgentRuntime>,
22    sub_session_id: Mutex<Option<SessionId>>,
23    session_policy: SubAgentSessionPolicy,
24}
25
26impl SubAgentTool {
27    pub fn new(
28        name: &'static str,
29        description: &'static str,
30        sub_runtime: AgentRuntime,
31    ) -> Self {
32        Self {
33            name,
34            description,
35            sub_runtime: Mutex::new(sub_runtime),
36            sub_session_id: Mutex::new(None),
37            session_policy: SubAgentSessionPolicy::Ephemeral,
38        }
39    }
40
41    pub fn with_persistent(
42        name: &'static str,
43        description: &'static str,
44        sub_runtime: AgentRuntime,
45    ) -> Self {
46        Self {
47            name,
48            description,
49            sub_runtime: Mutex::new(sub_runtime),
50            sub_session_id: Mutex::new(None),
51            session_policy: SubAgentSessionPolicy::Persistent,
52        }
53    }
54}
55
56#[async_trait]
57impl Tool for SubAgentTool {
58    fn name(&self) -> &'static str {
59        self.name
60    }
61
62    fn definition(&self) -> Value {
63        json!({
64            "type": "function",
65            "function": {
66                "name": self.name,
67                "description": self.description,
68                "parameters": {
69                    "type": "object",
70                    "properties": {
71                        "task": {
72                            "type": "string",
73                            "description": "Task description to delegate to the sub-agent"
74                        }
75                    },
76                    "required": ["task"]
77                }
78            }
79        })
80    }
81
82    async fn call(&self, args: &Value, ctx: &ToolContext) -> AgentResult<ToolOutput> {
83        let task = args
84            .get("task")
85            .and_then(Value::as_str)
86            .ok_or_else(|| AgentError::ToolArgsInvalid {
87                name: self.name.to_string(),
88                raw: args.to_string(),
89            })?;
90
91        if task.is_empty() {
92            return Ok(ToolOutput {
93                summary: "Task description is empty, cannot execute".to_string(),
94                raw: None,
95                control_flow: ToolControlFlow::Break,
96                truncation: None,
97            });
98        }
99
100        let parent_event_bus = ctx.event_bus.clone();
101        let parent_session_id = ctx.session_id.clone();
102
103        let sub_session_id = match self.session_policy {
104            SubAgentSessionPolicy::Ephemeral => {
105                let runtime = self.sub_runtime.lock().await;
106                let new_id = runtime.create_session().await;
107                let mut sid_guard = self.sub_session_id.lock().await;
108                *sid_guard = Some(new_id.clone());
109                new_id
110            }
111            SubAgentSessionPolicy::Persistent => {
112                let mut sid_guard = self.sub_session_id.lock().await;
113                if let Some(id) = sid_guard.clone() {
114                    id
115                } else {
116                    let runtime = self.sub_runtime.lock().await;
117                    let new_id = runtime.create_session().await;
118                    *sid_guard = Some(new_id.clone());
119                    new_id
120                }
121            }
122        };
123
124        // Add the user task as a message to the sub-agent session
125        {
126            let runtime = self.sub_runtime.lock().await;
127            runtime.add_user_message(&sub_session_id, task).await.map_err(|e| AgentError::ToolExecution {
128                name: self.name.to_string(),
129                source: Box::new(e),
130            })?;
131        }
132
133        let mut events = Vec::new();
134        let _outcome = {
135            let runtime = self.sub_runtime.lock().await;
136            runtime
137                .run(sub_session_id, |event| {
138                    events.push(event.clone());
139                    Ok(())
140                })
141                .await
142                .map_err(|e| AgentError::ToolExecution {
143                    name: self.name.to_string(),
144                    source: Box::new(e),
145                })?
146        };
147
148        let mut final_text = String::new();
149        for event in &events {
150            match event {
151                AgentEvent::TextDelta { text, .. } => {
152                    final_text.push_str(&text);
153                }
154                _ => {}
155            }
156            let _ = parent_event_bus.send(AgentEvent::Custom {
157                session_id: parent_session_id.clone(),
158                payload: json!({
159                    "type": "subagent_event",
160                    "subagent": self.name,
161                    "event": event_to_value(&event),
162                }),
163            });
164        }
165
166        let summary = if final_text.is_empty() {
167            format!("Sub-agent [{}] finished", self.name)
168        } else {
169            final_text
170        };
171
172        Ok(ToolOutput {
173            summary,
174            raw: None,
175            control_flow: ToolControlFlow::Continue,
176            truncation: None,
177        })
178    }
179}
180
181fn event_to_value(event: &AgentEvent) -> Value {
182    match event {
183        AgentEvent::TextDelta { text, .. } => json!({"type": "TextDelta", "text": text}),
184        AgentEvent::ThoughtDelta { text, .. } => json!({"type": "ThoughtDelta", "text": text}),
185        AgentEvent::ToolCallStarted { tool_name, args_json, .. } => {
186            json!({"type": "ToolCallStarted", "tool_name": tool_name, "args_json": args_json})
187        }
188        AgentEvent::ToolCallFinished { tool_name, summary, .. } => {
189            json!({"type": "ToolCallFinished", "tool_name": tool_name, "summary": summary})
190        }
191        AgentEvent::AwaitingApproval { request, .. } => {
192            json!({"type": "AwaitingApproval", "title": request.title})
193        }
194        AgentEvent::Checkpoint { .. } => json!({"type": "Checkpoint"}),
195        AgentEvent::RunFinished { .. } => json!({"type": "RunFinished"}),
196        AgentEvent::Custom { payload, .. } => json!({"type": "Custom", "payload": payload}),
197        AgentEvent::PlanGenerated { plan, .. } => json!({"type": "PlanGenerated", "plan_id": plan.id}),
198        AgentEvent::PlanStepStarted { step_id, step_description, .. } => {
199            json!({"type": "PlanStepStarted", "step_id": step_id, "step_description": step_description})
200        }
201        AgentEvent::PlanStepCompleted { step_id, success, result, .. } => {
202            json!({"type": "PlanStepCompleted", "step_id": step_id, "success": success, "result": result})
203        }
204        AgentEvent::PlanCompleted { plan_id, success, .. } => {
205            json!({"type": "PlanCompleted", "plan_id": plan_id, "success": success})
206        }
207        AgentEvent::PlanGenerating { plan_id, .. } => {
208            json!({"type": "PlanGenerating", "plan_id": plan_id})
209        }
210        AgentEvent::PlanStepParsed { plan_id, step_index, step_id, step_description, .. } => {
211            json!({"type": "PlanStepParsed", "plan_id": plan_id, "step_index": step_index, "step_id": step_id, "step_description": step_description})
212        }
213        AgentEvent::PlanFailed { plan_id, error, .. } => {
214            json!({"type": "PlanFailed", "plan_id": plan_id, "error": error})
215        }
216    }
217}