agent_base/tool/
subagent.rs1use async_trait::async_trait;
2use serde_json::{Value, json};
3use tokio::sync::Mutex;
4
5use super::{Tool, ToolContext, ToolControlFlow, ToolOutput};
6use crate::engine::AgentRuntime;
7use crate::types::{AgentError, AgentResult, RuntimeEvent, SessionId, UserEvent};
8
9#[derive(Clone, Debug)]
11pub enum SubAgentSessionPolicy {
12 Ephemeral,
14 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(name: &'static str, description: &'static str, sub_runtime: AgentRuntime) -> Self {
28 Self {
29 name,
30 description,
31 sub_runtime: Mutex::new(sub_runtime),
32 sub_session_id: Mutex::new(None),
33 session_policy: SubAgentSessionPolicy::Ephemeral,
34 }
35 }
36
37 pub fn with_persistent(
38 name: &'static str,
39 description: &'static str,
40 sub_runtime: AgentRuntime,
41 ) -> Self {
42 Self {
43 name,
44 description,
45 sub_runtime: Mutex::new(sub_runtime),
46 sub_session_id: Mutex::new(None),
47 session_policy: SubAgentSessionPolicy::Persistent,
48 }
49 }
50}
51
52#[async_trait]
53impl Tool for SubAgentTool {
54 fn name(&self) -> &'static str {
55 self.name
56 }
57
58 fn definition(&self) -> Value {
59 json!({
60 "type": "function",
61 "function": {
62 "name": self.name,
63 "description": self.description,
64 "parameters": {
65 "type": "object",
66 "properties": {
67 "task": {
68 "type": "string",
69 "description": "Task description to delegate to the sub-agent"
70 }
71 },
72 "required": ["task"]
73 }
74 }
75 })
76 }
77
78 async fn call(&self, args: &Value, ctx: &ToolContext) -> AgentResult<ToolOutput> {
79 let task = args.get("task").and_then(Value::as_str).ok_or_else(|| {
80 AgentError::ToolArgsInvalid {
81 name: self.name.to_string(),
82 raw: args.to_string(),
83 }
84 })?;
85
86 if task.is_empty() {
87 return Ok(ToolOutput {
88 summary: "Task description is empty, cannot execute".to_string(),
89 raw: None,
90 control_flow: ToolControlFlow::Break,
91 truncation: None,
92 });
93 }
94
95 let user_event_tx = ctx.user_event_tx.clone();
96
97 let sub_session_id = match self.session_policy {
98 SubAgentSessionPolicy::Ephemeral => {
99 let runtime = self.sub_runtime.lock().await;
100 let new_id = runtime.create_session().await;
101 let mut sid_guard = self.sub_session_id.lock().await;
102 *sid_guard = Some(new_id.clone());
103 tracing::debug!(
104 subagent = self.name,
105 sub_session = new_id.id,
106 "sub-agent ephemeral session created"
107 );
108 new_id
109 }
110 SubAgentSessionPolicy::Persistent => {
111 let mut sid_guard = self.sub_session_id.lock().await;
112 if let Some(id) = sid_guard.clone() {
113 tracing::debug!(
114 subagent = self.name,
115 sub_session = id.id,
116 "sub-agent reusing persistent session"
117 );
118 id
119 } else {
120 let runtime = self.sub_runtime.lock().await;
121 let new_id = runtime.create_session().await;
122 *sid_guard = Some(new_id.clone());
123 tracing::debug!(
124 subagent = self.name,
125 sub_session = new_id.id,
126 "sub-agent persistent session created"
127 );
128 new_id
129 }
130 }
131 };
132
133 {
135 let runtime = self.sub_runtime.lock().await;
136 runtime
137 .add_user_message(&sub_session_id, task)
138 .await
139 .map_err(|e| AgentError::ToolExecution {
140 name: self.name.to_string(),
141 source: Box::new(e),
142 })?;
143 }
144
145 let mut runtime_events = Vec::new();
146 let _outcome = {
147 let runtime = self.sub_runtime.lock().await;
148 runtime
149 .run(sub_session_id, |event| {
150 runtime_events.push(event.clone());
151 Ok(())
152 })
153 .await
154 .map_err(|e| AgentError::ToolExecution {
155 name: self.name.to_string(),
156 source: Box::new(e),
157 })?
158 };
159
160 let mut final_text = String::new();
161 for event in &runtime_events {
162 match event {
163 RuntimeEvent::TextDelta { text, .. } => {
164 final_text.push_str(&text);
165 }
166 _ => {}
167 }
168 let _ = user_event_tx.send(UserEvent::SubAgentEvent {
170 subagent: self.name.to_string(),
171 event: Box::new(event.clone()),
172 });
173 }
174
175 let summary = if final_text.is_empty() {
176 format!("Sub-agent [{}] finished", self.name)
177 } else {
178 final_text
179 };
180
181 tracing::debug!(
182 subagent = self.name,
183 text_len = summary.len(),
184 event_count = runtime_events.len(),
185 "sub-agent completed"
186 );
187
188 Ok(ToolOutput {
189 summary,
190 raw: None,
191 control_flow: ToolControlFlow::Continue,
192 truncation: None,
193 })
194 }
195}