1use std::collections::HashMap;
40use std::sync::{Arc, RwLock};
41use std::time::Duration;
42use tokio::sync::mpsc;
43
44#[derive(Debug, Clone)]
46pub struct TeamConfig {
47 pub max_tasks: usize,
50 pub channel_buffer: usize,
53 pub max_rounds: usize,
56 pub poll_interval_ms: u64,
59}
60
61impl Default for TeamConfig {
62 fn default() -> Self {
63 Self {
64 max_tasks: 50,
65 channel_buffer: 128,
66 max_rounds: 10,
67 poll_interval_ms: 200,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum TeamRole {
76 Lead,
78 Worker,
80 Reviewer,
82}
83
84impl std::fmt::Display for TeamRole {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 TeamRole::Lead => write!(f, "lead"),
88 TeamRole::Worker => write!(f, "worker"),
89 TeamRole::Reviewer => write!(f, "reviewer"),
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct TeamMessage {
97 pub from: String,
99 pub to: String,
101 pub content: String,
103 pub task_id: Option<String>,
105 pub timestamp: i64,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum TaskStatus {
112 Open,
114 InProgress,
116 InReview,
118 Done,
120 Rejected,
122}
123
124impl std::fmt::Display for TaskStatus {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 match self {
127 TaskStatus::Open => write!(f, "open"),
128 TaskStatus::InProgress => write!(f, "in_progress"),
129 TaskStatus::InReview => write!(f, "in_review"),
130 TaskStatus::Done => write!(f, "done"),
131 TaskStatus::Rejected => write!(f, "rejected"),
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct TeamTask {
139 pub id: String,
141 pub description: String,
143 pub posted_by: String,
145 pub assigned_to: Option<String>,
147 pub status: TaskStatus,
149 pub result: Option<String>,
151 pub created_at: i64,
153 pub updated_at: i64,
155}
156
157#[derive(Debug)]
159pub struct TeamTaskBoard {
160 tasks: RwLock<Vec<TeamTask>>,
161 max_tasks: usize,
162 next_id: RwLock<u64>,
163}
164
165impl TeamTaskBoard {
166 pub fn new(max_tasks: usize) -> Self {
168 Self {
169 tasks: RwLock::new(Vec::new()),
170 max_tasks,
171 next_id: RwLock::new(1),
172 }
173 }
174
175 pub fn post(
177 &self,
178 description: &str,
179 posted_by: &str,
180 assign_to: Option<&str>,
181 ) -> Option<String> {
182 let mut tasks = self.tasks.write().unwrap();
183 if tasks.len() >= self.max_tasks {
184 return None;
185 }
186
187 let mut id_counter = self.next_id.write().unwrap();
188 let id = format!("task-{}", *id_counter);
189 *id_counter += 1;
190
191 let now = chrono::Utc::now().timestamp();
192 let status = if assign_to.is_some() {
193 TaskStatus::InProgress
194 } else {
195 TaskStatus::Open
196 };
197
198 tasks.push(TeamTask {
199 id: id.clone(),
200 description: description.to_string(),
201 posted_by: posted_by.to_string(),
202 assigned_to: assign_to.map(|s| s.to_string()),
203 status,
204 result: None,
205 created_at: now,
206 updated_at: now,
207 });
208
209 Some(id)
210 }
211
212 pub fn claim(&self, member_id: &str) -> Option<TeamTask> {
217 let mut tasks = self.tasks.write().unwrap();
218 let task = tasks
219 .iter_mut()
220 .find(|t| t.status == TaskStatus::Open || t.status == TaskStatus::Rejected)?;
221 task.assigned_to = Some(member_id.to_string());
222 task.status = TaskStatus::InProgress;
223 task.updated_at = chrono::Utc::now().timestamp();
224 Some(task.clone())
225 }
226
227 pub fn complete(&self, task_id: &str, result: &str) -> bool {
229 let mut tasks = self.tasks.write().unwrap();
230 if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
231 task.status = TaskStatus::InReview;
232 task.result = Some(result.to_string());
233 task.updated_at = chrono::Utc::now().timestamp();
234 true
235 } else {
236 false
237 }
238 }
239
240 pub fn approve(&self, task_id: &str) -> bool {
242 let mut tasks = self.tasks.write().unwrap();
243 if let Some(task) = tasks
244 .iter_mut()
245 .find(|t| t.id == task_id && t.status == TaskStatus::InReview)
246 {
247 task.status = TaskStatus::Done;
248 task.updated_at = chrono::Utc::now().timestamp();
249 true
250 } else {
251 false
252 }
253 }
254
255 pub fn reject(&self, task_id: &str) -> bool {
257 let mut tasks = self.tasks.write().unwrap();
258 if let Some(task) = tasks
259 .iter_mut()
260 .find(|t| t.id == task_id && t.status == TaskStatus::InReview)
261 {
262 task.status = TaskStatus::Rejected;
263 task.assigned_to = None;
264 task.updated_at = chrono::Utc::now().timestamp();
265 true
266 } else {
267 false
268 }
269 }
270
271 pub fn by_status(&self, status: TaskStatus) -> Vec<TeamTask> {
273 self.tasks
274 .read()
275 .unwrap()
276 .iter()
277 .filter(|t| t.status == status)
278 .cloned()
279 .collect()
280 }
281
282 pub fn by_assignee(&self, member_id: &str) -> Vec<TeamTask> {
284 self.tasks
285 .read()
286 .unwrap()
287 .iter()
288 .filter(|t| t.assigned_to.as_deref() == Some(member_id))
289 .cloned()
290 .collect()
291 }
292
293 pub fn get(&self, task_id: &str) -> Option<TeamTask> {
295 self.tasks
296 .read()
297 .unwrap()
298 .iter()
299 .find(|t| t.id == task_id)
300 .cloned()
301 }
302
303 pub fn len(&self) -> usize {
305 self.tasks.read().unwrap().len()
306 }
307
308 pub fn is_empty(&self) -> bool {
310 self.tasks.read().unwrap().is_empty()
311 }
312
313 pub fn stats(&self) -> (usize, usize, usize, usize, usize) {
315 let tasks = self.tasks.read().unwrap();
316 let open = tasks
317 .iter()
318 .filter(|t| t.status == TaskStatus::Open)
319 .count();
320 let progress = tasks
321 .iter()
322 .filter(|t| t.status == TaskStatus::InProgress)
323 .count();
324 let review = tasks
325 .iter()
326 .filter(|t| t.status == TaskStatus::InReview)
327 .count();
328 let done = tasks
329 .iter()
330 .filter(|t| t.status == TaskStatus::Done)
331 .count();
332 let rejected = tasks
333 .iter()
334 .filter(|t| t.status == TaskStatus::Rejected)
335 .count();
336 (open, progress, review, done, rejected)
337 }
338}
339
340#[derive(Debug, Clone)]
342pub struct TeamMember {
343 pub id: String,
345 pub role: TeamRole,
347}
348
349pub struct AgentTeam {
351 name: String,
353 config: TeamConfig,
355 members: HashMap<String, TeamMember>,
357 task_board: Arc<TeamTaskBoard>,
359 senders: HashMap<String, mpsc::Sender<TeamMessage>>,
361 receivers: HashMap<String, mpsc::Receiver<TeamMessage>>,
363}
364
365impl AgentTeam {
366 pub fn new(name: &str, config: TeamConfig) -> Self {
368 Self {
369 name: name.to_string(),
370 config,
371 members: HashMap::new(),
372 task_board: Arc::new(TeamTaskBoard::new(50)),
373 senders: HashMap::new(),
374 receivers: HashMap::new(),
375 }
376 }
377
378 pub fn name(&self) -> &str {
380 &self.name
381 }
382
383 pub fn add_member(&mut self, id: &str, role: TeamRole) {
385 let (tx, rx) = mpsc::channel(self.config.channel_buffer);
386 self.members.insert(
387 id.to_string(),
388 TeamMember {
389 id: id.to_string(),
390 role,
391 },
392 );
393 self.senders.insert(id.to_string(), tx);
394 self.receivers.insert(id.to_string(), rx);
395 }
396
397 pub fn remove_member(&mut self, id: &str) -> bool {
399 self.senders.remove(id);
400 self.receivers.remove(id);
401 self.members.remove(id).is_some()
402 }
403
404 pub fn task_board(&self) -> &TeamTaskBoard {
406 &self.task_board
407 }
408
409 pub fn task_board_arc(&self) -> Arc<TeamTaskBoard> {
411 Arc::clone(&self.task_board)
412 }
413
414 pub async fn send_message(
416 &self,
417 from: &str,
418 to: &str,
419 content: &str,
420 task_id: Option<&str>,
421 ) -> bool {
422 let sender = match self.senders.get(to) {
423 Some(s) => s,
424 None => return false,
425 };
426
427 let msg = TeamMessage {
428 from: from.to_string(),
429 to: to.to_string(),
430 content: content.to_string(),
431 task_id: task_id.map(|s| s.to_string()),
432 timestamp: chrono::Utc::now().timestamp(),
433 };
434
435 sender.send(msg).await.is_ok()
436 }
437
438 pub fn take_receiver(&mut self, member_id: &str) -> Option<mpsc::Receiver<TeamMessage>> {
440 self.receivers.remove(member_id)
441 }
442
443 pub async fn broadcast(&self, from: &str, content: &str, task_id: Option<&str>) {
445 for (id, sender) in &self.senders {
446 if id == from {
447 continue;
448 }
449 let msg = TeamMessage {
450 from: from.to_string(),
451 to: id.clone(),
452 content: content.to_string(),
453 task_id: task_id.map(|s| s.to_string()),
454 timestamp: chrono::Utc::now().timestamp(),
455 };
456 let _ = sender.send(msg).await;
457 }
458 }
459
460 pub fn members(&self) -> Vec<&TeamMember> {
462 self.members.values().collect()
463 }
464
465 pub fn members_by_role(&self, role: TeamRole) -> Vec<&TeamMember> {
467 self.members.values().filter(|m| m.role == role).collect()
468 }
469
470 pub fn member_count(&self) -> usize {
472 self.members.len()
473 }
474}
475
476impl std::fmt::Debug for AgentTeam {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 f.debug_struct("AgentTeam")
479 .field("name", &self.name)
480 .field("members", &self.members.len())
481 .field("tasks", &self.task_board.len())
482 .finish()
483 }
484}
485
486#[async_trait::async_trait]
494pub trait AgentExecutor: Send + Sync {
495 async fn execute(&self, prompt: &str) -> crate::error::Result<String>;
497}
498
499#[async_trait::async_trait]
500impl AgentExecutor for crate::agent_api::AgentSession {
501 async fn execute(&self, prompt: &str) -> crate::error::Result<String> {
502 let result = self.send(prompt, None).await?;
503 Ok(result.text)
504 }
505}
506
507#[derive(Debug)]
513pub struct TeamRunResult {
514 pub done_tasks: Vec<TeamTask>,
516 pub rejected_tasks: Vec<TeamTask>,
518 pub rounds: usize,
520}
521
522const LEAD_PROMPT: &str = crate::prompts::TEAM_LEAD;
527
528const REVIEWER_PROMPT: &str = crate::prompts::TEAM_REVIEWER;
529
530#[derive(Debug, Default, Clone)]
533pub struct TeamMemberOptions {
534 pub workspace: Option<String>,
539 pub model: Option<String>,
541 pub prompt_slots: Option<crate::prompts::SystemPromptSlots>,
543 pub max_tool_rounds: Option<usize>,
545}
546
547impl TeamMemberOptions {
548 fn into_session_options(self) -> Option<crate::agent_api::SessionOptions> {
549 if self.model.is_none() && self.prompt_slots.is_none() && self.max_tool_rounds.is_none() {
550 return None;
551 }
552 let mut opts = crate::agent_api::SessionOptions::new();
553 if let Some(m) = self.model {
554 opts = opts.with_model(m);
555 }
556 if let Some(slots) = self.prompt_slots {
557 opts = opts.with_prompt_slots(slots);
558 }
559 if let Some(rounds) = self.max_tool_rounds {
560 opts = opts.with_max_tool_rounds(rounds);
561 }
562 Some(opts)
563 }
564}
565
566struct DefaultAgentContext {
570 agent: Arc<crate::agent_api::Agent>,
571 workspace: String,
572 registry: Arc<crate::subagent::AgentRegistry>,
573}
574
575pub struct TeamRunner {
578 team: AgentTeam,
579 sessions: HashMap<String, Arc<dyn AgentExecutor>>,
580 default_ctx: Option<DefaultAgentContext>,
581 worker_count: usize,
582}
583
584impl TeamRunner {
585 pub fn new(team: AgentTeam) -> Self {
587 Self {
588 team,
589 sessions: HashMap::new(),
590 default_ctx: None,
591 worker_count: 0,
592 }
593 }
594
595 pub fn with_agent(
602 team: AgentTeam,
603 agent: Arc<crate::agent_api::Agent>,
604 workspace: &str,
605 registry: Arc<crate::subagent::AgentRegistry>,
606 ) -> Self {
607 Self {
608 team,
609 sessions: HashMap::new(),
610 default_ctx: Some(DefaultAgentContext {
611 agent,
612 workspace: workspace.to_string(),
613 registry,
614 }),
615 worker_count: 0,
616 }
617 }
618
619 pub fn bind_session(
623 &mut self,
624 member_id: &str,
625 executor: Arc<dyn AgentExecutor>,
626 ) -> crate::error::Result<()> {
627 if !self.team.members.contains_key(member_id) {
628 return Err(anyhow::anyhow!(
629 "member '{}' not found in team '{}'",
630 member_id,
631 self.team.name()
632 )
633 .into());
634 }
635 self.sessions.insert(member_id.to_string(), executor);
636 Ok(())
637 }
638
639 pub fn bind_agent(
653 &mut self,
654 member_id: &str,
655 agent: &crate::agent_api::Agent,
656 workspace: &str,
657 agent_name: &str,
658 registry: &crate::subagent::AgentRegistry,
659 ) -> crate::error::Result<()> {
660 let def = registry
661 .get(agent_name)
662 .ok_or_else(|| anyhow::anyhow!("agent '{}' not found in registry", agent_name))?;
663 let session = agent.session_for_agent(workspace, &def, None)?;
664 self.bind_session(member_id, Arc::new(session))
665 }
666
667 fn create_session_from_default(
672 &self,
673 agent_name: &str,
674 member_opts: Option<TeamMemberOptions>,
675 ) -> crate::error::Result<crate::agent_api::AgentSession> {
676 let ctx = self.default_ctx.as_ref().ok_or_else(|| {
677 anyhow::anyhow!("no default agent context; use TeamRunner::with_agent")
678 })?;
679 let def = ctx
680 .registry
681 .get(agent_name)
682 .ok_or_else(|| anyhow::anyhow!("agent '{}' not found in registry", agent_name))?;
683 let workspace = member_opts
684 .as_ref()
685 .and_then(|o| o.workspace.clone())
686 .unwrap_or_else(|| ctx.workspace.clone());
687 let session_opts = member_opts.and_then(|o| o.into_session_options());
688 ctx.agent.session_for_agent(workspace, &def, session_opts)
689 }
690
691 pub fn add_lead(
703 &mut self,
704 agent_name: &str,
705 opts: Option<TeamMemberOptions>,
706 ) -> crate::error::Result<()> {
707 let session = self.create_session_from_default(agent_name, opts)?;
708 self.team.add_member("lead", TeamRole::Lead);
709 self.sessions.insert("lead".to_string(), Arc::new(session));
710 Ok(())
711 }
712
713 pub fn add_worker(
728 &mut self,
729 agent_name: &str,
730 opts: Option<TeamMemberOptions>,
731 ) -> crate::error::Result<()> {
732 self.worker_count += 1;
733 let id = format!("worker-{}", self.worker_count);
734 let session = self.create_session_from_default(agent_name, opts)?;
735 self.team.add_member(&id, TeamRole::Worker);
736 self.sessions.insert(id, Arc::new(session));
737 Ok(())
738 }
739
740 pub fn add_reviewer(
752 &mut self,
753 agent_name: &str,
754 opts: Option<TeamMemberOptions>,
755 ) -> crate::error::Result<()> {
756 let session = self.create_session_from_default(agent_name, opts)?;
757 self.team.add_member("reviewer", TeamRole::Reviewer);
758 self.sessions
759 .insert("reviewer".to_string(), Arc::new(session));
760 Ok(())
761 }
762
763 pub fn team_mut(&mut self) -> &mut AgentTeam {
765 &mut self.team
766 }
767
768 pub fn task_board(&self) -> Arc<TeamTaskBoard> {
770 self.team.task_board_arc()
771 }
772
773 pub async fn run_until_done(&self, goal: &str) -> crate::error::Result<TeamRunResult> {
786 let lead = self
788 .team
789 .members_by_role(TeamRole::Lead)
790 .into_iter()
791 .next()
792 .ok_or_else(|| anyhow::anyhow!("team has no Lead member"))?;
793
794 let lead_executor = self
795 .sessions
796 .get(&lead.id)
797 .ok_or_else(|| anyhow::anyhow!("no executor bound for lead member '{}'", lead.id))?;
798
799 let lead_prompt = LEAD_PROMPT.replace("{goal}", goal);
800 let raw = lead_executor.execute(&lead_prompt).await?;
801 let task_descriptions = parse_task_list(&raw)?;
802
803 let board = self.team.task_board_arc();
804 for desc in &task_descriptions {
805 board.post(desc, &lead.id, None);
806 }
807
808 let poll = Duration::from_millis(self.team.config.poll_interval_ms);
816 let max_rounds = self.team.config.max_rounds;
817
818 let workers: Vec<(String, Arc<dyn AgentExecutor>)> = self
819 .team
820 .members_by_role(TeamRole::Worker)
821 .into_iter()
822 .filter_map(|m| {
823 self.sessions
824 .get(&m.id)
825 .map(|e| (m.id.clone(), Arc::clone(e)))
826 })
827 .collect();
828
829 let reviewer: Option<(String, Arc<dyn AgentExecutor>)> = self
830 .team
831 .members_by_role(TeamRole::Reviewer)
832 .into_iter()
833 .next()
834 .and_then(|m| {
835 self.sessions
836 .get(&m.id)
837 .map(|e| (m.id.clone(), Arc::clone(e)))
838 });
839
840 let mut total_reviewer_rounds = 0usize;
841
842 for _cycle in 0..max_rounds {
843 let mut worker_handles = Vec::new();
845 for (id, executor) in &workers {
846 let b = Arc::clone(&board);
847 let id = id.clone();
848 let executor = Arc::clone(executor);
849 let handle = tokio::spawn(async move {
850 run_worker(id, executor, b, max_rounds, poll).await;
851 });
852 worker_handles.push(handle);
853 }
854 for h in worker_handles {
855 let _ = h.await;
856 }
857
858 if let Some((ref id, ref executor)) = reviewer {
860 let rounds = run_reviewer(
861 id.clone(),
862 Arc::clone(executor),
863 Arc::clone(&board),
864 max_rounds,
865 poll,
866 )
867 .await;
868 total_reviewer_rounds += rounds;
869 }
870
871 let (open, in_progress, in_review, _, rejected) = board.stats();
873 if open == 0 && in_progress == 0 && in_review == 0 && rejected == 0 {
874 break;
875 }
876 if rejected == 0 {
877 break;
878 }
879 }
880
881 let done_tasks = board.by_status(TaskStatus::Done);
882 let rejected_tasks = board.by_status(TaskStatus::Rejected);
883
884 Ok(TeamRunResult {
885 done_tasks,
886 rejected_tasks,
887 rounds: total_reviewer_rounds,
888 })
889 }
890}
891
892impl std::fmt::Debug for TeamRunner {
893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894 f.debug_struct("TeamRunner")
895 .field("team", &self.team.name())
896 .field("bound_sessions", &self.sessions.len())
897 .finish()
898 }
899}
900
901fn parse_task_list(response: &str) -> crate::error::Result<Vec<String>> {
903 let start = response
905 .find('{')
906 .ok_or_else(|| anyhow::anyhow!("lead response contains no JSON object: {}", response))?;
907 let end = response
908 .rfind('}')
909 .ok_or_else(|| anyhow::anyhow!("lead response JSON object is unclosed"))?;
910 let json_str = &response[start..=end];
911
912 let value: serde_json::Value = serde_json::from_str(json_str)
913 .map_err(|e| anyhow::anyhow!("failed to parse lead JSON response: {e}"))?;
914
915 let tasks: Vec<String> = value["tasks"]
916 .as_array()
917 .ok_or_else(|| anyhow::anyhow!("lead JSON response missing 'tasks' array"))?
918 .iter()
919 .filter_map(|v: &serde_json::Value| v.as_str().map(|s| s.to_string()))
920 .collect();
921
922 Ok(tasks)
923}
924
925async fn run_worker(
927 member_id: String,
928 executor: Arc<dyn AgentExecutor>,
929 board: Arc<TeamTaskBoard>,
930 max_rounds: usize,
931 poll: Duration,
932) {
933 let mut idle = 0usize;
934 loop {
935 if let Some(task) = board.claim(&member_id) {
936 idle = 0;
937 let result = executor
938 .execute(&task.description)
939 .await
940 .unwrap_or_else(|e| format!("execution error: {e}"));
941 board.complete(&task.id, &result);
942 } else {
943 let (open, in_progress, in_review, _, rejected) = board.stats();
944 if open == 0 && in_progress == 0 && in_review == 0 && rejected == 0 {
948 break;
949 }
950 idle += 1;
951 if idle >= max_rounds {
952 break;
953 }
954 tokio::time::sleep(poll).await;
955 }
956 }
957}
958
959async fn run_reviewer(
962 _member_id: String,
963 executor: Arc<dyn AgentExecutor>,
964 board: Arc<TeamTaskBoard>,
965 max_rounds: usize,
966 poll: Duration,
967) -> usize {
968 let mut rounds = 0usize;
969 loop {
970 let in_review = board.by_status(TaskStatus::InReview);
971 for task in in_review {
972 let result_text = task.result.as_deref().unwrap_or("");
973 let prompt = REVIEWER_PROMPT
974 .replace("{task}", &task.description)
975 .replace("{result}", result_text);
976 let verdict = executor
977 .execute(&prompt)
978 .await
979 .unwrap_or_else(|_| "REJECTED: execution error".to_string());
980 if verdict.contains("APPROVED") {
981 board.approve(&task.id);
982 } else {
983 board.reject(&task.id);
984 }
985 tokio::task::yield_now().await;
987 }
988
989 let (open, in_progress, in_review_count, _, rejected) = board.stats();
990 if open == 0 && in_progress == 0 && in_review_count == 0 && rejected == 0 {
991 break;
992 }
993 rounds += 1;
994 if rounds >= max_rounds {
995 break;
996 }
997 tokio::time::sleep(poll).await;
998 }
999 rounds
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn test_team_creation() {
1008 let team = AgentTeam::new("test-team", TeamConfig::default());
1009 assert_eq!(team.name(), "test-team");
1010 assert_eq!(team.member_count(), 0);
1011 }
1012
1013 #[test]
1014 fn test_add_remove_members() {
1015 let mut team = AgentTeam::new("test", TeamConfig::default());
1016 team.add_member("lead", TeamRole::Lead);
1017 team.add_member("w1", TeamRole::Worker);
1018 team.add_member("w2", TeamRole::Worker);
1019 team.add_member("rev", TeamRole::Reviewer);
1020 assert_eq!(team.member_count(), 4);
1021 assert_eq!(team.members_by_role(TeamRole::Worker).len(), 2);
1022
1023 assert!(team.remove_member("w2"));
1024 assert_eq!(team.member_count(), 3);
1025 assert!(!team.remove_member("nonexistent"));
1026 }
1027
1028 #[test]
1029 fn test_task_board_post_and_claim() {
1030 let board = TeamTaskBoard::new(10);
1031 let id = board.post("Fix auth bug", "lead", None).unwrap();
1032 assert_eq!(board.len(), 1);
1033
1034 let task = board.claim("worker-1").unwrap();
1035 assert_eq!(task.id, id);
1036 assert_eq!(task.assigned_to.as_deref(), Some("worker-1"));
1037 assert_eq!(task.status, TaskStatus::InProgress);
1038
1039 assert!(board.claim("worker-2").is_none());
1041 }
1042
1043 #[test]
1044 fn test_task_board_workflow() {
1045 let board = TeamTaskBoard::new(10);
1046 let id = board.post("Write tests", "lead", None).unwrap();
1047
1048 board.claim("worker-1");
1050
1051 assert!(board.complete(&id, "Added 5 tests"));
1053 let task = board.get(&id).unwrap();
1054 assert_eq!(task.status, TaskStatus::InReview);
1055
1056 assert!(board.approve(&id));
1058 let task = board.get(&id).unwrap();
1059 assert_eq!(task.status, TaskStatus::Done);
1060 }
1061
1062 #[test]
1063 fn test_task_board_reject() {
1064 let board = TeamTaskBoard::new(10);
1065 let id = board.post("Refactor module", "lead", None).unwrap();
1066 board.claim("worker-1");
1067 board.complete(&id, "Done");
1068
1069 assert!(board.reject(&id));
1070 let task = board.get(&id).unwrap();
1071 assert_eq!(task.status, TaskStatus::Rejected);
1072 assert!(task.assigned_to.is_none());
1073 }
1074
1075 #[test]
1076 fn test_task_board_max_capacity() {
1077 let board = TeamTaskBoard::new(2);
1078 assert!(board.post("Task 1", "lead", None).is_some());
1079 assert!(board.post("Task 2", "lead", None).is_some());
1080 assert!(board.post("Task 3", "lead", None).is_none()); }
1082
1083 #[test]
1084 fn test_task_board_stats() {
1085 let board = TeamTaskBoard::new(10);
1086 board.post("T1", "lead", None);
1087 board.post("T2", "lead", None);
1088 let id3 = board.post("T3", "lead", Some("w1")).unwrap();
1089 board.complete(&id3, "done");
1090
1091 let (open, progress, review, done, rejected) = board.stats();
1092 assert_eq!(open, 2);
1093 assert_eq!(progress, 0);
1094 assert_eq!(review, 1);
1095 assert_eq!(done, 0);
1096 assert_eq!(rejected, 0);
1097 }
1098
1099 #[test]
1100 fn test_task_board_by_assignee() {
1101 let board = TeamTaskBoard::new(10);
1102 board.post("T1", "lead", Some("w1"));
1103 board.post("T2", "lead", Some("w2"));
1104 board.post("T3", "lead", Some("w1"));
1105
1106 let w1_tasks = board.by_assignee("w1");
1107 assert_eq!(w1_tasks.len(), 2);
1108 }
1109
1110 #[tokio::test]
1111 async fn test_send_message() {
1112 let mut team = AgentTeam::new("msg-test", TeamConfig::default());
1113 team.add_member("lead", TeamRole::Lead);
1114 team.add_member("worker", TeamRole::Worker);
1115
1116 let mut rx = team.take_receiver("worker").unwrap();
1117
1118 assert!(
1119 team.send_message("lead", "worker", "Please fix the bug", Some("task-1"))
1120 .await
1121 );
1122
1123 let msg = rx.recv().await.unwrap();
1124 assert_eq!(msg.from, "lead");
1125 assert_eq!(msg.to, "worker");
1126 assert_eq!(msg.content, "Please fix the bug");
1127 assert_eq!(msg.task_id.as_deref(), Some("task-1"));
1128 }
1129
1130 #[tokio::test]
1131 async fn test_broadcast() {
1132 let mut team = AgentTeam::new("broadcast-test", TeamConfig::default());
1133 team.add_member("lead", TeamRole::Lead);
1134 team.add_member("w1", TeamRole::Worker);
1135 team.add_member("w2", TeamRole::Worker);
1136
1137 let mut rx1 = team.take_receiver("w1").unwrap();
1138 let mut rx2 = team.take_receiver("w2").unwrap();
1139
1140 team.broadcast("lead", "New task available", None).await;
1141
1142 let m1 = rx1.recv().await.unwrap();
1143 let m2 = rx2.recv().await.unwrap();
1144 assert_eq!(m1.content, "New task available");
1145 assert_eq!(m2.content, "New task available");
1146 }
1147
1148 #[test]
1149 fn test_role_display() {
1150 assert_eq!(TeamRole::Lead.to_string(), "lead");
1151 assert_eq!(TeamRole::Worker.to_string(), "worker");
1152 assert_eq!(TeamRole::Reviewer.to_string(), "reviewer");
1153 }
1154
1155 #[test]
1156 fn test_task_status_display() {
1157 assert_eq!(TaskStatus::Open.to_string(), "open");
1158 assert_eq!(TaskStatus::InProgress.to_string(), "in_progress");
1159 assert_eq!(TaskStatus::InReview.to_string(), "in_review");
1160 assert_eq!(TaskStatus::Done.to_string(), "done");
1161 assert_eq!(TaskStatus::Rejected.to_string(), "rejected");
1162 }
1163
1164 struct MockExecutor {
1170 response: String,
1171 }
1172
1173 impl MockExecutor {
1174 fn new(response: impl Into<String>) -> Arc<Self> {
1175 Arc::new(Self {
1176 response: response.into(),
1177 })
1178 }
1179 }
1180
1181 #[async_trait::async_trait]
1182 impl AgentExecutor for MockExecutor {
1183 async fn execute(&self, _prompt: &str) -> crate::error::Result<String> {
1184 Ok(self.response.clone())
1185 }
1186 }
1187
1188 #[test]
1189 fn test_team_runner_session_binding() {
1190 let mut team = AgentTeam::new("bind-test", TeamConfig::default());
1191 team.add_member("lead", TeamRole::Lead);
1192 team.add_member("w1", TeamRole::Worker);
1193
1194 let mut runner = TeamRunner::new(team);
1195
1196 assert!(runner.bind_session("lead", MockExecutor::new("ok")).is_ok());
1198 assert!(runner.bind_session("w1", MockExecutor::new("ok")).is_ok());
1199
1200 assert!(runner
1202 .bind_session("nobody", MockExecutor::new("ok"))
1203 .is_err());
1204 }
1205
1206 #[test]
1207 fn test_parse_task_list() {
1208 let json = r#"{"tasks": ["Write tests", "Fix lints", "Update docs"]}"#;
1209 let tasks = parse_task_list(json).unwrap();
1210 assert_eq!(tasks.len(), 3);
1211 assert_eq!(tasks[0], "Write tests");
1212 assert_eq!(tasks[2], "Update docs");
1213 }
1214
1215 #[test]
1216 fn test_parse_task_list_no_json() {
1217 assert!(parse_task_list("no json here").is_err());
1218 }
1219
1220 #[test]
1221 fn test_claim_rejected_tasks() {
1222 let board = TeamTaskBoard::new(10);
1223 let id = board.post("Refactor module", "lead", None).unwrap();
1224
1225 board.claim("worker-1");
1227 board.complete(&id, "initial attempt");
1228 board.reject(&id);
1229
1230 assert_eq!(board.get(&id).unwrap().status, TaskStatus::Rejected);
1231
1232 let task = board.claim("worker-2");
1234 assert!(task.is_some());
1235 let task = task.unwrap();
1236 assert_eq!(task.id, id);
1237 assert_eq!(task.assigned_to.as_deref(), Some("worker-2"));
1238 assert_eq!(task.status, TaskStatus::InProgress);
1239 }
1240
1241 #[tokio::test]
1242 async fn test_team_runner_goal_decomposition() {
1243 let config = TeamConfig {
1244 poll_interval_ms: 1,
1245 max_rounds: 3,
1246 ..TeamConfig::default()
1247 };
1248 let mut team = AgentTeam::new("decomp-test", config);
1249 team.add_member("lead", TeamRole::Lead);
1250 team.add_member("w1", TeamRole::Worker);
1251 team.add_member("rev", TeamRole::Reviewer);
1252
1253 let mut runner = TeamRunner::new(team);
1254 runner
1256 .bind_session(
1257 "lead",
1258 MockExecutor::new(r#"{"tasks": ["Task A", "Task B"]}"#),
1259 )
1260 .unwrap();
1261 runner
1263 .bind_session("w1", MockExecutor::new("done"))
1264 .unwrap();
1265 runner
1267 .bind_session("rev", MockExecutor::new("APPROVED: looks good"))
1268 .unwrap();
1269
1270 let result = runner.run_until_done("Build the feature").await.unwrap();
1271
1272 assert_eq!(result.done_tasks.len(), 2);
1273 assert!(result.rejected_tasks.is_empty());
1274 }
1275
1276 #[tokio::test]
1277 async fn test_team_runner_worker_execution() {
1278 let config = TeamConfig {
1279 poll_interval_ms: 1,
1280 max_rounds: 3,
1281 ..TeamConfig::default()
1282 };
1283 let mut team = AgentTeam::new("worker-exec-test", config);
1284 team.add_member("lead", TeamRole::Lead);
1285 team.add_member("w1", TeamRole::Worker);
1286
1287 let mut runner = TeamRunner::new(team);
1288 runner
1289 .bind_session(
1290 "lead",
1291 MockExecutor::new(r#"{"tasks": ["Write unit tests"]}"#),
1292 )
1293 .unwrap();
1294 runner
1295 .bind_session("w1", MockExecutor::new("Added 3 tests"))
1296 .unwrap();
1297
1298 let board = runner.task_board();
1300 let _ = runner.run_until_done("Test the module").await;
1301
1302 let tasks = board.by_status(TaskStatus::InReview);
1304 assert_eq!(tasks.len(), 1);
1305 assert_eq!(tasks[0].result.as_deref(), Some("Added 3 tests"));
1306 }
1307
1308 #[tokio::test]
1309 async fn test_team_runner_reviewer_approval() {
1310 let config = TeamConfig {
1311 poll_interval_ms: 1,
1312 max_rounds: 5,
1313 ..TeamConfig::default()
1314 };
1315 let mut team = AgentTeam::new("reviewer-test", config);
1316 team.add_member("lead", TeamRole::Lead);
1317 team.add_member("w1", TeamRole::Worker);
1318 team.add_member("rev", TeamRole::Reviewer);
1319
1320 let mut runner = TeamRunner::new(team);
1321 runner
1322 .bind_session(
1323 "lead",
1324 MockExecutor::new(r#"{"tasks": ["Implement feature X"]}"#),
1325 )
1326 .unwrap();
1327 runner
1328 .bind_session("w1", MockExecutor::new("Feature X implemented"))
1329 .unwrap();
1330 runner
1331 .bind_session("rev", MockExecutor::new("APPROVED: complete"))
1332 .unwrap();
1333
1334 let result = runner.run_until_done("Ship feature X").await.unwrap();
1335
1336 assert_eq!(result.done_tasks.len(), 1);
1337 assert_eq!(
1338 result.done_tasks[0].result.as_deref(),
1339 Some("Feature X implemented")
1340 );
1341 }
1342
1343 #[tokio::test]
1344 async fn test_team_runner_rejection_and_retry() {
1345 use std::sync::atomic::{AtomicUsize, Ordering};
1346
1347 struct ConditionalReviewer {
1349 calls: AtomicUsize,
1350 }
1351
1352 #[async_trait::async_trait]
1353 impl AgentExecutor for ConditionalReviewer {
1354 async fn execute(&self, _prompt: &str) -> crate::error::Result<String> {
1355 let n = self.calls.fetch_add(1, Ordering::SeqCst);
1356 if n == 0 {
1357 Ok("REJECTED: needs improvement".to_string())
1358 } else {
1359 Ok("APPROVED: now correct".to_string())
1360 }
1361 }
1362 }
1363
1364 let config = TeamConfig {
1365 poll_interval_ms: 1,
1366 max_rounds: 10,
1367 ..TeamConfig::default()
1368 };
1369 let mut team = AgentTeam::new("retry-test", config);
1370 team.add_member("lead", TeamRole::Lead);
1371 team.add_member("w1", TeamRole::Worker);
1372 team.add_member("rev", TeamRole::Reviewer);
1373
1374 let mut runner = TeamRunner::new(team);
1375 runner
1376 .bind_session("lead", MockExecutor::new(r#"{"tasks": ["Do the thing"]}"#))
1377 .unwrap();
1378 runner
1379 .bind_session("w1", MockExecutor::new("attempt result"))
1380 .unwrap();
1381 runner
1382 .bind_session(
1383 "rev",
1384 Arc::new(ConditionalReviewer {
1385 calls: AtomicUsize::new(0),
1386 }),
1387 )
1388 .unwrap();
1389
1390 let result = runner.run_until_done("Complete the thing").await.unwrap();
1391
1392 assert_eq!(result.done_tasks.len(), 1);
1394 assert!(result.rejected_tasks.is_empty());
1395 }
1396}