1use crate::kernel::{AgentKernel, TurnContext, TurnOutcome};
4use crate::message::Message;
5use crate::permissions::PermissionMode;
6use crate::tasks::TaskRegistry;
7use crate::tools::{
8 AgentDefinitions, AgentTool, ListWorkersTool, SendMessageTool, ToolRegistry, WorkerRegistry,
9};
10#[cfg(feature = "coordinator-mode")]
11use crate::tools::{
12 TaskCreateTool, TaskGetTool, TaskListTool, TaskOutputTool, TaskStopTool, TaskUpdateTool,
13};
14use crate::{ChatProvider, Config};
15use std::collections::{HashMap, VecDeque};
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::Arc;
18use std::time::{SystemTime, UNIX_EPOCH};
19use tokio::sync::{broadcast, RwLock};
20
21#[derive(Clone)]
23pub struct SharedMemory {
24 store: Arc<RwLock<HashMap<String, MemoryEntry>>>,
25 seq: Arc<AtomicU64>,
26}
27
28#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
35pub struct MemoryEntry {
36 pub key: String,
37 pub value: String,
38 pub author: String,
39 pub timestamp: u64,
40 #[serde(default)]
41 pub seq: u64,
42}
43
44impl SharedMemory {
45 pub fn new() -> Self {
46 Self {
47 store: Arc::new(RwLock::new(HashMap::new())),
48 seq: Arc::new(AtomicU64::new(1)),
49 }
50 }
51
52 pub async fn set(&self, key: String, value: String, author: String) {
53 let timestamp = SystemTime::now()
54 .duration_since(UNIX_EPOCH)
55 .unwrap_or_default()
56 .as_secs();
57 let seq = self.seq.fetch_add(1, Ordering::Relaxed);
58 let entry = MemoryEntry {
59 key: key.clone(),
60 value,
61 author,
62 timestamp,
63 seq,
64 };
65 self.store.write().await.insert(key, entry);
66 }
67
68 pub async fn get(&self, key: &str) -> Option<MemoryEntry> {
69 self.store.read().await.get(key).cloned()
70 }
71
72 pub async fn keys(&self) -> Vec<String> {
73 self.store.read().await.keys().cloned().collect()
74 }
75
76 pub async fn all(&self) -> Vec<MemoryEntry> {
77 self.store.read().await.values().cloned().collect()
78 }
79
80 pub async fn remove(&self, key: &str) -> bool {
81 self.store.write().await.remove(key).is_some()
82 }
83
84 pub async fn to_context_string(&self) -> String {
85 let store = self.store.read().await;
86 if store.is_empty() {
87 return String::new();
88 }
89 let mut lines = vec!["[Shared Memory]".to_string()];
90 for entry in store.values() {
91 lines.push(format!(
92 "- {} = {} (by {})",
93 entry.key, entry.value, entry.author
94 ));
95 }
96 lines.join("\n")
97 }
98
99 pub async fn len(&self) -> usize {
100 self.store.read().await.len()
101 }
102
103 pub async fn is_empty(&self) -> bool {
104 self.store.read().await.is_empty()
105 }
106}
107
108impl Default for SharedMemory {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
118pub enum MessageType {
119 Task,
120 Result,
121 Question,
122 Feedback,
123 Broadcast,
124}
125
126#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
128pub struct AgentMessage {
129 pub id: String,
130 pub from: String,
131 pub to: String,
132 pub content: String,
133 pub msg_type: MessageType,
134 pub timestamp: u64,
135}
136
137fn generate_message_id() -> String {
139 static COUNTER: AtomicU64 = AtomicU64::new(0);
140
141 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
142 let now = SystemTime::now()
143 .duration_since(UNIX_EPOCH)
144 .unwrap_or_default();
145 let input = format!("msg-{}-{}", now.as_nanos(), count);
146 let hash = blake3::hash(input.as_bytes());
147 hash.to_hex()[..16].to_string()
148}
149
150fn now_timestamp() -> u64 {
152 SystemTime::now()
153 .duration_since(UNIX_EPOCH)
154 .unwrap_or_default()
155 .as_secs()
156}
157
158pub const MESSAGE_BUS_CAPACITY: usize = 1000;
163
164#[derive(Clone)]
166pub struct MessageBus {
167 messages: Arc<RwLock<VecDeque<AgentMessage>>>,
171 subscribers: Arc<RwLock<HashMap<String, broadcast::Sender<AgentMessage>>>>,
172 capacity: usize,
175}
176
177impl MessageBus {
178 pub fn new() -> Self {
179 Self {
180 messages: Arc::new(RwLock::new(VecDeque::with_capacity(MESSAGE_BUS_CAPACITY))),
181 subscribers: Arc::new(RwLock::new(HashMap::new())),
182 capacity: MESSAGE_BUS_CAPACITY,
183 }
184 }
185
186 pub async fn send(&self, msg: AgentMessage) {
189 {
190 let mut history = self.messages.write().await;
191 if history.len() >= self.capacity {
192 history.pop_front();
193 }
194 history.push_back(msg.clone());
195 }
196 let subs = self.subscribers.read().await;
197 if msg.to == "broadcast" {
198 for tx in subs.values() {
199 let _ = tx.send(msg.clone());
200 }
201 } else if let Some(tx) = subs.get(&msg.to) {
202 let _ = tx.send(msg);
203 }
204 }
205
206 pub fn with_capacity(cap: usize) -> Self {
208 Self {
209 messages: Arc::new(RwLock::new(VecDeque::with_capacity(cap))),
210 subscribers: Arc::new(RwLock::new(HashMap::new())),
211 capacity: cap,
212 }
213 }
214
215 pub async fn subscribe(&self, role: &str) -> broadcast::Receiver<AgentMessage> {
217 let mut subs = self.subscribers.write().await;
218 let tx = subs.entry(role.to_string()).or_insert_with(|| {
219 let (tx, _) = broadcast::channel(64);
220 tx
221 });
222 tx.subscribe()
223 }
224
225 pub async fn inbox(&self, role: &str) -> Vec<AgentMessage> {
227 self.messages
228 .read()
229 .await
230 .iter()
231 .filter(|m| m.to == role || m.to == "broadcast")
232 .cloned()
233 .collect()
234 }
235
236 pub async fn outbox(&self, role: &str) -> Vec<AgentMessage> {
238 self.messages
239 .read()
240 .await
241 .iter()
242 .filter(|m| m.from == role)
243 .cloned()
244 .collect()
245 }
246
247 pub async fn history(&self) -> VecDeque<AgentMessage> {
249 self.messages.read().await.clone()
250 }
251
252 pub async fn clear(&self) {
254 self.messages.write().await.clear();
255 }
256}
257
258impl Default for MessageBus {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264#[derive(Clone, Debug)]
266pub struct AgentRole {
267 pub name: String,
268 pub system_prompt: String,
269 pub max_steps: usize,
270 pub allowed_tools: Vec<String>,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum AgentMode {
276 Single,
278 Parallel,
280 Sequential,
282}
283
284impl AgentMode {
285 pub fn parse(s: &str) -> Option<Self> {
286 match s {
287 "single" => Some(Self::Single),
288 "parallel" => Some(Self::Parallel),
289 "sequential" => Some(Self::Sequential),
290 _ => None,
291 }
292 }
293}
294
295#[derive(Clone, Debug)]
297pub struct WorkerManifestEntry {
298 pub system_prompt: String,
299 pub allowed_tools: Vec<String>,
300}
301
302pub type AgentManifest = HashMap<String, WorkerManifestEntry>;
304
305pub struct AgentPool {
307 roles: HashMap<String, AgentRole>,
308 provider: Arc<dyn ChatProvider>,
309 memory: SharedMemory,
310 bus: MessageBus,
311}
312
313impl AgentPool {
314 pub fn new(provider: Arc<dyn ChatProvider>, _config: Config) -> Self {
315 Self {
316 roles: HashMap::new(),
317 provider,
318 memory: SharedMemory::new(),
319 bus: MessageBus::new(),
320 }
321 }
322
323 pub fn memory(&self) -> &SharedMemory {
324 &self.memory
325 }
326
327 pub fn bus(&self) -> &MessageBus {
328 &self.bus
329 }
330
331 pub fn add_role(&mut self, role: AgentRole) {
332 self.roles.insert(role.name.clone(), role);
333 }
334
335 pub fn get_role(&self, name: &str) -> Option<&AgentRole> {
336 self.roles.get(name)
337 }
338
339 pub fn role_names(&self) -> Vec<&str> {
340 self.roles.keys().map(|s| s.as_str()).collect()
341 }
342
343 pub fn role_count(&self) -> usize {
344 self.roles.len()
345 }
346
347 pub fn remove_role(&mut self, name: &str) -> bool {
349 self.roles.remove(name).is_some()
350 }
351
352 pub async fn run_with_role(
353 &self,
354 role_name: &str,
355 goal: &str,
356 ) -> Result<TurnOutcome, crate::Error> {
357 let role = self
358 .roles
359 .get(role_name)
360 .ok_or_else(|| crate::Error::Config {
361 message: format!("unknown role: {role_name}"),
362 })?;
363
364 let memory_ctx = self.memory.to_context_string().await;
365 let system_prompt = if memory_ctx.is_empty() {
366 role.system_prompt.clone()
367 } else {
368 format!("{}\n\n{}", role.system_prompt, memory_ctx)
369 };
370
371 let kernel = AgentKernel::builder()
372 .llm(self.provider.clone())
373 .max_steps(role.max_steps)
374 .build()?;
375
376 let ctx = TurnContext {
377 messages: Arc::new(vec![
378 Message::system(system_prompt),
379 Message::user(goal.to_string()),
380 ]),
381 step_events_tx: None,
382 tool_specs: kernel.tools().specs(),
383 streaming: false,
384 permission_hook: None,
385 exploring_plan_mode: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
386 permission_mode: PermissionMode::Default,
387 mailbox: None,
388 turn: 0,
389 prompt_segments: None,
390 wall_timeout_secs: 0,
391 };
392
393 kernel.run(ctx).await
394 }
395
396 pub async fn send_task(&self, from: &str, to: &str, content: &str) {
398 self.bus
399 .send(AgentMessage {
400 id: generate_message_id(),
401 from: from.to_string(),
402 to: to.to_string(),
403 content: content.to_string(),
404 msg_type: MessageType::Task,
405 timestamp: now_timestamp(),
406 })
407 .await;
408 }
409
410 pub async fn send_result(&self, from: &str, to: &str, content: &str) {
412 self.bus
413 .send(AgentMessage {
414 id: generate_message_id(),
415 from: from.to_string(),
416 to: to.to_string(),
417 content: content.to_string(),
418 msg_type: MessageType::Result,
419 timestamp: now_timestamp(),
420 })
421 .await;
422 }
423}
424
425pub fn coordinator_system_prompt() -> &'static str {
440 concat!(
441 "You are a coordinator agent. Your job is to decompose complex tasks and delegate them ",
442 "to specialist workers you design on the fly.\n\n",
443 "## Your tools\n\n",
444 "- **`agent`** — your primary tool. Spawns one or more specialist workers. Each worker ",
445 " runs its own agent loop with a restricted tool set and an isolated transcript.\n",
446 "- **`send_message`** — push a follow-up message to a worker. For a background worker this ",
447 "drives a NEW turn on the same runtime, preserving its transcript/context (cross-turn ",
448 "continuation). Use `task_id` (returned by a background `agent` call) to address it.\n",
449 "- If present, **`task_list` / `task_output` / `task_stop`** let you inspect running tasks, ",
450 "drain their buffered output, or cancel them. (These are optional and may not be registered ",
451 "in every build; if a tool call is rejected as unknown, simply rely on `agent` and ",
452 "`send_message` instead.)\n\n",
453 "## Your workflow\n\n",
454 "1. **Analyse** — understand the task and identify the distinct subtasks and the expertise each requires.\n",
455 "2. **Design specialists** — for each distinct expertise, add an entry to the `agent` tool's ",
456 "`manifest`. Each entry is a worker_id → role mapping. A role is defined by:\n",
457 " - A focused `system_prompt` that defines the specialist's role, constraints, and output format\n",
458 " - Optional `allowed_tools` (omit or leave empty for the read-only set: ",
459 "Read, Grep, Glob, WebFetch, SearchFiles; list tool names to grant write tools)\n",
460 " - Or a `definition` referencing a built-in role from `.recursive/agents/*.md` ",
461 "(e.g. `explore`, `plan`, `verification`, `general-purpose`)\n",
462 "3. **Dispatch** — set the `agent` tool's `mode`:\n",
463 " - `single` — exactly one worker, runs to completion and returns its result.\n",
464 " - `parallel` — all workers run concurrently. Use for independent read-only tasks ",
465 "(research, review).\n",
466 " - `sequential` — workers run one after another. Use when one task's output feeds the next, ",
467 "or for write-heavy tasks that may touch the same files.\n",
468 " - For a long-running task you want to continue later, set `background: true` with ",
469 "`mode: \"single\"`. The worker is spawned in the background and you get a `task_id` immediately ",
470 "(instead of waiting). Use `send_message(task_id=...)` to run follow-up turns on the same worker, ",
471 "and `task_output` / `task_stop` to inspect or cancel it.\n",
472 "4. **Synthesise** — collect all worker results and write a final unified answer.\n\n",
473 "## Rules\n\n",
474 "- Design the *minimum* number of specialists the task requires — avoid over-decomposition.\n",
475 "- Read-only tasks (analysis, review, research) can always run in parallel.\n",
476 "- Write-heavy tasks (coding, patching) should run sequentially unless you are certain they touch different files.\n",
477 "- After all workers finish, synthesise their outputs into a single coherent response rather than just concatenating them.\n\n",
478 "## Writing worker prompts\n\n",
479 "**Workers cannot see this conversation.** Every `prompt` you pass to the `agent` tool, ",
480 "and every `message` you send via `send_message`, must be self-contained — it is the only ",
481 "context the worker has.\n\n",
482 "### Never delegate understanding\n\n",
483 "When a worker reports research findings, **you** must understand them before directing\n",
484 "follow-up work. Read the findings, identify the approach, then write a prompt that *proves*\n",
485 "you understood it by naming the exact `file:line`, the specific change, and what \"done\" looks\n",
486 "like. Never write \"based on your findings, fix the bug\" or \"based on the research, implement\n",
487 "it\" — those phrases push the synthesis onto the worker instead of doing it yourself. You never\n",
488 "hand off understanding to another worker.\n\n",
489 "```\n",
490 "// Bad — lazy delegation (workers can't see this conversation)\n",
491 "agent({ mode: \"single\",\n",
492 " manifest: { fixer: { system_prompt: \"You fix bugs.\" } },\n",
493 " prompt: \"Based on your findings, fix the auth bug\" })\n",
494 "\n",
495 "// Good — synthesised spec: file:line, root cause, exact change, done-criterion\n",
496 "agent({ mode: \"single\",\n",
497 " manifest: { coder: { system_prompt: \"You implement tasks using the available tools.\",\n",
498 " allowed_tools: [\"Read\", \"Edit\", \"Bash\"] } },\n",
499 " prompt: \"Fix the null deref in src/auth/validate.rs:42. The `user` field on Session\n",
500 " (src/auth/types.rs:15) is None when a session expires but the token stays cached.\n",
501 " Add a None check before `user.id` access — if None, return 401 with 'Session expired'.\n",
502 " Run `cargo test -p auth` and report the result.\" })\n",
503 "```\n\n",
504 "### What every worker prompt must contain\n\n",
505 "- **Target**: concrete `file:line` references and the exact change to make — not \"the auth\n",
506 " module\", but `src/auth/validate.rs:42`.\n",
507 "- **Root cause, not symptom**: guide the worker toward a durable fix, not a patch that\n",
508 " silences the error.\n",
509 "- **Done-criterion**: state what \"done\" looks like. For implementation: \"run the relevant\n",
510 " tests and typecheck, then report the result\". For research: \"report file paths, line\n",
511 " numbers, and signatures — do not modify files\".\n",
512 "- **Purpose**: one line on why the task matters, so the worker can calibrate depth (\"this\n",
513 " informs a PR description — focus on user-facing changes\").\n\n",
514 "### Continue vs spawn — decide by context overlap\n\n",
515 "A background worker (`background: true`) stays alive across turns: `send_message(task_id=...)` ",
516 "runs a new turn on the same runtime, so the worker keeps its loaded files and prior findings. ",
517 "Choose between continuing an existing background worker and spawning a fresh one by how much of ",
518 "its loaded context overlaps the next task:\n\n",
519 "| Situation | Mechanism | Why |\n",
520 "|-----------|-----------|-----|\n",
521 "| Follow-up refines the background worker's current task | **Continue** (`send_message`) | Worker already has the files loaded AND now gets a clearer plan |\n",
522 "| Research was broad but the next task is narrow | **Spawn fresh** (`agent`) | Focused context is cleaner than dragging along exploration noise |\n",
523 "| Correcting a failure or extending recent work | **Continue** (`send_message`) | Worker has the error context and knows what it just tried |\n",
524 "| Verifying code a different worker just wrote | **Spawn fresh** (`agent`) | A verifier should see the code with fresh eyes, not carry the implementer's assumptions |\n",
525 "| First attempt used the wrong approach entirely | **Spawn fresh** | Wrong-approach context pollutes the retry; clean slate avoids anchoring on the failed path |\n",
526 "| Unrelated task | **Spawn fresh** | No useful context to reuse |\n\n",
527 "There is no universal default. High overlap → continue with `send_message`. Low overlap → spawn fresh.\n\n",
528 "### Verification — prove it works, don't confirm it exists\n\n",
529 "When a worker (or you) claims a change is done, verification means **proving the code works**,\n",
530 "not confirming that it exists. A verifier that rubber-stamps weak work undermines everything.\n",
531 "- Run tests **with the feature exercised**, not just \"the suite passes overall\".\n",
532 "- Run typecheck/clippy and **investigate** errors — don't dismiss them as \"unrelated\".\n",
533 "- Be sceptical: if something looks off, dig in. Try edge cases and error paths, not just the\n",
534 " happy path the implementer already ran.\n"
535 )
536}
537
538pub fn register_subagent_if_enabled(
555 tools: ToolRegistry,
556 config: &Config,
557 provider: Arc<dyn ChatProvider>,
558) -> ToolRegistry {
559 if !config.subagent_enabled {
560 return tools;
561 }
562 let defs = AgentDefinitions::load(&config.workspace).unwrap_or_else(|e| {
563 tracing::warn!("Failed to load agent definitions: {e}");
564 AgentDefinitions::default()
565 });
566 let task_registry = Arc::new(TaskRegistry::new());
569 let worker_registry = WorkerRegistry::new();
570 let worker_table: crate::tools::agent::WorkerTable =
571 Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
572
573 let agent = AgentTool::new(
574 &config.workspace,
575 provider,
576 tools.fork(),
577 config.subagent_max_depth,
578 0,
579 None,
580 )
581 .with_definitions(defs)
582 .with_task_registry(task_registry.clone())
583 .with_registry(worker_registry.clone())
584 .with_workers(worker_table.clone());
585
586 let tools = tools
592 .register(Arc::new(agent))
593 .register(Arc::new(SendMessageTool::new(
594 worker_registry.clone(),
595 task_registry.clone(),
596 worker_table.clone(),
597 )))
598 .register(Arc::new(ListWorkersTool::new(
599 worker_registry.clone(),
600 task_registry.clone(),
601 )));
602
603 #[cfg(feature = "coordinator-mode")]
604 {
605 tools
606 .register(Arc::new(TaskCreateTool::new(task_registry.clone())))
607 .register(Arc::new(TaskGetTool::new(task_registry.clone())))
608 .register(Arc::new(TaskListTool::new(task_registry.clone())))
609 .register(Arc::new(TaskOutputTool::new(task_registry.clone())))
610 .register(Arc::new(TaskStopTool::new(task_registry.clone())))
611 .register(Arc::new(TaskUpdateTool::new(task_registry.clone())))
612 }
613 #[cfg(not(feature = "coordinator-mode"))]
614 {
615 let _ = &task_registry;
620 tools
621 }
622}
623
624pub fn default_roles() -> Vec<AgentRole> {
626 vec![
627 AgentRole {
628 name: "planner".into(),
629 system_prompt: "You are a planning agent. Analyze the task, break it into steps, \
630 and output a structured plan. Do not execute — only plan."
631 .into(),
632 max_steps: 10,
633 allowed_tools: vec![],
634 },
635 AgentRole {
636 name: "coder".into(),
637 system_prompt: "You are a coding agent. Implement the task using the available \
638 tools. Write code, run tests, fix errors."
639 .into(),
640 max_steps: 50,
641 allowed_tools: vec![],
642 },
643 AgentRole {
644 name: "reviewer".into(),
645 system_prompt: "You are a code review agent. Read the code changes, identify \
646 issues, suggest improvements. Do not modify files."
647 .into(),
648 max_steps: 20,
649 allowed_tools: vec!["Read".into(), "Grep".into()],
650 },
651 ]
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657 use crate::llm::{Completion, MockProvider};
658 use std::path::PathBuf;
659
660 fn test_config() -> Config {
661 Config {
662 workspace: PathBuf::from("."),
663 api_base: String::new(),
664 api_key: None,
665 model: String::new(),
666 provider_type: "openai".into(),
667 preset: None,
668 max_steps: 32,
669 max_tokens: 65536,
670 temperature: 0.2,
671 system_prompt: String::new(),
672 retry_max: 2,
673 retry_initial_backoff_secs: 1,
674 retry_max_backoff_secs: 8,
675 shell_timeout_secs: 300,
676 headless: false,
677 memory_summary_limit: 5,
678 thinking_budget: None,
679 session_name: None,
680 max_budget_usd: None,
681 extra_dirs: Vec::new(),
682 extra_readonly_dirs: Vec::new(),
683 allow_tools: Vec::new(),
684 context_window_override: None,
685 subagent_max_depth: 2,
686 subagent_enabled: false,
687 allow_bypass_permissions: false,
688 max_search_rounds: 3,
689 stuck_window: 10,
690 stuck_error_rate: 0.8,
691 max_concurrent_runs: 8,
692 goal_eval_transcript_tail: 12,
693 web_search_provider: None,
694 web_search_api_key: None,
695 web_search_jina_key: None,
696 wall_timeout_secs: 0,
697 }
698 }
699
700 #[tokio::test]
701 async fn shared_memory_assigns_monotonic_seq_across_writes() {
702 let mem = SharedMemory::new();
703 mem.set("a".into(), "1".into(), "alpha".into()).await;
704 mem.set("b".into(), "2".into(), "alpha".into()).await;
705 let a = mem.get("a").await.unwrap();
706 let b = mem.get("b").await.unwrap();
707 assert!(a.seq > 0, "first write should not have seq 0");
708 assert!(b.seq > a.seq, "second write seq must exceed first");
709 }
710
711 #[tokio::test]
712 async fn shared_memory_seq_advances_on_overwrite() {
713 let mem = SharedMemory::new();
714 mem.set("k".into(), "v1".into(), "alpha".into()).await;
715 let v1 = mem.get("k").await.unwrap().seq;
716 mem.set("k".into(), "v2".into(), "beta".into()).await;
717 let v2 = mem.get("k").await.unwrap().seq;
718 assert!(v2 > v1, "overwriting the same key must advance seq");
719 }
720
721 #[test]
722 fn memory_entry_deserializes_without_seq_field() {
723 let json = r#"{"key":"k","value":"v","author":"a","timestamp":123}"#;
725 let entry: MemoryEntry = serde_json::from_str(json).expect("deserialize legacy entry");
726 assert_eq!(entry.seq, 0);
727 }
728
729 #[test]
730 fn memory_entry_round_trips_with_seq() {
731 let entry = MemoryEntry {
732 key: "k".into(),
733 value: "v".into(),
734 author: "a".into(),
735 timestamp: 123,
736 seq: 42,
737 };
738 let json = serde_json::to_string(&entry).expect("serialize");
739 let back: MemoryEntry = serde_json::from_str(&json).expect("deserialize");
740 assert_eq!(back.seq, 42);
741 }
742
743 #[test]
744 fn new_pool_is_empty() {
745 let provider = Arc::new(MockProvider::new(vec![]));
746 let pool = AgentPool::new(provider, test_config());
747 assert_eq!(pool.role_count(), 0);
748 }
749
750 #[test]
751 fn add_role_and_get_role() {
752 let provider = Arc::new(MockProvider::new(vec![]));
753 let mut pool = AgentPool::new(provider, test_config());
754
755 let role = AgentRole {
756 name: "tester".into(),
757 system_prompt: "You test things.".into(),
758 max_steps: 5,
759 allowed_tools: vec!["Bash".into()],
760 };
761 pool.add_role(role.clone());
762
763 let retrieved = pool.get_role("tester").unwrap();
764 assert_eq!(retrieved.name, "tester");
765 assert_eq!(retrieved.system_prompt, "You test things.");
766 assert_eq!(retrieved.max_steps, 5);
767 assert_eq!(retrieved.allowed_tools, vec!["Bash"]);
768 }
769
770 #[test]
771 fn role_names_returns_all_registered() {
772 let provider = Arc::new(MockProvider::new(vec![]));
773 let mut pool = AgentPool::new(provider, test_config());
774
775 pool.add_role(AgentRole {
776 name: "alpha".into(),
777 system_prompt: "A".into(),
778 max_steps: 1,
779 allowed_tools: vec![],
780 });
781 pool.add_role(AgentRole {
782 name: "beta".into(),
783 system_prompt: "B".into(),
784 max_steps: 2,
785 allowed_tools: vec![],
786 });
787
788 let mut names = pool.role_names();
789 names.sort();
790 assert_eq!(names, vec!["alpha", "beta"]);
791 assert_eq!(pool.role_count(), 2);
792 }
793
794 #[tokio::test]
795 async fn run_with_unknown_role_returns_error() {
796 let provider = Arc::new(MockProvider::new(vec![]));
797 let pool = AgentPool::new(provider, test_config());
798
799 let result = pool.run_with_role("nonexistent", "do something").await;
800 assert!(result.is_err());
801 let err = result.unwrap_err();
802 assert!(err.to_string().contains("unknown role"));
803 }
804
805 #[tokio::test]
806 async fn run_with_role_succeeds_with_mock() {
807 let provider = Arc::new(MockProvider::new(vec![Completion {
808 content: "Plan: step 1, step 2, step 3".into(),
809 tool_calls: vec![],
810 finish_reason: Some("stop".into()),
811 usage: None,
812 reasoning_content: None,
813 }]));
814
815 let mut pool = AgentPool::new(provider, test_config());
816 pool.add_role(AgentRole {
817 name: "planner".into(),
818 system_prompt: "You are a planner.".into(),
819 max_steps: 5,
820 allowed_tools: vec![],
821 });
822
823 let outcome = pool.run_with_role("planner", "plan a task").await.unwrap();
824 assert_eq!(
825 outcome.finish_reason,
826 crate::agent::FinishReason::NoMoreToolCalls
827 );
828 assert!(outcome.final_text.unwrap().contains("Plan:"));
829 }
830
831 #[test]
832 fn default_roles_returns_three_roles() {
833 let roles = default_roles();
834 assert_eq!(roles.len(), 3);
835
836 let names: Vec<&str> = roles.iter().map(|r| r.name.as_str()).collect();
837 assert!(names.contains(&"planner"));
838 assert!(names.contains(&"coder"));
839 assert!(names.contains(&"reviewer"));
840 }
841
842 #[tokio::test]
843 async fn shared_memory_set_and_get() {
844 let mem = SharedMemory::new();
845 mem.set("goal".into(), "build feature X".into(), "planner".into())
846 .await;
847
848 let entry = mem.get("goal").await.unwrap();
849 assert_eq!(entry.key, "goal");
850 assert_eq!(entry.value, "build feature X");
851 assert_eq!(entry.author, "planner");
852 assert!(entry.timestamp > 0);
853 }
854
855 #[tokio::test]
856 async fn shared_memory_keys() {
857 let mem = SharedMemory::new();
858 mem.set("a".into(), "1".into(), "agent1".into()).await;
859 mem.set("b".into(), "2".into(), "agent2".into()).await;
860
861 let mut keys = mem.keys().await;
862 keys.sort();
863 assert_eq!(keys, vec!["a", "b"]);
864 assert_eq!(mem.len().await, 2);
865 assert!(!mem.is_empty().await);
866 }
867
868 #[tokio::test]
869 async fn shared_memory_remove() {
870 let mem = SharedMemory::new();
871 mem.set("tmp".into(), "val".into(), "x".into()).await;
872 assert!(mem.get("tmp").await.is_some());
873
874 let removed = mem.remove("tmp").await;
875 assert!(removed);
876 assert!(mem.get("tmp").await.is_none());
877
878 let removed_again = mem.remove("tmp").await;
880 assert!(!removed_again);
881 }
882
883 #[tokio::test]
884 async fn shared_memory_to_context_string() {
885 let mem = SharedMemory::new();
886 mem.set("status".into(), "in-progress".into(), "coder".into())
887 .await;
888
889 let ctx = mem.to_context_string().await;
890 assert!(ctx.contains("[Shared Memory]"));
891 assert!(ctx.contains("status = in-progress (by coder)"));
892 }
893
894 #[tokio::test]
895 async fn shared_memory_empty_context_returns_empty() {
896 let mem = SharedMemory::new();
897 let ctx = mem.to_context_string().await;
898 assert!(ctx.is_empty());
899 }
900
901 #[tokio::test]
902 async fn agent_pool_includes_memory_context() {
903 let provider = Arc::new(MockProvider::new(vec![Completion {
904 content: "I see the shared memory context.".into(),
905 tool_calls: vec![],
906 finish_reason: Some("stop".into()),
907 usage: None,
908 reasoning_content: None,
909 }]));
910
911 let mut pool = AgentPool::new(provider, test_config());
912 pool.add_role(AgentRole {
913 name: "worker".into(),
914 system_prompt: "You are a worker.".into(),
915 max_steps: 5,
916 allowed_tools: vec![],
917 });
918
919 pool.memory()
921 .set("plan".into(), "step 1 done".into(), "planner".into())
922 .await;
923
924 let outcome = pool.run_with_role("worker", "continue work").await.unwrap();
925 assert_eq!(
926 outcome.finish_reason,
927 crate::agent::FinishReason::NoMoreToolCalls
928 );
929 assert!(outcome.final_text.is_some());
931 }
932
933 fn make_msg(from: &str, to: &str, content: &str, msg_type: MessageType) -> AgentMessage {
936 AgentMessage {
937 id: generate_message_id(),
938 from: from.to_string(),
939 to: to.to_string(),
940 content: content.to_string(),
941 msg_type,
942 timestamp: now_timestamp(),
943 }
944 }
945
946 #[tokio::test]
947 async fn message_bus_send_and_inbox() {
948 let bus = MessageBus::new();
949 let msg = make_msg("planner", "coder", "implement feature X", MessageType::Task);
950 bus.send(msg).await;
951
952 let inbox = bus.inbox("coder").await;
953 assert_eq!(inbox.len(), 1);
954 assert_eq!(inbox[0].content, "implement feature X");
955 assert_eq!(inbox[0].from, "planner");
956 assert_eq!(inbox[0].msg_type, MessageType::Task);
957
958 let empty = bus.inbox("reviewer").await;
960 assert!(empty.is_empty());
961 }
962
963 #[tokio::test]
964 async fn message_bus_outbox() {
965 let bus = MessageBus::new();
966 bus.send(make_msg(
967 "coder",
968 "reviewer",
969 "done coding",
970 MessageType::Result,
971 ))
972 .await;
973 bus.send(make_msg(
974 "coder",
975 "planner",
976 "need clarification",
977 MessageType::Question,
978 ))
979 .await;
980 bus.send(make_msg(
981 "planner",
982 "coder",
983 "here is the plan",
984 MessageType::Task,
985 ))
986 .await;
987
988 let outbox = bus.outbox("coder").await;
989 assert_eq!(outbox.len(), 2);
990 assert!(outbox.iter().all(|m| m.from == "coder"));
991
992 let planner_outbox = bus.outbox("planner").await;
993 assert_eq!(planner_outbox.len(), 1);
994 }
995
996 #[tokio::test]
997 async fn message_bus_broadcast_reaches_all() {
998 let bus = MessageBus::new();
999 bus.send(make_msg(
1000 "admin",
1001 "broadcast",
1002 "system update",
1003 MessageType::Broadcast,
1004 ))
1005 .await;
1006
1007 let coder_inbox = bus.inbox("coder").await;
1008 let reviewer_inbox = bus.inbox("reviewer").await;
1009 let planner_inbox = bus.inbox("planner").await;
1010
1011 assert_eq!(coder_inbox.len(), 1);
1012 assert_eq!(reviewer_inbox.len(), 1);
1013 assert_eq!(planner_inbox.len(), 1);
1014 assert_eq!(coder_inbox[0].content, "system update");
1015 }
1016
1017 #[tokio::test]
1018 async fn message_bus_subscribe_receives() {
1019 let bus = MessageBus::new();
1020 let mut rx = bus.subscribe("coder").await;
1021
1022 let msg = make_msg("planner", "coder", "task for you", MessageType::Task);
1024 bus.send(msg).await;
1025
1026 let received = rx.recv().await.unwrap();
1027 assert_eq!(received.content, "task for you");
1028 assert_eq!(received.from, "planner");
1029 }
1030
1031 #[tokio::test]
1032 async fn message_bus_history() {
1033 let bus = MessageBus::new();
1034 bus.send(make_msg("a", "b", "msg1", MessageType::Task))
1035 .await;
1036 bus.send(make_msg("b", "a", "msg2", MessageType::Result))
1037 .await;
1038 bus.send(make_msg("a", "broadcast", "msg3", MessageType::Broadcast))
1039 .await;
1040
1041 let history = bus.history().await;
1042 assert_eq!(history.len(), 3);
1043 assert_eq!(history[0].content, "msg1");
1044 assert_eq!(history[1].content, "msg2");
1045 assert_eq!(history[2].content, "msg3");
1046 }
1047
1048 #[tokio::test]
1049 async fn message_bus_clear() {
1050 let bus = MessageBus::new();
1051 bus.send(make_msg("a", "b", "hello", MessageType::Task))
1052 .await;
1053 assert_eq!(bus.history().await.len(), 1);
1054
1055 bus.clear().await;
1056 assert!(bus.history().await.is_empty());
1057 assert!(bus.inbox("b").await.is_empty());
1058 }
1059
1060 #[tokio::test]
1061 async fn agent_pool_send_task_convenience() {
1062 let provider = Arc::new(MockProvider::new(vec![]));
1063 let pool = AgentPool::new(provider, test_config());
1064
1065 pool.send_task("planner", "coder", "build module Y").await;
1066 pool.send_result("coder", "planner", "module Y complete")
1067 .await;
1068
1069 let inbox = pool.bus().inbox("coder").await;
1070 assert_eq!(inbox.len(), 1);
1071 assert_eq!(inbox[0].content, "build module Y");
1072 assert_eq!(inbox[0].msg_type, MessageType::Task);
1073
1074 let planner_inbox = pool.bus().inbox("planner").await;
1075 assert_eq!(planner_inbox.len(), 1);
1076 assert_eq!(planner_inbox[0].content, "module Y complete");
1077 assert_eq!(planner_inbox[0].msg_type, MessageType::Result);
1078
1079 let history = pool.bus().history().await;
1080 assert_eq!(history.len(), 2);
1081 }
1082
1083 #[tokio::test]
1084 async fn message_bus_evicts_oldest_on_overflow() {
1085 let bus = MessageBus::with_capacity(3);
1086 for i in 0..5 {
1087 bus.send(make_msg(
1088 &format!("a{i}"),
1089 "broadcast",
1090 &format!("msg-{i}"),
1091 MessageType::Feedback,
1092 ))
1093 .await;
1094 }
1095 let history = bus.history().await;
1096 let contents: Vec<_> = history.iter().map(|m| m.content.clone()).collect();
1097 assert_eq!(contents, vec!["msg-2", "msg-3", "msg-4"]);
1099 assert_eq!(history.len(), 3);
1100 }
1101
1102 #[tokio::test]
1103 async fn message_bus_default_capacity_is_bounded() {
1104 let bus = MessageBus::new();
1105 assert_eq!(MESSAGE_BUS_CAPACITY, 1000);
1106 for i in 0..10 {
1108 bus.send(make_msg("a", "b", &format!("m{i}"), MessageType::Task))
1109 .await;
1110 }
1111 assert_eq!(bus.history().await.len(), 10);
1112 }
1113
1114 #[tokio::test]
1117 async fn shared_memory_all_returns_all_entries() {
1118 let mem = SharedMemory::new();
1119 mem.set("x".into(), "1".into(), "a".into()).await;
1120 mem.set("y".into(), "2".into(), "b".into()).await;
1121 let all = mem.all().await;
1122 assert_eq!(all.len(), 2, "all() must return every stored entry");
1123 let mut keys: Vec<String> = all.iter().map(|e| e.key.clone()).collect();
1124 keys.sort();
1125 assert_eq!(keys, vec!["x", "y"]);
1126 }
1127
1128 #[tokio::test]
1129 async fn shared_memory_all_empty_returns_empty_vec() {
1130 let mem = SharedMemory::new();
1131 assert!(
1132 mem.all().await.is_empty(),
1133 "all() on empty store must return empty vec"
1134 );
1135 }
1136
1137 #[test]
1140 fn agent_mode_parse_single() {
1141 assert_eq!(AgentMode::parse("single"), Some(AgentMode::Single));
1142 }
1143
1144 #[test]
1145 fn agent_mode_parse_parallel() {
1146 assert_eq!(AgentMode::parse("parallel"), Some(AgentMode::Parallel));
1147 }
1148
1149 #[test]
1150 fn agent_mode_parse_sequential() {
1151 assert_eq!(AgentMode::parse("sequential"), Some(AgentMode::Sequential));
1152 }
1153
1154 #[test]
1155 fn agent_mode_parse_unknown_returns_none() {
1156 assert_eq!(AgentMode::parse(""), None);
1157 assert_eq!(AgentMode::parse("xyzzy"), None);
1158 }
1159
1160 #[test]
1163 fn agent_pool_remove_role_returns_true_when_present() {
1164 let provider = Arc::new(MockProvider::new(vec![]));
1165 let mut pool = AgentPool::new(provider, test_config());
1166 pool.add_role(AgentRole {
1167 name: "tmp".into(),
1168 system_prompt: "X".into(),
1169 max_steps: 1,
1170 allowed_tools: vec![],
1171 });
1172 assert!(
1173 pool.remove_role("tmp"),
1174 "remove existing role must return true"
1175 );
1176 assert_eq!(pool.role_count(), 0);
1177 }
1178
1179 #[test]
1180 fn agent_pool_remove_role_returns_false_when_absent() {
1181 let provider = Arc::new(MockProvider::new(vec![]));
1182 let mut pool = AgentPool::new(provider, test_config());
1183 assert!(
1184 !pool.remove_role("nonexistent"),
1185 "remove absent role must return false"
1186 );
1187 }
1188
1189 #[test]
1192 fn coordinator_system_prompt_is_nonempty_and_not_placeholder() {
1193 let prompt = coordinator_system_prompt();
1194 assert!(!prompt.is_empty(), "coordinator prompt must not be empty");
1195 assert_ne!(
1196 prompt, "xyzzy",
1197 "coordinator prompt must not be xyzzy placeholder"
1198 );
1199 assert!(
1200 prompt.contains("coordinator"),
1201 "coordinator prompt must mention 'coordinator'"
1202 );
1203 }
1204
1205 #[test]
1206 fn coordinator_system_prompt_teaches_worker_briefing() {
1207 let prompt = coordinator_system_prompt();
1212 assert!(
1213 prompt.contains("Writing worker prompts"),
1214 "coordinator prompt must have a 'Writing worker prompts' section"
1215 );
1216 assert!(
1217 prompt.contains("Never delegate understanding"),
1218 "coordinator prompt must forbid delegating understanding to workers"
1219 );
1220 assert!(
1222 prompt.contains("cannot see this conversation"),
1223 "coordinator prompt must state workers cannot see the conversation"
1224 );
1225 assert!(
1227 prompt.contains("send_message") && prompt.contains("agent"),
1228 "coordinator prompt must cover both continue (send_message) and spawn (agent)"
1229 );
1230 assert!(
1232 prompt.contains("prove"),
1233 "coordinator prompt must raise the verification bar beyond 'it exists'"
1234 );
1235 }
1236
1237 #[test]
1240 fn default_roles_have_expected_steps_and_tools() {
1241 let roles = default_roles();
1242 assert!(!roles.is_empty(), "default_roles must return non-empty vec");
1243
1244 let planner = roles
1245 .iter()
1246 .find(|r| r.name == "planner")
1247 .expect("planner role");
1248 let coder = roles
1249 .iter()
1250 .find(|r| r.name == "coder")
1251 .expect("coder role");
1252 let reviewer = roles
1253 .iter()
1254 .find(|r| r.name == "reviewer")
1255 .expect("reviewer role");
1256
1257 assert!(planner.max_steps > 0);
1259 assert!(coder.max_steps > 0);
1260 assert!(reviewer.max_steps > 0);
1261
1262 assert!(
1264 !reviewer.allowed_tools.is_empty(),
1265 "reviewer must have allowed_tools"
1266 );
1267
1268 assert!(!planner.system_prompt.is_empty());
1270 assert!(!coder.system_prompt.is_empty());
1271 assert!(!reviewer.system_prompt.is_empty());
1272 }
1273
1274 #[test]
1277 fn register_subagent_if_enabled_noop_when_disabled() {
1278 let provider = Arc::new(MockProvider::new(vec![]));
1279 let config = test_config(); let tools = crate::tools::ToolRegistry::local();
1281 let initial_names = tools.names();
1282 let result = register_subagent_if_enabled(tools, &config, provider);
1283 assert_eq!(
1284 result.names(),
1285 initial_names,
1286 "disabled subagent must not register any additional tools"
1287 );
1288 }
1289}