1use std::sync::Arc;
2
3use tokio::sync::mpsc;
4use tracing::info;
5use uuid::Uuid;
6
7use crate::config::{AgentConfig, LlmConfig};
8use crate::error::SdkResult;
9use crate::traits::llm_client::LlmClient;
10use crate::traits::prompt_builder::{DefaultPromptBuilder, PromptBuilder};
11use crate::mailbox::broker::MessageBroker;
12use crate::storage::AgentPaths;
13use crate::task::store::TaskStore;
14use crate::types::task::Task;
15
16use super::agent_loop::{AgentLoop, AgentLoopResult};
17use super::events::AgentEvent;
18use super::hooks::HookRegistry;
19use super::memory::MemoryStore;
20use super::team_lead::{ExecutionSummary, TeamLead, TeammateSpec};
21
22#[derive(Debug)]
24pub enum TeamResult {
25 Single(AgentLoopResult),
27 Team(ExecutionSummary),
29}
30
31impl TeamResult {
32 pub fn total_tokens(&self) -> u64 {
33 match self {
34 Self::Single(r) => r.total_tokens,
35 Self::Team(s) => s.total_tokens_used,
36 }
37 }
38}
39
40pub struct AgentTeam {
76 llm_config: LlmConfig,
77 agent_config: AgentConfig,
78 llm_client: Option<Arc<dyn LlmClient>>,
79 prompt_builder: Arc<dyn PromptBuilder>,
80 hooks: HookRegistry,
81 source_root: std::path::PathBuf,
82 work_dir: std::path::PathBuf,
83 event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
84 teammate_specs: Vec<TeammateSpec>,
86 tasks: Vec<Task>,
88}
89
90impl AgentTeam {
91 pub fn new(llm_config: LlmConfig, agent_config: AgentConfig) -> Self {
93 Self {
94 llm_config,
95 agent_config,
96 llm_client: None,
97 prompt_builder: Arc::new(DefaultPromptBuilder),
98 hooks: HookRegistry::new(),
99 source_root: std::path::PathBuf::from("."),
100 work_dir: std::path::PathBuf::from("./output"),
101 event_tx: None,
102 teammate_specs: Vec::new(),
103 tasks: Vec::new(),
104 }
105 }
106
107 pub fn source_root(mut self, path: impl Into<std::path::PathBuf>) -> Self {
109 self.source_root = path.into();
110 self
111 }
112
113 pub fn work_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
115 self.work_dir = path.into();
116 self
117 }
118
119 pub fn prompt_builder(mut self, builder: Arc<dyn PromptBuilder>) -> Self {
121 self.prompt_builder = builder;
122 self
123 }
124
125 pub fn event_channel(mut self, tx: mpsc::UnboundedSender<AgentEvent>) -> Self {
127 self.event_tx = Some(tx);
128 self
129 }
130
131 pub fn llm_client(mut self, client: Arc<dyn LlmClient>) -> Self {
133 self.llm_client = Some(client);
134 self
135 }
136
137 pub fn add_hook(mut self, hook: impl super::hooks::Hook + 'static) -> Self {
139 self.hooks.add(hook);
140 self
141 }
142
143 pub fn add_teammate(
153 mut self,
154 name: impl Into<String>,
155 prompt: impl Into<String>,
156 ) -> Self {
157 self.teammate_specs.push(TeammateSpec {
158 name: name.into(),
159 prompt: prompt.into(),
160 require_plan_approval: false,
161 });
162 self
163 }
164
165 pub fn add_teammate_with_plan_approval(
169 mut self,
170 name: impl Into<String>,
171 prompt: impl Into<String>,
172 ) -> Self {
173 self.teammate_specs.push(TeammateSpec {
174 name: name.into(),
175 prompt: prompt.into(),
176 require_plan_approval: true,
177 });
178 self
179 }
180
181 pub fn add_task(mut self, task: Task) -> Self {
196 self.tasks.push(task);
197 self
198 }
199
200 pub async fn run(mut self, _goal: &str) -> SdkResult<TeamResult> {
203 let client = match self.llm_client.take() {
204 Some(c) => c,
205 None => crate::llm::create_client(&self.llm_config)?,
206 };
207 let paths = AgentPaths::for_work_dir(&self.work_dir)?;
208 let team_name = paths.new_team_name();
209 let team_config_path = paths.team_config_path(&team_name);
210
211 let hooks = Arc::new(std::mem::take(&mut self.hooks));
212 let task_store = Arc::new(TaskStore::new(paths.team_tasks_dir(&team_name)));
213 task_store.init()?;
214
215 for task in &self.tasks {
217 let hook_result = hooks.evaluate(
218 &super::hooks::HookEvent::TaskCreated { task: task.clone() },
219 );
220 if let super::hooks::HookResult::Reject { feedback } = hook_result {
221 self.emit_event(AgentEvent::HookRejected {
222 event_name: "TaskCreated".to_string(),
223 feedback,
224 });
225 continue;
226 }
227 task_store.create_task(task)?;
228 }
229
230 std::fs::create_dir_all(paths.team_dir(&team_name)).map_err(crate::error::SdkError::Io)?;
231 let broker = Arc::new(MessageBroker::new(paths.team_mailbox_dir(&team_name))?);
232 let memory = Arc::new(MemoryStore::new(paths.team_memory_dir(&team_name))?);
233
234 let lead = TeamLead {
235 id: Uuid::new_v4(),
236 team_name,
237 team_config_path,
238 task_store,
239 broker,
240 llm_client: client,
241 prompt_builder: self.prompt_builder.clone(),
242 config: self.agent_config.clone(),
243 source_root: self.source_root.clone(),
244 work_dir: self.work_dir.clone(),
245 memory_store: memory,
246 event_tx: self.event_tx.clone(),
247 hooks,
248 teammate_specs: self.teammate_specs.clone(),
249 };
250
251 self.emit_event(AgentEvent::TeamSpawned {
252 teammate_count: self.teammate_specs.len().max(self.agent_config.max_parallel_agents),
253 });
254
255 lead.run().await.map(TeamResult::Team)
256 }
257
258 pub async fn run_single(mut self, user_message: &str) -> SdkResult<AgentLoopResult> {
260 let client = match self.llm_client.take() {
261 Some(c) => c,
262 None => crate::llm::create_client(&self.llm_config)?,
263 };
264
265 use crate::tools::command_tools::RunCommandTool;
266 use crate::tools::fs_tools::{ListDirectoryTool, ReadFileTool, WriteFileTool};
267 use crate::tools::registry::ToolRegistry;
268 use crate::tools::search_tools::SearchFilesTool;
269 use crate::tools::web_tools::WebSearchTool;
270
271 let mut tools = ToolRegistry::new();
272 tools.register(Arc::new(ReadFileTool {
273 source_root: self.source_root.clone(),
274 work_dir: self.work_dir.clone(),
275 }));
276 tools.register(Arc::new(WriteFileTool {
277 work_dir: self.work_dir.clone(),
278 }));
279 tools.register(Arc::new(ListDirectoryTool {
280 source_root: self.source_root.clone(),
281 work_dir: self.work_dir.clone(),
282 }));
283 tools.register(Arc::new(SearchFilesTool {
284 source_root: self.source_root.clone(),
285 }));
286 tools.register(Arc::new(WebSearchTool));
287 tools.register(Arc::new(RunCommandTool::with_defaults(
288 self.work_dir.clone(),
289 )));
290
291 let system = crate::prompts::single_agent_system_prompt(
292 &self.source_root,
293 &self.work_dir,
294 );
295
296 let mut agent = AgentLoop::new(
297 Uuid::new_v4(),
298 client,
299 tools,
300 system,
301 self.agent_config.max_loop_iterations,
302 );
303
304 if let Some(ref tx) = self.event_tx {
305 agent.set_event_sink(tx.clone());
306 }
307
308 info!("Running as single agent");
309 agent.run(user_message.to_string()).await
310 }
311
312 fn emit_event(&self, event: AgentEvent) {
313 if let Some(ref tx) = self.event_tx {
314 let _ = tx.send(event);
315 }
316 }
317}