1use crate::agent::PlanningMode;
4use crate::kernel::{AgentKernel, TurnContext, TurnOutcome};
5use crate::message::Message;
6use crate::permissions::PermissionMode;
7use crate::{Config, LlmProvider};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::{SystemTime, UNIX_EPOCH};
12use tokio::sync::{broadcast, RwLock};
13
14#[derive(Clone)]
16pub struct SharedMemory {
17 store: Arc<RwLock<HashMap<String, MemoryEntry>>>,
18}
19
20#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
22pub struct MemoryEntry {
23 pub key: String,
24 pub value: String,
25 pub author: String,
26 pub timestamp: u64,
27}
28
29impl SharedMemory {
30 pub fn new() -> Self {
31 Self {
32 store: Arc::new(RwLock::new(HashMap::new())),
33 }
34 }
35
36 pub async fn set(&self, key: String, value: String, author: String) {
37 let timestamp = SystemTime::now()
38 .duration_since(UNIX_EPOCH)
39 .unwrap_or_default()
40 .as_secs();
41 let entry = MemoryEntry {
42 key: key.clone(),
43 value,
44 author,
45 timestamp,
46 };
47 self.store.write().await.insert(key, entry);
48 }
49
50 pub async fn get(&self, key: &str) -> Option<MemoryEntry> {
51 self.store.read().await.get(key).cloned()
52 }
53
54 pub async fn keys(&self) -> Vec<String> {
55 self.store.read().await.keys().cloned().collect()
56 }
57
58 pub async fn all(&self) -> Vec<MemoryEntry> {
59 self.store.read().await.values().cloned().collect()
60 }
61
62 pub async fn remove(&self, key: &str) -> bool {
63 self.store.write().await.remove(key).is_some()
64 }
65
66 pub async fn to_context_string(&self) -> String {
67 let store = self.store.read().await;
68 if store.is_empty() {
69 return String::new();
70 }
71 let mut lines = vec!["[Shared Memory]".to_string()];
72 for entry in store.values() {
73 lines.push(format!(
74 "- {} = {} (by {})",
75 entry.key, entry.value, entry.author
76 ));
77 }
78 lines.join("\n")
79 }
80
81 pub async fn len(&self) -> usize {
82 self.store.read().await.len()
83 }
84
85 pub async fn is_empty(&self) -> bool {
86 self.store.read().await.is_empty()
87 }
88}
89
90impl Default for SharedMemory {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
100pub enum MessageType {
101 Task,
102 Result,
103 Question,
104 Feedback,
105 Broadcast,
106}
107
108#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
110pub struct AgentMessage {
111 pub id: String,
112 pub from: String,
113 pub to: String,
114 pub content: String,
115 pub msg_type: MessageType,
116 pub timestamp: u64,
117}
118
119fn generate_message_id() -> String {
121 static COUNTER: AtomicU64 = AtomicU64::new(0);
122
123 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
124 let now = SystemTime::now()
125 .duration_since(UNIX_EPOCH)
126 .unwrap_or_default();
127 let input = format!("msg-{}-{}", now.as_nanos(), count);
128 let hash = blake3::hash(input.as_bytes());
129 hash.to_hex()[..16].to_string()
130}
131
132fn now_timestamp() -> u64 {
134 SystemTime::now()
135 .duration_since(UNIX_EPOCH)
136 .unwrap_or_default()
137 .as_secs()
138}
139
140#[derive(Clone)]
142pub struct MessageBus {
143 messages: Arc<RwLock<Vec<AgentMessage>>>,
144 subscribers: Arc<RwLock<HashMap<String, broadcast::Sender<AgentMessage>>>>,
145}
146
147impl MessageBus {
148 pub fn new() -> Self {
149 Self {
150 messages: Arc::new(RwLock::new(Vec::new())),
151 subscribers: Arc::new(RwLock::new(HashMap::new())),
152 }
153 }
154
155 pub async fn send(&self, msg: AgentMessage) {
157 self.messages.write().await.push(msg.clone());
158 let subs = self.subscribers.read().await;
159 if msg.to == "broadcast" {
160 for tx in subs.values() {
161 let _ = tx.send(msg.clone());
162 }
163 } else if let Some(tx) = subs.get(&msg.to) {
164 let _ = tx.send(msg);
165 }
166 }
167
168 pub async fn subscribe(&self, role: &str) -> broadcast::Receiver<AgentMessage> {
170 let mut subs = self.subscribers.write().await;
171 let tx = subs.entry(role.to_string()).or_insert_with(|| {
172 let (tx, _) = broadcast::channel(64);
173 tx
174 });
175 tx.subscribe()
176 }
177
178 pub async fn inbox(&self, role: &str) -> Vec<AgentMessage> {
180 self.messages
181 .read()
182 .await
183 .iter()
184 .filter(|m| m.to == role || m.to == "broadcast")
185 .cloned()
186 .collect()
187 }
188
189 pub async fn outbox(&self, role: &str) -> Vec<AgentMessage> {
191 self.messages
192 .read()
193 .await
194 .iter()
195 .filter(|m| m.from == role)
196 .cloned()
197 .collect()
198 }
199
200 pub async fn history(&self) -> Vec<AgentMessage> {
202 self.messages.read().await.clone()
203 }
204
205 pub async fn clear(&self) {
207 self.messages.write().await.clear();
208 }
209}
210
211impl Default for MessageBus {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217#[derive(Clone, Debug)]
219pub struct AgentRole {
220 pub name: String,
221 pub system_prompt: String,
222 pub max_steps: usize,
223 pub allowed_tools: Vec<String>,
224}
225
226pub struct AgentPool {
228 roles: HashMap<String, AgentRole>,
229 provider: Arc<dyn LlmProvider>,
230 #[allow(dead_code)]
231 config: Config,
232 memory: SharedMemory,
233 bus: MessageBus,
234}
235
236impl AgentPool {
237 pub fn new(provider: Arc<dyn LlmProvider>, config: Config) -> Self {
238 Self {
239 roles: HashMap::new(),
240 provider,
241 config,
242 memory: SharedMemory::new(),
243 bus: MessageBus::new(),
244 }
245 }
246
247 pub fn memory(&self) -> &SharedMemory {
248 &self.memory
249 }
250
251 pub fn bus(&self) -> &MessageBus {
252 &self.bus
253 }
254
255 pub fn add_role(&mut self, role: AgentRole) {
256 self.roles.insert(role.name.clone(), role);
257 }
258
259 pub fn get_role(&self, name: &str) -> Option<&AgentRole> {
260 self.roles.get(name)
261 }
262
263 pub fn role_names(&self) -> Vec<&str> {
264 self.roles.keys().map(|s| s.as_str()).collect()
265 }
266
267 pub fn role_count(&self) -> usize {
268 self.roles.len()
269 }
270
271 pub fn remove_role(&mut self, name: &str) -> bool {
273 self.roles.remove(name).is_some()
274 }
275
276 pub async fn run_with_role(
277 &self,
278 role_name: &str,
279 goal: &str,
280 ) -> Result<TurnOutcome, crate::Error> {
281 let role = self
282 .roles
283 .get(role_name)
284 .ok_or_else(|| crate::Error::Config {
285 message: format!("unknown role: {role_name}"),
286 })?;
287
288 let memory_ctx = self.memory.to_context_string().await;
289 let system_prompt = if memory_ctx.is_empty() {
290 role.system_prompt.clone()
291 } else {
292 format!("{}\n\n{}", role.system_prompt, memory_ctx)
293 };
294
295 let kernel = AgentKernel::builder()
296 .llm(self.provider.clone())
297 .max_steps(role.max_steps)
298 .build()?;
299
300 let ctx = TurnContext {
301 messages: vec![
302 Message::system(system_prompt),
303 Message::user(goal.to_string()),
304 ],
305 step_events_tx: None,
306 plan_confirmed: false,
307 plan_buffer: None,
308 tool_specs: kernel.tools().specs(),
309 streaming: false,
310 permission_hook: None,
311 planning_mode: PlanningMode::default(),
312 exploring_plan_mode: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
313 permission_mode: PermissionMode::Default,
314 mailbox: None,
315 };
316
317 kernel.run(ctx).await
318 }
319
320 pub async fn send_task(&self, from: &str, to: &str, content: &str) {
322 self.bus
323 .send(AgentMessage {
324 id: generate_message_id(),
325 from: from.to_string(),
326 to: to.to_string(),
327 content: content.to_string(),
328 msg_type: MessageType::Task,
329 timestamp: now_timestamp(),
330 })
331 .await;
332 }
333
334 pub async fn send_result(&self, from: &str, to: &str, content: &str) {
336 self.bus
337 .send(AgentMessage {
338 id: generate_message_id(),
339 from: from.to_string(),
340 to: to.to_string(),
341 content: content.to_string(),
342 msg_type: MessageType::Result,
343 timestamp: now_timestamp(),
344 })
345 .await;
346 }
347}
348
349pub struct Pipeline {
351 stages: Vec<String>,
352}
353
354impl Pipeline {
355 pub fn new(stages: Vec<String>) -> Self {
356 Self { stages }
357 }
358
359 pub async fn execute(
361 &self,
362 pool: &AgentPool,
363 initial_goal: &str,
364 ) -> Result<PipelineResult, crate::Error> {
365 let mut current_input = initial_goal.to_string();
366 let mut stage_outcomes = Vec::new();
367
368 for role_name in &self.stages {
369 let outcome = pool.run_with_role(role_name, ¤t_input).await?;
370
371 let output = outcome
372 .new_messages
373 .iter()
374 .rev()
375 .find(|m| m.role == crate::message::Role::Assistant)
376 .map(|m| m.content.clone())
377 .unwrap_or_default();
378
379 stage_outcomes.push(StageOutcome {
380 role: role_name.clone(),
381 output: output.clone(),
382 steps: outcome.steps,
383 });
384
385 current_input = output;
386 }
387
388 Ok(PipelineResult {
389 stages: stage_outcomes,
390 })
391 }
392}
393
394#[derive(Debug)]
396pub struct PipelineResult {
397 pub stages: Vec<StageOutcome>,
398}
399
400#[derive(Debug)]
402pub struct StageOutcome {
403 pub role: String,
404 pub output: String,
405 pub steps: usize,
406}
407
408impl PipelineResult {
409 pub fn final_output(&self) -> &str {
410 self.stages.last().map(|s| s.output.as_str()).unwrap_or("")
411 }
412 pub fn stage_count(&self) -> usize {
413 self.stages.len()
414 }
415}
416
417pub struct TeamOrchestrator {
420 lead_role: String,
421 available_roles: Vec<String>,
422}
423
424impl TeamOrchestrator {
425 pub fn new(lead_role: String, available_roles: Vec<String>) -> Self {
426 Self {
427 lead_role,
428 available_roles,
429 }
430 }
431
432 pub async fn run(&self, pool: &AgentPool, goal: &str) -> Result<TeamResult, crate::Error> {
434 let delegation_prompt = format!(
436 "{}\n\nAvailable specialists: {}\n\nTo delegate, use: DELEGATE:<role>:<task>\nWhen done, provide your final answer.",
437 goal,
438 self.available_roles.join(", ")
439 );
440
441 let lead_outcome = pool
442 .run_with_role(&self.lead_role, &delegation_prompt)
443 .await?;
444 let lead_response = lead_outcome
445 .new_messages
446 .iter()
447 .rev()
448 .find(|m| m.role == crate::message::Role::Assistant)
449 .map(|m| m.content.clone())
450 .unwrap_or_default();
451
452 let delegations = parse_delegations(&lead_response);
453
454 let mut delegation_results = Vec::new();
456 for (role, task) in &delegations {
457 if self.available_roles.contains(role) {
458 match pool.run_with_role(role, task).await {
459 Ok(outcome) => {
460 let result = outcome
461 .new_messages
462 .iter()
463 .rev()
464 .find(|m| m.role == crate::message::Role::Assistant)
465 .map(|m| m.content.clone())
466 .unwrap_or_default();
467 delegation_results.push(DelegationResult {
468 role: role.clone(),
469 task: task.clone(),
470 output: result,
471 success: true,
472 });
473 }
474 Err(e) => {
475 delegation_results.push(DelegationResult {
476 role: role.clone(),
477 task: task.clone(),
478 output: format!("Error: {e}"),
479 success: false,
480 });
481 }
482 }
483 }
484 }
485
486 let final_output = if delegation_results.is_empty() {
488 lead_response
489 } else {
490 let results_summary = delegation_results
491 .iter()
492 .map(|r| {
493 format!(
494 "- {} ({}): {}",
495 r.role,
496 if r.success { "ok" } else { "failed" },
497 r.output
498 )
499 })
500 .collect::<Vec<_>>()
501 .join("\n");
502
503 let synthesis_prompt = format!(
504 "Results from delegated tasks:\n\n{}\n\nProvide a final synthesis.",
505 results_summary
506 );
507
508 let synthesis = pool
509 .run_with_role(&self.lead_role, &synthesis_prompt)
510 .await?;
511 synthesis
512 .new_messages
513 .iter()
514 .rev()
515 .find(|m| m.role == crate::message::Role::Assistant)
516 .map(|m| m.content.clone())
517 .unwrap_or_default()
518 };
519
520 Ok(TeamResult {
521 delegations: delegation_results,
522 final_output,
523 })
524 }
525}
526
527pub fn parse_delegations(text: &str) -> Vec<(String, String)> {
529 text.lines()
530 .filter_map(|line| {
531 let trimmed = line.trim();
532 if let Some(rest) = trimmed.strip_prefix("DELEGATE:") {
533 let parts: Vec<&str> = rest.splitn(2, ':').collect();
534 if parts.len() == 2 {
535 Some((parts[0].to_string(), parts[1].to_string()))
536 } else {
537 None
538 }
539 } else {
540 None
541 }
542 })
543 .collect()
544}
545
546#[derive(Debug)]
548pub struct TeamResult {
549 pub delegations: Vec<DelegationResult>,
550 pub final_output: String,
551}
552
553#[derive(Debug)]
555pub struct DelegationResult {
556 pub role: String,
557 pub task: String,
558 pub output: String,
559 pub success: bool,
560}
561
562pub fn default_roles() -> Vec<AgentRole> {
564 vec![
565 AgentRole {
566 name: "planner".into(),
567 system_prompt: "You are a planning agent. Analyze the task, break it into steps, \
568 and output a structured plan. Do not execute — only plan."
569 .into(),
570 max_steps: 10,
571 allowed_tools: vec![],
572 },
573 AgentRole {
574 name: "coder".into(),
575 system_prompt: "You are a coding agent. Implement the task using the available \
576 tools. Write code, run tests, fix errors."
577 .into(),
578 max_steps: 50,
579 allowed_tools: vec![],
580 },
581 AgentRole {
582 name: "reviewer".into(),
583 system_prompt: "You are a code review agent. Read the code changes, identify \
584 issues, suggest improvements. Do not modify files."
585 .into(),
586 max_steps: 20,
587 allowed_tools: vec!["read_file".into(), "search_files".into()],
588 },
589 ]
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::llm::{Completion, MockProvider};
596 use std::path::PathBuf;
597
598 fn test_config() -> Config {
599 Config {
600 workspace: PathBuf::from("."),
601 api_base: String::new(),
602 api_key: None,
603 model: String::new(),
604 provider_type: "openai".into(),
605 preset: None,
606 max_steps: 32,
607 temperature: 0.2,
608 system_prompt: String::new(),
609 retry_max: 2,
610 retry_initial_backoff_secs: 1,
611 retry_max_backoff_secs: 8,
612 shell_timeout_secs: 300,
613 headless: false,
614 memory_summary_limit: 5,
615 thinking_budget: None,
616 session_name: None,
617 }
618 }
619
620 #[test]
621 fn new_pool_is_empty() {
622 let provider = Arc::new(MockProvider::new(vec![]));
623 let pool = AgentPool::new(provider, test_config());
624 assert_eq!(pool.role_count(), 0);
625 }
626
627 #[test]
628 fn add_role_and_get_role() {
629 let provider = Arc::new(MockProvider::new(vec![]));
630 let mut pool = AgentPool::new(provider, test_config());
631
632 let role = AgentRole {
633 name: "tester".into(),
634 system_prompt: "You test things.".into(),
635 max_steps: 5,
636 allowed_tools: vec!["run_shell".into()],
637 };
638 pool.add_role(role.clone());
639
640 let retrieved = pool.get_role("tester").unwrap();
641 assert_eq!(retrieved.name, "tester");
642 assert_eq!(retrieved.system_prompt, "You test things.");
643 assert_eq!(retrieved.max_steps, 5);
644 assert_eq!(retrieved.allowed_tools, vec!["run_shell"]);
645 }
646
647 #[test]
648 fn role_names_returns_all_registered() {
649 let provider = Arc::new(MockProvider::new(vec![]));
650 let mut pool = AgentPool::new(provider, test_config());
651
652 pool.add_role(AgentRole {
653 name: "alpha".into(),
654 system_prompt: "A".into(),
655 max_steps: 1,
656 allowed_tools: vec![],
657 });
658 pool.add_role(AgentRole {
659 name: "beta".into(),
660 system_prompt: "B".into(),
661 max_steps: 2,
662 allowed_tools: vec![],
663 });
664
665 let mut names = pool.role_names();
666 names.sort();
667 assert_eq!(names, vec!["alpha", "beta"]);
668 assert_eq!(pool.role_count(), 2);
669 }
670
671 #[tokio::test]
672 async fn run_with_unknown_role_returns_error() {
673 let provider = Arc::new(MockProvider::new(vec![]));
674 let pool = AgentPool::new(provider, test_config());
675
676 let result = pool.run_with_role("nonexistent", "do something").await;
677 assert!(result.is_err());
678 let err = result.unwrap_err();
679 assert!(err.to_string().contains("unknown role"));
680 }
681
682 #[tokio::test]
683 async fn run_with_role_succeeds_with_mock() {
684 let provider = Arc::new(MockProvider::new(vec![Completion {
685 content: "Plan: step 1, step 2, step 3".into(),
686 tool_calls: vec![],
687 finish_reason: Some("stop".into()),
688 usage: None,
689 reasoning_content: None,
690 }]));
691
692 let mut pool = AgentPool::new(provider, test_config());
693 pool.add_role(AgentRole {
694 name: "planner".into(),
695 system_prompt: "You are a planner.".into(),
696 max_steps: 5,
697 allowed_tools: vec![],
698 });
699
700 let outcome = pool.run_with_role("planner", "plan a task").await.unwrap();
701 assert_eq!(
702 outcome.finish_reason,
703 crate::agent::FinishReason::NoMoreToolCalls
704 );
705 assert!(outcome.final_text.unwrap().contains("Plan:"));
706 }
707
708 #[test]
709 fn default_roles_returns_three_roles() {
710 let roles = default_roles();
711 assert_eq!(roles.len(), 3);
712
713 let names: Vec<&str> = roles.iter().map(|r| r.name.as_str()).collect();
714 assert!(names.contains(&"planner"));
715 assert!(names.contains(&"coder"));
716 assert!(names.contains(&"reviewer"));
717 }
718
719 #[tokio::test]
720 async fn shared_memory_set_and_get() {
721 let mem = SharedMemory::new();
722 mem.set("goal".into(), "build feature X".into(), "planner".into())
723 .await;
724
725 let entry = mem.get("goal").await.unwrap();
726 assert_eq!(entry.key, "goal");
727 assert_eq!(entry.value, "build feature X");
728 assert_eq!(entry.author, "planner");
729 assert!(entry.timestamp > 0);
730 }
731
732 #[tokio::test]
733 async fn shared_memory_keys() {
734 let mem = SharedMemory::new();
735 mem.set("a".into(), "1".into(), "agent1".into()).await;
736 mem.set("b".into(), "2".into(), "agent2".into()).await;
737
738 let mut keys = mem.keys().await;
739 keys.sort();
740 assert_eq!(keys, vec!["a", "b"]);
741 assert_eq!(mem.len().await, 2);
742 assert!(!mem.is_empty().await);
743 }
744
745 #[tokio::test]
746 async fn shared_memory_remove() {
747 let mem = SharedMemory::new();
748 mem.set("tmp".into(), "val".into(), "x".into()).await;
749 assert!(mem.get("tmp").await.is_some());
750
751 let removed = mem.remove("tmp").await;
752 assert!(removed);
753 assert!(mem.get("tmp").await.is_none());
754
755 let removed_again = mem.remove("tmp").await;
757 assert!(!removed_again);
758 }
759
760 #[tokio::test]
761 async fn shared_memory_to_context_string() {
762 let mem = SharedMemory::new();
763 mem.set("status".into(), "in-progress".into(), "coder".into())
764 .await;
765
766 let ctx = mem.to_context_string().await;
767 assert!(ctx.contains("[Shared Memory]"));
768 assert!(ctx.contains("status = in-progress (by coder)"));
769 }
770
771 #[tokio::test]
772 async fn shared_memory_empty_context_returns_empty() {
773 let mem = SharedMemory::new();
774 let ctx = mem.to_context_string().await;
775 assert!(ctx.is_empty());
776 }
777
778 #[tokio::test]
779 async fn agent_pool_includes_memory_context() {
780 let provider = Arc::new(MockProvider::new(vec![Completion {
781 content: "I see the shared memory context.".into(),
782 tool_calls: vec![],
783 finish_reason: Some("stop".into()),
784 usage: None,
785 reasoning_content: None,
786 }]));
787
788 let mut pool = AgentPool::new(provider, test_config());
789 pool.add_role(AgentRole {
790 name: "worker".into(),
791 system_prompt: "You are a worker.".into(),
792 max_steps: 5,
793 allowed_tools: vec![],
794 });
795
796 pool.memory()
798 .set("plan".into(), "step 1 done".into(), "planner".into())
799 .await;
800
801 let outcome = pool.run_with_role("worker", "continue work").await.unwrap();
802 assert_eq!(
803 outcome.finish_reason,
804 crate::agent::FinishReason::NoMoreToolCalls
805 );
806 assert!(outcome.final_text.is_some());
808 }
809
810 fn make_msg(from: &str, to: &str, content: &str, msg_type: MessageType) -> AgentMessage {
813 AgentMessage {
814 id: generate_message_id(),
815 from: from.to_string(),
816 to: to.to_string(),
817 content: content.to_string(),
818 msg_type,
819 timestamp: now_timestamp(),
820 }
821 }
822
823 #[tokio::test]
824 async fn message_bus_send_and_inbox() {
825 let bus = MessageBus::new();
826 let msg = make_msg("planner", "coder", "implement feature X", MessageType::Task);
827 bus.send(msg).await;
828
829 let inbox = bus.inbox("coder").await;
830 assert_eq!(inbox.len(), 1);
831 assert_eq!(inbox[0].content, "implement feature X");
832 assert_eq!(inbox[0].from, "planner");
833 assert_eq!(inbox[0].msg_type, MessageType::Task);
834
835 let empty = bus.inbox("reviewer").await;
837 assert!(empty.is_empty());
838 }
839
840 #[tokio::test]
841 async fn message_bus_outbox() {
842 let bus = MessageBus::new();
843 bus.send(make_msg(
844 "coder",
845 "reviewer",
846 "done coding",
847 MessageType::Result,
848 ))
849 .await;
850 bus.send(make_msg(
851 "coder",
852 "planner",
853 "need clarification",
854 MessageType::Question,
855 ))
856 .await;
857 bus.send(make_msg(
858 "planner",
859 "coder",
860 "here is the plan",
861 MessageType::Task,
862 ))
863 .await;
864
865 let outbox = bus.outbox("coder").await;
866 assert_eq!(outbox.len(), 2);
867 assert!(outbox.iter().all(|m| m.from == "coder"));
868
869 let planner_outbox = bus.outbox("planner").await;
870 assert_eq!(planner_outbox.len(), 1);
871 }
872
873 #[tokio::test]
874 async fn message_bus_broadcast_reaches_all() {
875 let bus = MessageBus::new();
876 bus.send(make_msg(
877 "admin",
878 "broadcast",
879 "system update",
880 MessageType::Broadcast,
881 ))
882 .await;
883
884 let coder_inbox = bus.inbox("coder").await;
885 let reviewer_inbox = bus.inbox("reviewer").await;
886 let planner_inbox = bus.inbox("planner").await;
887
888 assert_eq!(coder_inbox.len(), 1);
889 assert_eq!(reviewer_inbox.len(), 1);
890 assert_eq!(planner_inbox.len(), 1);
891 assert_eq!(coder_inbox[0].content, "system update");
892 }
893
894 #[tokio::test]
895 async fn message_bus_subscribe_receives() {
896 let bus = MessageBus::new();
897 let mut rx = bus.subscribe("coder").await;
898
899 let msg = make_msg("planner", "coder", "task for you", MessageType::Task);
901 bus.send(msg).await;
902
903 let received = rx.recv().await.unwrap();
904 assert_eq!(received.content, "task for you");
905 assert_eq!(received.from, "planner");
906 }
907
908 #[tokio::test]
909 async fn message_bus_history() {
910 let bus = MessageBus::new();
911 bus.send(make_msg("a", "b", "msg1", MessageType::Task))
912 .await;
913 bus.send(make_msg("b", "a", "msg2", MessageType::Result))
914 .await;
915 bus.send(make_msg("a", "broadcast", "msg3", MessageType::Broadcast))
916 .await;
917
918 let history = bus.history().await;
919 assert_eq!(history.len(), 3);
920 assert_eq!(history[0].content, "msg1");
921 assert_eq!(history[1].content, "msg2");
922 assert_eq!(history[2].content, "msg3");
923 }
924
925 #[tokio::test]
926 async fn message_bus_clear() {
927 let bus = MessageBus::new();
928 bus.send(make_msg("a", "b", "hello", MessageType::Task))
929 .await;
930 assert_eq!(bus.history().await.len(), 1);
931
932 bus.clear().await;
933 assert!(bus.history().await.is_empty());
934 assert!(bus.inbox("b").await.is_empty());
935 }
936
937 #[tokio::test]
938 async fn agent_pool_send_task_convenience() {
939 let provider = Arc::new(MockProvider::new(vec![]));
940 let pool = AgentPool::new(provider, test_config());
941
942 pool.send_task("planner", "coder", "build module Y").await;
943 pool.send_result("coder", "planner", "module Y complete")
944 .await;
945
946 let inbox = pool.bus().inbox("coder").await;
947 assert_eq!(inbox.len(), 1);
948 assert_eq!(inbox[0].content, "build module Y");
949 assert_eq!(inbox[0].msg_type, MessageType::Task);
950
951 let planner_inbox = pool.bus().inbox("planner").await;
952 assert_eq!(planner_inbox.len(), 1);
953 assert_eq!(planner_inbox[0].content, "module Y complete");
954 assert_eq!(planner_inbox[0].msg_type, MessageType::Result);
955
956 let history = pool.bus().history().await;
957 assert_eq!(history.len(), 2);
958 }
959
960 #[tokio::test]
963 async fn pipeline_empty_returns_empty_result() {
964 let provider = Arc::new(MockProvider::new(vec![]));
965 let pool = AgentPool::new(provider, test_config());
966
967 let pipeline = Pipeline::new(vec![]);
968 let result = pipeline.execute(&pool, "hello").await.unwrap();
969 assert_eq!(result.stage_count(), 0);
970 assert_eq!(result.final_output(), "");
971 }
972
973 #[tokio::test]
974 async fn pipeline_single_stage() {
975 let provider = Arc::new(MockProvider::new(vec![Completion {
976 content: "stage one output".into(),
977 tool_calls: vec![],
978 finish_reason: Some("stop".into()),
979 usage: None,
980 reasoning_content: None,
981 }]));
982
983 let mut pool = AgentPool::new(provider, test_config());
984 pool.add_role(AgentRole {
985 name: "writer".into(),
986 system_prompt: "You write things.".into(),
987 max_steps: 5,
988 allowed_tools: vec![],
989 });
990
991 let pipeline = Pipeline::new(vec!["writer".into()]);
992 let result = pipeline.execute(&pool, "write something").await.unwrap();
993
994 assert_eq!(result.stage_count(), 1);
995 assert_eq!(result.stages[0].role, "writer");
996 assert_eq!(result.stages[0].output, "stage one output");
997 assert_eq!(result.final_output(), "stage one output");
998 }
999
1000 #[tokio::test]
1001 async fn pipeline_multi_stage_passes_output() {
1002 let provider = Arc::new(MockProvider::new(vec![
1003 Completion {
1004 content: "draft text".into(),
1005 tool_calls: vec![],
1006 finish_reason: Some("stop".into()),
1007 usage: None,
1008 reasoning_content: None,
1009 },
1010 Completion {
1011 content: "polished text".into(),
1012 tool_calls: vec![],
1013 finish_reason: Some("stop".into()),
1014 usage: None,
1015 reasoning_content: None,
1016 },
1017 ]));
1018
1019 let mock_ref = provider.clone();
1020 let mut pool = AgentPool::new(provider, test_config());
1021 pool.add_role(AgentRole {
1022 name: "drafter".into(),
1023 system_prompt: "You draft text.".into(),
1024 max_steps: 5,
1025 allowed_tools: vec![],
1026 });
1027 pool.add_role(AgentRole {
1028 name: "editor".into(),
1029 system_prompt: "You polish text.".into(),
1030 max_steps: 5,
1031 allowed_tools: vec![],
1032 });
1033
1034 let pipeline = Pipeline::new(vec!["drafter".into(), "editor".into()]);
1035 let result = pipeline.execute(&pool, "original goal").await.unwrap();
1036
1037 assert_eq!(result.stage_count(), 2);
1038 assert_eq!(result.stages[0].output, "draft text");
1039 assert_eq!(result.stages[1].output, "polished text");
1040 assert_eq!(result.final_output(), "polished text");
1041
1042 let calls = mock_ref.calls();
1044 assert_eq!(calls.len(), 2);
1045 let second_call_user_msg = calls[1]
1047 .iter()
1048 .find(|m| m.role == crate::message::Role::User);
1049 assert!(second_call_user_msg.unwrap().content.contains("draft text"));
1050 }
1051
1052 #[tokio::test]
1053 async fn pipeline_fails_on_unknown_role() {
1054 let provider = Arc::new(MockProvider::new(vec![]));
1055 let pool = AgentPool::new(provider, test_config());
1056
1057 let pipeline = Pipeline::new(vec!["nonexistent".into()]);
1058 let result = pipeline.execute(&pool, "hello").await;
1059
1060 assert!(result.is_err());
1061 let err = result.unwrap_err();
1062 assert!(err.to_string().contains("unknown role"));
1063 }
1064
1065 #[tokio::test]
1066 async fn pipeline_final_output() {
1067 let provider = Arc::new(MockProvider::new(vec![
1068 Completion {
1069 content: "first".into(),
1070 tool_calls: vec![],
1071 finish_reason: Some("stop".into()),
1072 usage: None,
1073 reasoning_content: None,
1074 },
1075 Completion {
1076 content: "second".into(),
1077 tool_calls: vec![],
1078 finish_reason: Some("stop".into()),
1079 usage: None,
1080 reasoning_content: None,
1081 },
1082 Completion {
1083 content: "final answer".into(),
1084 tool_calls: vec![],
1085 finish_reason: Some("stop".into()),
1086 usage: None,
1087 reasoning_content: None,
1088 },
1089 ]));
1090
1091 let mut pool = AgentPool::new(provider, test_config());
1092 pool.add_role(AgentRole {
1093 name: "a".into(),
1094 system_prompt: "A".into(),
1095 max_steps: 5,
1096 allowed_tools: vec![],
1097 });
1098 pool.add_role(AgentRole {
1099 name: "b".into(),
1100 system_prompt: "B".into(),
1101 max_steps: 5,
1102 allowed_tools: vec![],
1103 });
1104 pool.add_role(AgentRole {
1105 name: "c".into(),
1106 system_prompt: "C".into(),
1107 max_steps: 5,
1108 allowed_tools: vec![],
1109 });
1110
1111 let pipeline = Pipeline::new(vec!["a".into(), "b".into(), "c".into()]);
1112 let result = pipeline.execute(&pool, "start").await.unwrap();
1113
1114 assert_eq!(result.final_output(), "final answer");
1115 assert_eq!(result.stage_count(), 3);
1116 }
1117
1118 #[test]
1121 fn parse_delegations_extracts_role_and_task() {
1122 let text = "DELEGATE:coder:write hello";
1123 let result = parse_delegations(text);
1124 assert_eq!(
1125 result,
1126 vec![("coder".to_string(), "write hello".to_string())]
1127 );
1128 }
1129
1130 #[test]
1131 fn parse_delegations_ignores_non_delegation() {
1132 let text = "Here is my plan:\n- Think about it\nDELEGATE:coder:implement feature\nSome other text\nDELEGATE:reviewer:check code";
1133 let result = parse_delegations(text);
1134 assert_eq!(result.len(), 2);
1135 assert_eq!(
1136 result[0],
1137 ("coder".to_string(), "implement feature".to_string())
1138 );
1139 assert_eq!(
1140 result[1],
1141 ("reviewer".to_string(), "check code".to_string())
1142 );
1143 }
1144
1145 #[test]
1146 fn parse_delegations_handles_colons_in_task() {
1147 let text = "DELEGATE:coder:write file:test.rs";
1148 let result = parse_delegations(text);
1149 assert_eq!(
1150 result,
1151 vec![("coder".to_string(), "write file:test.rs".to_string())]
1152 );
1153 }
1154
1155 #[tokio::test]
1156 async fn orchestrator_no_delegations_returns_lead_response() {
1157 let provider = Arc::new(MockProvider::new(vec![Completion {
1159 content: "I will handle this myself. The answer is 42.".into(),
1160 tool_calls: vec![],
1161 finish_reason: Some("stop".into()),
1162 usage: None,
1163 reasoning_content: None,
1164 }]));
1165
1166 let mut pool = AgentPool::new(provider, test_config());
1167 pool.add_role(AgentRole {
1168 name: "lead".into(),
1169 system_prompt: "You are the lead.".into(),
1170 max_steps: 5,
1171 allowed_tools: vec![],
1172 });
1173 pool.add_role(AgentRole {
1174 name: "coder".into(),
1175 system_prompt: "You code.".into(),
1176 max_steps: 5,
1177 allowed_tools: vec![],
1178 });
1179
1180 let orchestrator = TeamOrchestrator::new("lead".into(), vec!["coder".into()]);
1181 let result = orchestrator
1182 .run(&pool, "What is the meaning of life?")
1183 .await
1184 .unwrap();
1185
1186 assert!(result.delegations.is_empty());
1187 assert_eq!(
1188 result.final_output,
1189 "I will handle this myself. The answer is 42."
1190 );
1191 }
1192
1193 #[tokio::test]
1194 async fn orchestrator_with_delegations_executes_them() {
1195 let provider = Arc::new(MockProvider::new(vec![
1197 Completion {
1198 content: "Let me delegate this.\nDELEGATE:coder:write a hello world program".into(),
1199 tool_calls: vec![],
1200 finish_reason: Some("stop".into()),
1201 usage: None,
1202 reasoning_content: None,
1203 },
1204 Completion {
1205 content: "fn main() { println!(\"Hello, world!\"); }".into(),
1206 tool_calls: vec![],
1207 finish_reason: Some("stop".into()),
1208 usage: None,
1209 reasoning_content: None,
1210 },
1211 Completion {
1212 content: "The coder produced a working hello world program in Rust.".into(),
1213 tool_calls: vec![],
1214 finish_reason: Some("stop".into()),
1215 usage: None,
1216 reasoning_content: None,
1217 },
1218 ]));
1219
1220 let mut pool = AgentPool::new(provider, test_config());
1221 pool.add_role(AgentRole {
1222 name: "lead".into(),
1223 system_prompt: "You are the lead.".into(),
1224 max_steps: 5,
1225 allowed_tools: vec![],
1226 });
1227 pool.add_role(AgentRole {
1228 name: "coder".into(),
1229 system_prompt: "You write code.".into(),
1230 max_steps: 5,
1231 allowed_tools: vec![],
1232 });
1233
1234 let orchestrator = TeamOrchestrator::new("lead".into(), vec!["coder".into()]);
1235 let result = orchestrator
1236 .run(&pool, "Create a hello world program")
1237 .await
1238 .unwrap();
1239
1240 assert_eq!(result.delegations.len(), 1);
1241 assert_eq!(result.delegations[0].role, "coder");
1242 assert_eq!(result.delegations[0].task, "write a hello world program");
1243 assert!(result.delegations[0].success);
1244 assert!(result.delegations[0].output.contains("Hello, world!"));
1245 assert_eq!(
1246 result.final_output,
1247 "The coder produced a working hello world program in Rust."
1248 );
1249 }
1250}