1use async_trait::async_trait;
27use serde_json::{json, Value};
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex};
30use tokio::sync::{mpsc, RwLock};
31
32use crate::agent::FinishReason;
33use crate::error::{Error, Result};
34use crate::llm::{ChatProvider, ToolSpec};
35use crate::multi::{AgentManifest, AgentMode, AgentPool, WorkerManifestEntry};
36use crate::runtime::{AgentRuntime, AgentRuntimeBuilder};
37use crate::tasks::{TaskId, TaskRegistry, TaskState};
38use crate::tools::agent_defs::AgentDefinitions;
39use crate::tools::edit::EditTool;
40use crate::tools::fs::{ReadFile, ReadFileState, WriteFile};
41use crate::tools::send_message::{ListWorkersTool, SendMessageTool, WorkerRegistry};
42use crate::tools::{PermissionHook, Tool, ToolRegistry, ToolSideEffect};
43
44pub struct SharedMemoryRead {
50 pool: Arc<RwLock<AgentPool>>,
51}
52
53impl SharedMemoryRead {
54 pub fn new(pool: Arc<RwLock<AgentPool>>) -> Self {
55 Self { pool }
56 }
57}
58
59#[async_trait]
60impl Tool for SharedMemoryRead {
61 fn spec(&self) -> ToolSpec {
62 ToolSpec {
63 name: "shared_memory_read".into(),
64 description: "Read a value from the shared memory store. Use this to retrieve context published by other workers.".into(),
65 parameters: json!({
66 "type": "object",
67 "properties": {
68 "key": {
69 "type": "string",
70 "description": "The key to read from shared memory."
71 }
72 },
73 "required": ["key"]
74 }),
75 }
76 }
77
78 fn side_effect_class(&self) -> ToolSideEffect {
79 ToolSideEffect::ReadOnly
80 }
81
82 async fn execute(&self, arguments: Value) -> Result<String> {
83 let key = arguments["key"]
84 .as_str()
85 .ok_or_else(|| Error::BadToolArgs {
86 name: "shared_memory_read".into(),
87 message: "missing required parameter: key".to_string(),
88 })?;
89
90 let pool = self.pool.read().await;
91 match pool.memory().get(key).await {
92 Some(entry) => Ok(entry.value),
93 None => Ok(format!("Key '{key}' not found in shared memory.")),
94 }
95 }
96}
97
98pub struct SharedMemoryWrite {
104 pool: Arc<RwLock<AgentPool>>,
105 author: String,
106}
107
108impl SharedMemoryWrite {
109 pub fn new(pool: Arc<RwLock<AgentPool>>, author: String) -> Self {
110 Self { pool, author }
111 }
112}
113
114#[async_trait]
115impl Tool for SharedMemoryWrite {
116 fn spec(&self) -> ToolSpec {
117 ToolSpec {
118 name: "shared_memory_write".into(),
119 description: "Write a value to the shared memory store. Other workers can read this via shared_memory_read. Use this to publish findings, decisions, or intermediate results.".into(),
120 parameters: json!({
121 "type": "object",
122 "properties": {
123 "key": {
124 "type": "string",
125 "description": "The key under which to store the value."
126 },
127 "value": {
128 "type": "string",
129 "description": "The value to store."
130 }
131 },
132 "required": ["key", "value"]
133 }),
134 }
135 }
136
137 fn side_effect_class(&self) -> ToolSideEffect {
138 ToolSideEffect::External
139 }
140
141 async fn execute(&self, arguments: Value) -> Result<String> {
142 let key = arguments["key"]
143 .as_str()
144 .ok_or_else(|| Error::BadToolArgs {
145 name: "shared_memory_write".into(),
146 message: "missing required parameter: key".to_string(),
147 })?
148 .to_string();
149 let value = arguments["value"]
150 .as_str()
151 .ok_or_else(|| Error::BadToolArgs {
152 name: "shared_memory_write".into(),
153 message: "missing required parameter: value".to_string(),
154 })?
155 .to_string();
156
157 self.pool
158 .read()
159 .await
160 .memory()
161 .set(key.clone(), value, self.author.clone())
162 .await;
163 Ok(format!("Stored '{key}' in shared memory."))
164 }
165}
166
167pub struct WorkerHandle {
181 pub tx: mpsc::UnboundedSender<String>,
184 pub task_id: TaskId,
186}
187
188pub type WorkerTable = Arc<Mutex<HashMap<String, Arc<WorkerHandle>>>>;
197
198struct WorkerDeregisterGuard {
209 workers: WorkerTable,
210 worker_key: String,
211}
212
213impl Drop for WorkerDeregisterGuard {
214 fn drop(&mut self) {
215 self.workers
216 .lock()
217 .unwrap_or_else(|e| e.into_inner())
218 .remove(&self.worker_key);
219 }
220}
221
222pub struct AgentTool {
227 workspace: std::path::PathBuf,
228 provider: Arc<dyn ChatProvider>,
229 all_tools: ToolRegistry,
230 max_depth: usize,
231 current_depth: usize,
232 permission_hook: Option<Arc<dyn PermissionHook>>,
233 registry: Option<WorkerRegistry>,
234 pool: Option<Arc<RwLock<AgentPool>>>,
235 task_registry: Arc<TaskRegistry>,
236 definitions: Option<AgentDefinitions>,
237 workers: WorkerTable,
241}
242
243impl AgentTool {
244 pub fn new(
245 workspace: impl Into<std::path::PathBuf>,
246 provider: Arc<dyn ChatProvider>,
247 all_tools: ToolRegistry,
248 max_depth: usize,
249 current_depth: usize,
250 permission_hook: Option<Arc<dyn PermissionHook>>,
251 ) -> Self {
252 Self {
253 workspace: workspace.into(),
254 provider,
255 all_tools,
256 max_depth,
257 current_depth,
258 permission_hook,
259 registry: None,
260 pool: None,
261 task_registry: Arc::new(TaskRegistry::new()),
262 definitions: None,
263 workers: Arc::new(Mutex::new(HashMap::new())),
264 }
265 }
266
267 pub fn with_registry(mut self, registry: WorkerRegistry) -> Self {
269 self.registry = Some(registry);
270 self
271 }
272
273 pub fn with_pool(mut self, pool: Arc<RwLock<AgentPool>>) -> Self {
275 self.pool = Some(pool);
276 self
277 }
278
279 pub fn with_task_registry(mut self, reg: Arc<TaskRegistry>) -> Self {
283 self.task_registry = reg;
284 self
285 }
286
287 pub fn with_definitions(mut self, defs: AgentDefinitions) -> Self {
290 self.definitions = Some(defs);
291 self
292 }
293
294 pub fn with_workers(mut self, workers: WorkerTable) -> Self {
298 self.workers = workers;
299 self
300 }
301
302 fn build_sub_registry(&self, tool_names: &[String]) -> ToolRegistry {
315 let sub_read_state = Arc::new(Mutex::new(ReadFileState::new()));
316 let mut reg = self
319 .all_tools
320 .with_same_transport()
321 .with_read_file_state(sub_read_state.clone());
322 for name in tool_names {
323 let tool: Arc<dyn Tool> = match name.as_str() {
327 "Read" => {
328 Arc::new(ReadFile::new(&self.workspace).with_read_state(sub_read_state.clone()))
329 }
330 "Edit" => {
331 Arc::new(EditTool::new(&self.workspace).with_read_state(sub_read_state.clone()))
332 }
333 "Write" => Arc::new(
334 WriteFile::new(&self.workspace).with_read_state(sub_read_state.clone()),
335 ),
336 _ => {
337 if let Some(t) = self.all_tools.get(name) {
338 t
339 } else {
340 continue;
341 }
342 }
343 };
344 reg = reg.register(tool);
345 }
346 reg
347 }
348
349 fn default_tool_names() -> Vec<String> {
351 vec![
352 "Read".to_string(),
353 "Grep".to_string(),
354 "Glob".to_string(),
355 "WebFetch".to_string(),
356 "SearchFiles".to_string(),
357 ]
358 }
359
360 async fn build_worker_runtime(
371 &self,
372 worker_id: &str,
373 entry: &WorkerManifestEntry,
374 max_steps: usize,
375 child_depth: usize,
376 ) -> Result<AgentRuntime> {
377 let tool_names: Vec<String> = if entry.allowed_tools.is_empty() {
379 Self::default_tool_names()
380 } else {
381 entry.allowed_tools.clone()
382 };
383
384 let mut sub_registry = self.build_sub_registry(&tool_names);
386
387 let mut child_agent = AgentTool::new(
389 &self.workspace,
390 self.provider.clone(),
391 self.all_tools.fork(),
392 self.max_depth,
393 child_depth,
394 self.permission_hook.clone(),
395 );
396 if let Some(reg) = &self.registry {
397 child_agent = child_agent.with_registry(reg.clone());
398 }
399 if let Some(pool) = &self.pool {
400 child_agent = child_agent.with_pool(pool.clone());
401 }
402 child_agent = child_agent
405 .with_task_registry(self.task_registry.clone())
406 .with_workers(self.workers.clone());
407 sub_registry = sub_registry.register(Arc::new(child_agent));
408
409 if let Some(pool) = &self.pool {
411 sub_registry = sub_registry.register(Arc::new(SharedMemoryRead::new(pool.clone())));
412 sub_registry = sub_registry.register(Arc::new(SharedMemoryWrite::new(
413 pool.clone(),
414 worker_id.to_string(),
415 )));
416 }
417
418 if let Some(reg) = &self.registry {
420 sub_registry = sub_registry.register(Arc::new(SendMessageTool::new(
421 reg.clone(),
422 self.task_registry.clone(),
423 self.workers.clone(),
424 )));
425 sub_registry = sub_registry.register(Arc::new(ListWorkersTool::new(
426 reg.clone(),
427 self.task_registry.clone(),
428 )));
429 }
430
431 let mut system_prompt = entry.system_prompt.clone();
433 if let Some(pool) = &self.pool {
434 let memory_ctx = pool.read().await.memory().to_context_string().await;
435 if !memory_ctx.is_empty() {
436 system_prompt = format!("{}\n\n{}", system_prompt, memory_ctx);
437 }
438 }
439
440 AgentRuntimeBuilder::new()
441 .llm(self.provider.clone())
442 .tools(sub_registry)
443 .max_steps(max_steps)
444 .system_prompt(system_prompt)
445 .build()
446 .map_err(|e| Error::Tool {
447 name: "agent".into(),
448 call_id: None,
449 message: format!("failed to build worker '{}' runtime: {e}", worker_id),
450 })
451 }
452
453 async fn run_worker(
459 &self,
460 worker_id: &str,
461 entry: &WorkerManifestEntry,
462 prompt: &str,
463 max_steps: usize,
464 child_depth: usize,
465 ) -> Result<String> {
466 let mut runtime = self
467 .build_worker_runtime(worker_id, entry, max_steps, child_depth)
468 .await?;
469 let outcome = runtime.run(prompt).await.map_err(|e| Error::Tool {
470 name: "agent".into(),
471 call_id: None,
472 message: format!("worker '{}' failed: {e}", worker_id),
473 })?;
474
475 let finish_label = match outcome.finish_reason {
476 FinishReason::NoMoreToolCalls => "NoMoreToolCalls".to_string(),
477 FinishReason::BudgetExceeded => "BudgetExceeded".to_string(),
478 FinishReason::ProviderStop(r) => r,
479 FinishReason::Stuck { .. } => "Stuck".to_string(),
480 FinishReason::TranscriptLimit { .. } => "TranscriptLimit".to_string(),
481 FinishReason::Cancelled => "Cancelled".to_string(),
482 FinishReason::PermissionDenialLimit => "PermissionDenialLimit".to_string(),
483 FinishReason::WallClockExceeded { .. } => "WallClockExceeded".to_string(),
484 };
485
486 let final_text = outcome
487 .final_text
488 .unwrap_or_else(|| "(no final message)".to_string());
489
490 Ok(format!(
491 "[worker '{worker_id}' finished: {finish_label}]\n{final_text}"
492 ))
493 }
494
495 async fn spawn_background_worker(
504 &self,
505 worker_id: &str,
506 entry: &WorkerManifestEntry,
507 prompt: &str,
508 max_steps: usize,
509 child_depth: usize,
510 ) -> Result<TaskId> {
511 let runtime = self
512 .build_worker_runtime(worker_id, entry, max_steps, child_depth)
513 .await?;
514 let runtime = Arc::new(tokio::sync::Mutex::new(runtime));
515
516 let (state, task_id) =
518 TaskState::new(format!("worker '{worker_id}'"), String::new(), worker_id);
519 let state = self.task_registry.register(state).await;
520
521 let (tx, mut rx) = mpsc::unbounded_channel::<String>();
524 let _ = tx.send(prompt.to_string());
525
526 let handle = Arc::new(WorkerHandle {
529 tx,
530 task_id: task_id.clone(),
531 });
532 self.workers
533 .lock()
534 .unwrap_or_else(|e| e.into_inner())
535 .insert(worker_id.to_string(), handle.clone());
536
537 let state_for_task = state.clone();
539 let workers = self.workers.clone();
550 let worker_key = worker_id.to_string();
551 let join_handle = tokio::spawn(async move {
552 let _dereg_guard = WorkerDeregisterGuard {
555 workers: workers.clone(),
556 worker_key: worker_key.clone(),
557 };
558 while let Some(msg) = rx.recv().await {
560 let mut rt = runtime.lock().await;
561 match rt.run(&msg).await {
562 Ok(outcome) => {
563 let text = outcome
564 .final_text
565 .unwrap_or_else(|| "(no final message)".to_string());
566 let _ = state_for_task
567 .append_output(format!("--- turn ---\n{text}"))
568 .await;
569 }
570 Err(e) => {
571 state_for_task
572 .mark_failed(format!("worker turn failed: {e}"))
573 .await;
574 return;
575 }
576 }
577 }
578 state_for_task
580 .mark_completed("worker finished".to_string())
581 .await;
582 });
583
584 state.set_handle(join_handle).await;
586
587 Ok(task_id)
588 }
589
590 async fn execute_single(
596 &self,
597 manifest: &AgentManifest,
598 prompt: &str,
599 max_steps: usize,
600 child_depth: usize,
601 ) -> Result<String> {
602 if manifest.len() != 1 {
603 return Err(Error::BadToolArgs {
604 name: "agent".into(),
605 message: format!(
606 "mode 'single' requires exactly one manifest entry, got {}",
607 manifest.len()
608 ),
609 });
610 }
611 let (worker_id, entry) = manifest.iter().next().ok_or_else(|| Error::BadToolArgs {
617 name: "agent".into(),
618 message: "mode 'single' requires exactly one manifest entry".to_string(),
619 })?;
620 self.run_worker(worker_id, entry, prompt, max_steps, child_depth)
621 .await
622 }
623
624 async fn execute_parallel(
626 &self,
627 manifest: &AgentManifest,
628 prompt: &str,
629 max_steps: usize,
630 child_depth: usize,
631 ) -> Result<String> {
632 if manifest.is_empty() {
633 return Err(Error::BadToolArgs {
634 name: "agent".into(),
635 message: "mode 'parallel' requires at least one manifest entry".to_string(),
636 });
637 }
638
639 if let Some(reg) = &self.registry {
641 for worker_id in manifest.keys() {
642 reg.register(worker_id).await;
643 }
644 }
645
646 let workspace = self.workspace.clone();
650 let provider = self.provider.clone();
651 let all_tools = self.all_tools.fork();
652 let max_depth = self.max_depth;
653 let permission_hook = self.permission_hook.clone();
654 let registry = self.registry.clone();
655 let pool = self.pool.clone();
656 let definitions = self.definitions.clone();
657 let workers = self.workers.clone();
658
659 let mut handles: Vec<tokio::task::JoinHandle<(String, Result<String>)>> = Vec::new();
661 for (worker_id, entry) in manifest.iter() {
662 let worker_id = worker_id.clone();
663 let entry = entry.clone();
664 let prompt = prompt.to_string();
665 let workspace = workspace.clone();
666 let provider = provider.clone();
667 let all_tools = all_tools.clone();
668 let permission_hook = permission_hook.clone();
669 let registry = registry.clone();
670 let pool = pool.clone();
671 let definitions = definitions.clone();
672 let workers = workers.clone();
673
674 handles.push(tokio::spawn(async move {
675 let agent = AgentTool {
676 workspace,
677 provider,
678 all_tools,
679 max_depth,
680 current_depth: child_depth,
681 permission_hook,
682 registry: registry.clone(),
683 pool: pool.clone(),
684 task_registry: Arc::new(crate::tasks::TaskRegistry::new()),
685 definitions,
686 workers,
687 };
688 let result = agent
689 .run_worker(&worker_id, &entry, &prompt, max_steps, child_depth)
690 .await;
691
692 if let Some(reg) = ®istry {
694 reg.deregister(&worker_id).await;
695 }
696
697 (worker_id, result)
698 }));
699 }
700
701 let outcomes = futures_util::future::join_all(handles).await;
703
704 let mut results: Vec<(String, String)> = Vec::new();
706 for outcome in outcomes {
707 match outcome {
708 Ok((id, Ok(text))) => results.push((id, text)),
709 Ok((id, Err(e))) => {
710 results.push((id, format!("ERROR: {e}")));
711 }
712 Err(join_err) => {
713 results.push(("(unknown)".into(), format!("join error: {join_err}")));
714 }
715 }
716 }
717
718 results.sort_by(|a, b| a.0.cmp(&b.0));
720
721 Ok(results
722 .into_iter()
723 .map(|(id, text)| format!("=== {id} ===\n{text}"))
724 .collect::<Vec<_>>()
725 .join("\n\n"))
726 }
727
728 async fn execute_sequential(
730 &self,
731 manifest: &AgentManifest,
732 prompt: &str,
733 max_steps: usize,
734 child_depth: usize,
735 ) -> Result<String> {
736 if manifest.is_empty() {
737 return Err(Error::BadToolArgs {
738 name: "agent".into(),
739 message: "mode 'sequential' requires at least one manifest entry".to_string(),
740 });
741 }
742
743 let mut keys: Vec<&String> = manifest.keys().collect();
745 keys.sort();
746
747 let mut result_parts = Vec::new();
748 for worker_id in &keys {
749 let entry = &manifest[*worker_id];
750 let result = self
751 .run_worker(worker_id, entry, prompt, max_steps, child_depth)
752 .await?;
753 result_parts.push(result);
754 }
755
756 Ok(result_parts.join("\n\n"))
757 }
758
759 fn parse_manifest(&self, value: &Value) -> Result<AgentManifest, Error> {
771 let obj = value.as_object().ok_or_else(|| Error::BadToolArgs {
772 name: "agent".into(),
773 message:
774 "`manifest` must be a JSON object mapping worker_id → {system_prompt | definition, allowed_tools?}"
775 .to_string(),
776 })?;
777
778 if obj.is_empty() {
779 return Err(Error::BadToolArgs {
780 name: "agent".into(),
781 message: "`manifest` must have at least one entry".to_string(),
782 });
783 }
784
785 let mut manifest = AgentManifest::new();
786 for (worker_id, entry_val) in obj {
787 let entry_obj = entry_val.as_object().ok_or_else(|| Error::BadToolArgs {
788 name: "agent".into(),
789 message: format!(
790 "manifest entry '{}' must be an object with `system_prompt` or `definition` (and optional `allowed_tools`)",
791 worker_id
792 ),
793 })?;
794
795 let def_name = entry_obj.get("definition").and_then(|v| v.as_str());
797
798 let (base_system_prompt, base_allowed_tools) = if let Some(name) = def_name {
799 let defs = self.definitions.as_ref().ok_or_else(|| {
800 Error::BadToolArgs {
801 name: "agent".into(),
802 message: format!(
803 "manifest entry '{}' references definition '{}', but no agent definitions are loaded (missing .recursive/agents/)",
804 worker_id, name
805 ),
806 }
807 })?;
808 let def = defs.get(name).ok_or_else(|| Error::BadToolArgs {
809 name: "agent".into(),
810 message: format!(
811 "manifest entry '{}' references unknown definition '{}'. Available: {}",
812 worker_id,
813 name,
814 defs.iter()
815 .map(|(n, _)| n.as_str())
816 .collect::<Vec<_>>()
817 .join(", ")
818 ),
819 })?;
820 (def.system_prompt.clone(), def.allowed_tools.clone())
821 } else {
822 (String::new(), Vec::new())
823 };
824
825 let system_prompt = entry_obj
827 .get("system_prompt")
828 .and_then(|v| v.as_str())
829 .map(String::from)
830 .or({
831 if !base_system_prompt.is_empty() {
832 Some(base_system_prompt)
833 } else {
834 None
835 }
836 })
837 .ok_or_else(|| Error::BadToolArgs {
838 name: "agent".into(),
839 message: format!(
840 "manifest entry '{}' requires a `system_prompt` string or a `definition` reference",
841 worker_id
842 ),
843 })?;
844
845 let allowed_tools: Vec<String> = entry_obj
847 .get("allowed_tools")
848 .and_then(|v| v.as_array())
849 .map(|arr| {
850 arr.iter()
851 .filter_map(|v| v.as_str().map(String::from))
852 .collect()
853 })
854 .or({
855 if !base_allowed_tools.is_empty() {
856 Some(base_allowed_tools)
857 } else {
858 None
859 }
860 })
861 .unwrap_or_default();
862
863 manifest.insert(
864 worker_id.clone(),
865 WorkerManifestEntry {
866 system_prompt,
867 allowed_tools,
868 },
869 );
870 }
871 Ok(manifest)
872 }
873}
874
875#[async_trait]
876impl Tool for AgentTool {
877 fn spec(&self) -> ToolSpec {
878 ToolSpec {
879 name: "agent".into(),
880 description: format!(
889 "{}\n\n{}",
890 concat!(
891 "Spawn one or more specialist sub-agents (workers) defined by a `manifest`. ",
892 "Use `mode: \"single\"` for one worker, `mode: \"parallel\"` for concurrent ",
893 "execution, or `mode: \"sequential\"` when each worker depends on the previous. ",
894 "Set `background: true` (with `single`) to spawn a long-lived worker that ",
895 "returns a `task_id` immediately and can be continued across turns. ",
896 "Workers have restricted tool sets and isolated transcripts."
897 ),
898 crate::multi::coordinator_system_prompt()
899 ),
900 parameters: json!({
901 "type": "object",
902 "properties": {
903 "mode": {
904 "type": "string",
905 "enum": ["single", "parallel", "sequential"],
906 "description": "Execution mode. 'single' spawns exactly one worker (manifest must have one entry). 'parallel' runs all workers concurrently. 'sequential' runs workers one after another.",
907 "default": "single"
908 },
909 "manifest": {
910 "type": "object",
911 "description": "Map of worker_id → { system_prompt, allowed_tools? }. Each worker gets its own system prompt and restricted tool set.",
912 "additionalProperties": {
913 "type": "object",
914 "properties": {
915 "system_prompt": {
916 "type": "string",
917 "description": "System prompt defining the worker's role, behavior, and output format."
918 },
919 "allowed_tools": {
920 "type": "array",
921 "items": { "type": "string" },
922 "description": "Optional tool allowlist. Empty/absent defaults to read-only tools: Read, Grep, Glob, WebFetch, SearchFiles."
923 }
924 },
925 "required": ["system_prompt"]
926 }
927 },
928 "prompt": {
929 "type": "string",
930 "description": "The task description / goal for the worker(s). Every worker receives the same prompt."
931 },
932 "max_steps": {
933 "type": "integer",
934 "description": "Maximum steps per worker (default 30, max 100).",
935 "default": 30
936 },
937 "background": {
938 "type": "boolean",
939 "description": "If true (and mode is 'single'), spawn the worker in the background and return its task_id immediately instead of waiting for it to finish. The worker runs on a long-lived runtime; use send_message(task_id=...) to run follow-up turns, and task_get/task_output/task_stop to inspect or cancel.",
940 "default": false
941 }
942 },
943 "required": ["manifest", "prompt"]
944 }),
945 }
946 }
947
948 fn side_effect_class(&self) -> ToolSideEffect {
949 ToolSideEffect::External
953 }
954
955 async fn execute(&self, arguments: Value) -> Result<String> {
956 let mode_str = arguments
958 .get("mode")
959 .and_then(|v| v.as_str())
960 .unwrap_or("single");
961 let mode = AgentMode::parse(mode_str).ok_or_else(|| Error::BadToolArgs {
962 name: "agent".into(),
963 message: format!(
964 "unknown mode '{mode_str}'. Valid modes: single, parallel, sequential"
965 ),
966 })?;
967
968 let prompt = arguments["prompt"]
970 .as_str()
971 .ok_or_else(|| Error::BadToolArgs {
972 name: "agent".into(),
973 message: "missing required parameter: prompt".to_string(),
974 })?;
975
976 let max_steps = arguments["max_steps"].as_i64().unwrap_or(30).clamp(1, 100) as usize;
978
979 let background = arguments
981 .get("background")
982 .and_then(|v| v.as_bool())
983 .unwrap_or(false);
984
985 let manifest = self.parse_manifest(&arguments["manifest"])?;
987
988 if self.current_depth >= self.max_depth {
990 return Ok(format!(
991 "ERROR: agent depth limit reached (max_depth={}). Cannot spawn deeper agents.",
992 self.max_depth
993 ));
994 }
995
996 let child_depth = self.current_depth + 1;
997
998 if background && matches!(mode, AgentMode::Single) {
1000 if manifest.len() != 1 {
1001 return Err(Error::BadToolArgs {
1002 name: "agent".into(),
1003 message: format!(
1004 "background=true with mode 'single' requires exactly one manifest entry, got {}",
1005 manifest.len()
1006 ),
1007 });
1008 }
1009 let (worker_id, entry) = manifest.iter().next().ok_or_else(|| Error::BadToolArgs {
1010 name: "agent".into(),
1011 message: "background=true requires one manifest entry".to_string(),
1012 })?;
1013 let task_id = self
1014 .spawn_background_worker(worker_id, entry, prompt, max_steps, child_depth)
1015 .await?;
1016 return Ok(format!(
1017 "Background worker '{worker_id}' spawned as task '{task_id}'. \
1018 Use send_message(task_id=\"{task_id}\", ...) to run follow-up turns, \
1019 task_get/task_output to inspect, or task_stop to cancel."
1020 ));
1021 }
1022
1023 match mode {
1025 AgentMode::Single => {
1026 self.execute_single(&manifest, prompt, max_steps, child_depth)
1027 .await
1028 }
1029 AgentMode::Parallel => {
1030 self.execute_parallel(&manifest, prompt, max_steps, child_depth)
1031 .await
1032 }
1033 AgentMode::Sequential => {
1034 self.execute_sequential(&manifest, prompt, max_steps, child_depth)
1035 .await
1036 }
1037 }
1038 }
1039}
1040
1041#[cfg(test)]
1046mod tests {
1047 use super::*;
1048 use crate::llm::{Completion, MockProvider};
1049 use crate::tools::{
1050 GlobTool, LocalTransport, ReadFile, SearchFiles, ToolTransport, WebFetch, WriteFile,
1051 };
1052
1053 fn mock_provider(script: Vec<Completion>) -> Arc<dyn ChatProvider> {
1054 Arc::new(MockProvider::new(script))
1055 }
1056
1057 fn full_tool_registry(workspace: &std::path::Path) -> ToolRegistry {
1058 let transport: Arc<dyn ToolTransport> = Arc::new(LocalTransport);
1059 ToolRegistry::new(transport)
1060 .register(Arc::new(ReadFile::new(workspace)))
1061 .register(Arc::new(SearchFiles::new(workspace)))
1062 .register(Arc::new(WriteFile::new(workspace)))
1063 .register(Arc::new(GlobTool::new(workspace)))
1064 .register(Arc::new(WebFetch::new()))
1065 }
1066
1067 #[tokio::test]
1068 async fn agent_single_mode_basic() {
1069 let provider = mock_provider(vec![Completion {
1070 content: "done".to_string(),
1071 tool_calls: vec![],
1072 finish_reason: Some("stop".into()),
1073 usage: None,
1074 reasoning_content: None,
1075 }]);
1076
1077 let tmp = tempfile::tempdir().unwrap();
1078 let all_tools = full_tool_registry(tmp.path());
1079 let agent = AgentTool::new(tmp.path(), provider, all_tools, 2, 0, None);
1080
1081 let result = agent
1082 .execute(json!({
1083 "mode": "single",
1084 "manifest": {
1085 "helper": {
1086 "system_prompt": "You are a helper.",
1087 "allowed_tools": ["Read"]
1088 }
1089 },
1090 "prompt": "say hi"
1091 }))
1092 .await
1093 .unwrap();
1094
1095 assert!(result.contains("helper"));
1096 assert!(result.contains("NoMoreToolCalls"));
1097 assert!(result.contains("done"));
1098 }
1099
1100 #[tokio::test]
1101 async fn agent_depth_limit() {
1102 let provider = mock_provider(vec![]);
1103 let tmp = tempfile::tempdir().unwrap();
1104 let all_tools = full_tool_registry(tmp.path());
1105 let agent = AgentTool::new(tmp.path(), provider, all_tools, 2, 2, None);
1107
1108 let result = agent
1109 .execute(json!({
1110 "manifest": {
1111 "w": { "system_prompt": "hi" }
1112 },
1113 "prompt": "test"
1114 }))
1115 .await
1116 .unwrap();
1117
1118 assert!(result.contains("depth limit reached"));
1119 }
1120
1121 #[tokio::test]
1122 async fn agent_sequential() {
1123 let provider = mock_provider(vec![
1124 Completion {
1125 content: "first".to_string(),
1126 tool_calls: vec![],
1127 finish_reason: Some("stop".into()),
1128 usage: None,
1129 reasoning_content: None,
1130 },
1131 Completion {
1132 content: "second".to_string(),
1133 tool_calls: vec![],
1134 finish_reason: Some("stop".into()),
1135 usage: None,
1136 reasoning_content: None,
1137 },
1138 ]);
1139
1140 let tmp = tempfile::tempdir().unwrap();
1141 let all_tools = full_tool_registry(tmp.path());
1142 let agent = AgentTool::new(tmp.path(), provider, all_tools, 2, 0, None);
1143
1144 let result = agent
1145 .execute(json!({
1146 "mode": "sequential",
1147 "manifest": {
1148 "a": { "system_prompt": "A", "allowed_tools": ["Read"] },
1149 "b": { "system_prompt": "B", "allowed_tools": ["Read"] }
1150 },
1151 "prompt": "process"
1152 }))
1153 .await
1154 .unwrap();
1155
1156 assert!(result.contains("first"));
1157 assert!(result.contains("second"));
1158 }
1159
1160 #[test]
1161 fn test_agent_mode_parse() {
1162 assert_eq!(AgentMode::parse("single"), Some(AgentMode::Single));
1163 assert_eq!(AgentMode::parse("parallel"), Some(AgentMode::Parallel));
1164 assert_eq!(AgentMode::parse("sequential"), Some(AgentMode::Sequential));
1165 assert_eq!(AgentMode::parse("unknown"), None);
1166 }
1167
1168 #[test]
1169 fn parse_manifest_empty_object_is_error() {
1170 let tmp = tempfile::tempdir().unwrap();
1172 let all_tools = full_tool_registry(tmp.path());
1173 let agent = AgentTool::new(tmp.path(), mock_provider(vec![]), all_tools, 2, 0, None);
1174 let err = agent.parse_manifest(&json!({})).unwrap_err();
1175 let msg = format!("{err}");
1176 assert!(
1177 msg.contains("at least one entry"),
1178 "empty manifest must error; got: {msg}"
1179 );
1180 }
1181
1182 #[test]
1183 fn parse_manifest_non_object_is_error() {
1184 let tmp = tempfile::tempdir().unwrap();
1186 let all_tools = full_tool_registry(tmp.path());
1187 let agent = AgentTool::new(tmp.path(), mock_provider(vec![]), all_tools, 2, 0, None);
1188 let err = agent.parse_manifest(&json!("not an object")).unwrap_err();
1189 let msg = format!("{err}");
1190 assert!(
1191 msg.contains("must be a JSON object"),
1192 "non-object manifest must error; got: {msg}"
1193 );
1194 }
1195
1196 #[test]
1201 fn parse_manifest_resolves_definition() {
1202 let tmp = tempfile::tempdir().unwrap();
1203 let all_tools = full_tool_registry(tmp.path());
1204
1205 let agents_dir = tmp.path().join(".recursive").join("agents");
1207 std::fs::create_dir_all(&agents_dir).unwrap();
1208 std::fs::write(
1209 agents_dir.join("reviewer.md"),
1210 "---
1211name: reviewer
1212system_prompt: 'You review code.'
1213allowed_tools:
1214 - Read
1215 - Glob
1216---
1217",
1218 )
1219 .unwrap();
1220
1221 let defs = AgentDefinitions::load(tmp.path()).unwrap();
1222 let agent = AgentTool::new(tmp.path(), mock_provider(vec![]), all_tools, 2, 0, None)
1223 .with_definitions(defs);
1224
1225 let manifest = agent
1226 .parse_manifest(&json!({
1227 "rev": { "definition": "reviewer" }
1228 }))
1229 .unwrap();
1230
1231 let entry = manifest.get("rev").unwrap();
1232 assert_eq!(entry.system_prompt, "You review code.");
1233 assert_eq!(entry.allowed_tools, vec!["Read", "Glob"]);
1234 }
1235
1236 #[test]
1237 fn parse_manifest_definition_with_inline_override() {
1238 let tmp = tempfile::tempdir().unwrap();
1239 let all_tools = full_tool_registry(tmp.path());
1240
1241 let agents_dir = tmp.path().join(".recursive").join("agents");
1242 std::fs::create_dir_all(&agents_dir).unwrap();
1243 std::fs::write(
1244 agents_dir.join("helper.md"),
1245 "---
1246name: helper
1247system_prompt: 'Base prompt.'
1248allowed_tools:
1249 - Read
1250---
1251",
1252 )
1253 .unwrap();
1254
1255 let defs = AgentDefinitions::load(tmp.path()).unwrap();
1256 let agent = AgentTool::new(tmp.path(), mock_provider(vec![]), all_tools, 2, 0, None)
1257 .with_definitions(defs);
1258
1259 let manifest = agent
1261 .parse_manifest(&json!({
1262 "h": {
1263 "definition": "helper",
1264 "system_prompt": "Overridden prompt.",
1265 "allowed_tools": ["Write"]
1266 }
1267 }))
1268 .unwrap();
1269
1270 let entry = manifest.get("h").unwrap();
1271 assert_eq!(entry.system_prompt, "Overridden prompt.");
1272 assert_eq!(entry.allowed_tools, vec!["Write"]);
1273 }
1274
1275 #[test]
1276 fn parse_manifest_unknown_definition_is_error() {
1277 let tmp = tempfile::tempdir().unwrap();
1278 let all_tools = full_tool_registry(tmp.path());
1279
1280 let defs = AgentDefinitions::load(tmp.path()).unwrap();
1282 let agent = AgentTool::new(tmp.path(), mock_provider(vec![]), all_tools, 2, 0, None)
1283 .with_definitions(defs);
1284
1285 let err = agent
1286 .parse_manifest(&json!({
1287 "w": { "definition": "nonexistent" }
1288 }))
1289 .unwrap_err();
1290
1291 let msg = format!("{err}");
1292 assert!(msg.contains("unknown definition"), "got: {msg}");
1293 assert!(msg.contains("nonexistent"), "got: {msg}");
1294 }
1295
1296 #[test]
1297 fn parse_manifest_definition_without_registry_is_error() {
1298 let tmp = tempfile::tempdir().unwrap();
1299 let all_tools = full_tool_registry(tmp.path());
1300
1301 let agent = AgentTool::new(tmp.path(), mock_provider(vec![]), all_tools, 2, 0, None);
1303
1304 let err = agent
1305 .parse_manifest(&json!({
1306 "w": { "definition": "some-agent" }
1307 }))
1308 .unwrap_err();
1309
1310 let msg = format!("{err}");
1311 assert!(
1312 msg.contains("no agent definitions are loaded"),
1313 "got: {msg}"
1314 );
1315 }
1316
1317 #[test]
1318 fn parse_manifest_neither_definition_nor_system_prompt_is_error() {
1319 let tmp = tempfile::tempdir().unwrap();
1320 let all_tools = full_tool_registry(tmp.path());
1321 let agent = AgentTool::new(tmp.path(), mock_provider(vec![]), all_tools, 2, 0, None);
1322
1323 let err = agent
1324 .parse_manifest(&json!({
1325 "w": { "allowed_tools": ["Read"] }
1326 }))
1327 .unwrap_err();
1328
1329 let msg = format!("{err}");
1330 assert!(
1331 msg.contains("requires a `system_prompt` string or a `definition` reference"),
1332 "got: {msg}"
1333 );
1334 }
1335
1336 #[tokio::test]
1337 async fn agent_single_mode_with_definition() {
1338 let provider = mock_provider(vec![Completion {
1339 content: "review done".to_string(),
1340 tool_calls: vec![],
1341 finish_reason: Some("stop".into()),
1342 usage: None,
1343 reasoning_content: None,
1344 }]);
1345
1346 let tmp = tempfile::tempdir().unwrap();
1347 let all_tools = full_tool_registry(tmp.path());
1348
1349 let agents_dir = tmp.path().join(".recursive").join("agents");
1351 std::fs::create_dir_all(&agents_dir).unwrap();
1352 std::fs::write(
1353 agents_dir.join("inspector.md"),
1354 "---
1355name: inspector
1356system_prompt: 'Inspect thoroughly.'
1357allowed_tools:
1358 - Read
1359---
1360",
1361 )
1362 .unwrap();
1363
1364 let defs = AgentDefinitions::load(tmp.path()).unwrap();
1365 let agent =
1366 AgentTool::new(tmp.path(), provider, all_tools, 2, 0, None).with_definitions(defs);
1367
1368 let result = agent
1369 .execute(json!({
1370 "mode": "single",
1371 "manifest": {
1372 "inspector": { "definition": "inspector" }
1373 },
1374 "prompt": "inspect this"
1375 }))
1376 .await
1377 .unwrap();
1378
1379 assert!(result.contains("inspector"));
1380 assert!(result.contains("review done"));
1381 }
1382
1383 #[tokio::test]
1384 async fn background_worker_returns_task_id_and_runs() {
1385 let provider = mock_provider(vec![Completion {
1389 content: "first turn done".to_string(),
1390 tool_calls: vec![],
1391 finish_reason: Some("stop".into()),
1392 usage: None,
1393 reasoning_content: None,
1394 }]);
1395 let tmp = tempfile::tempdir().unwrap();
1396 let all_tools = full_tool_registry(tmp.path());
1397 let task_registry = Arc::new(TaskRegistry::new());
1398 let worker_table: WorkerTable = Arc::new(Mutex::new(HashMap::new()));
1399 let agent = AgentTool::new(tmp.path(), provider, all_tools, 2, 0, None)
1400 .with_task_registry(task_registry.clone())
1401 .with_workers(worker_table);
1402
1403 let result = agent
1404 .execute(json!({
1405 "mode": "single",
1406 "background": true,
1407 "manifest": {
1408 "w1": { "system_prompt": "You are a helper." }
1409 },
1410 "prompt": "do the first thing"
1411 }))
1412 .await
1413 .unwrap();
1414
1415 assert!(result.contains("spawned as task"), "{result}");
1417 assert!(result.contains("send_message"), "{result}");
1418
1419 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
1422 let tasks = task_registry.list().await;
1423 assert_eq!(tasks.len(), 1, "exactly one task should be registered");
1424 let _ = task_registry.drain_output(&tasks[0].id).await;
1425 let output = tasks[0].output_snapshot().await;
1426 let joined = output.join("\n");
1427 assert!(joined.contains("first turn done"), "output was: {joined}");
1428 }
1429
1430 #[tokio::test]
1431 async fn send_message_continues_background_worker() {
1432 let provider = mock_provider(vec![
1436 Completion {
1437 content: "turn one".to_string(),
1438 tool_calls: vec![],
1439 finish_reason: Some("stop".into()),
1440 usage: None,
1441 reasoning_content: None,
1442 },
1443 Completion {
1444 content: "turn two".to_string(),
1445 tool_calls: vec![],
1446 finish_reason: Some("stop".into()),
1447 usage: None,
1448 reasoning_content: None,
1449 },
1450 ]);
1451 let tmp = tempfile::tempdir().unwrap();
1452 let all_tools = full_tool_registry(tmp.path());
1453 let task_registry = Arc::new(TaskRegistry::new());
1454 let worker_registry = WorkerRegistry::new();
1455 let worker_table: WorkerTable = Arc::new(Mutex::new(HashMap::new()));
1456 let agent = AgentTool::new(tmp.path(), provider, all_tools, 2, 0, None)
1457 .with_task_registry(task_registry.clone())
1458 .with_registry(worker_registry.clone())
1459 .with_workers(worker_table.clone());
1460
1461 let spawn_result = agent
1463 .execute(json!({
1464 "mode": "single",
1465 "background": true,
1466 "manifest": {
1467 "cw": { "system_prompt": "You are a helper." }
1468 },
1469 "prompt": "turn one please"
1470 }))
1471 .await
1472 .unwrap();
1473 let task_id = spawn_result
1474 .split("task '")
1475 .nth(1)
1476 .and_then(|s| s.split('\'').next())
1477 .expect("task id in spawn result")
1478 .to_string();
1479
1480 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
1482
1483 let send_tool = SendMessageTool::new(
1485 worker_registry.clone(),
1486 task_registry.clone(),
1487 worker_table.clone(),
1488 );
1489 let send_result = send_tool
1490 .execute(json!({
1491 "task_id": task_id,
1492 "message": "now turn two"
1493 }))
1494 .await
1495 .unwrap();
1496 assert!(
1497 send_result.contains("new turn"),
1498 "send_message should report a new turn: {send_result}"
1499 );
1500
1501 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
1503 let task = task_registry
1504 .get(&TaskId(task_id.clone()))
1505 .await
1506 .expect("task still registered");
1507 let _ = task_registry.drain_output(&task.id).await;
1508 let joined = task.output_snapshot().await.join("\n");
1509 assert!(joined.contains("turn one"), "missing turn one: {joined}");
1510 assert!(joined.contains("turn two"), "missing turn two: {joined}");
1511 }
1512
1513 #[tokio::test]
1514 async fn background_worker_deregisters_from_worker_table_on_exit() {
1515 let provider: Arc<dyn ChatProvider> =
1524 Arc::new(MockProvider::new(vec![]).with_errors(vec![Error::Llm {
1525 provider: "mock".into(),
1526 message: "injected failure for deregistration test".into(),
1527 }]));
1528 let tmp = tempfile::tempdir().unwrap();
1529 let all_tools = full_tool_registry(tmp.path());
1530 let task_registry = Arc::new(TaskRegistry::new());
1531 let worker_table: WorkerTable = Arc::new(Mutex::new(HashMap::new()));
1532 let agent = AgentTool::new(tmp.path(), provider, all_tools, 2, 0, None)
1533 .with_task_registry(task_registry.clone())
1534 .with_workers(worker_table.clone());
1535
1536 let spawn_result = agent
1538 .execute(json!({
1539 "mode": "single",
1540 "background": true,
1541 "manifest": {
1542 "w1": { "system_prompt": "You are a helper." }
1543 },
1544 "prompt": "do the thing"
1545 }))
1546 .await
1547 .unwrap();
1548 let task_id = spawn_result
1549 .split("task '")
1550 .nth(1)
1551 .and_then(|s| s.split('\'').next())
1552 .expect("task id in spawn result")
1553 .to_string();
1554
1555 assert!(
1557 worker_table
1558 .lock()
1559 .unwrap_or_else(|e| e.into_inner())
1560 .contains_key("w1"),
1561 "entry should exist while the worker is alive"
1562 );
1563
1564 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
1570 loop {
1571 if !worker_table
1572 .lock()
1573 .unwrap_or_else(|e| e.into_inner())
1574 .contains_key("w1")
1575 {
1576 break;
1577 }
1578 if std::time::Instant::now() > deadline {
1579 panic!("worker table entry for 'w1' was not removed after the worker exited");
1580 }
1581 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1582 }
1583
1584 let task = task_registry
1587 .get(&TaskId(task_id))
1588 .await
1589 .expect("task still registered");
1590 let status = task.status().await;
1591 assert!(
1592 matches!(status, crate::tasks::TaskStatus::Failed),
1593 "worker task should have failed, was {status:?}"
1594 );
1595 }
1596
1597 #[tokio::test]
1598 async fn task_stop_deregisters_aborted_worker_from_worker_table() {
1599 let provider = mock_provider(vec![Completion {
1607 content: "first turn done".to_string(),
1608 tool_calls: vec![],
1609 finish_reason: Some("stop".into()),
1610 usage: None,
1611 reasoning_content: None,
1612 }]);
1613 let tmp = tempfile::tempdir().unwrap();
1614 let all_tools = full_tool_registry(tmp.path());
1615 let task_registry = Arc::new(TaskRegistry::new());
1616 let worker_registry = WorkerRegistry::new();
1617 let worker_table: WorkerTable = Arc::new(Mutex::new(HashMap::new()));
1618 let agent = AgentTool::new(tmp.path(), provider, all_tools, 2, 0, None)
1619 .with_task_registry(task_registry.clone())
1620 .with_registry(worker_registry.clone())
1621 .with_workers(worker_table.clone());
1622
1623 let spawn_result = agent
1625 .execute(json!({
1626 "mode": "single",
1627 "background": true,
1628 "manifest": {
1629 "w1": { "system_prompt": "You are a helper." }
1630 },
1631 "prompt": "do the first thing"
1632 }))
1633 .await
1634 .unwrap();
1635 let task_id = spawn_result
1636 .split("task '")
1637 .nth(1)
1638 .and_then(|s| s.split('\'').next())
1639 .expect("task id in spawn result")
1640 .to_string();
1641
1642 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
1645 assert!(
1646 worker_table
1647 .lock()
1648 .unwrap_or_else(|e| e.into_inner())
1649 .contains_key("w1"),
1650 "entry should exist while the worker is alive"
1651 );
1652
1653 let stopped = task_registry.stop(&TaskId(task_id.clone())).await;
1657 assert!(stopped, "task_stop must find a live handle to abort");
1658
1659 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
1663 loop {
1664 if !worker_table
1665 .lock()
1666 .unwrap_or_else(|e| e.into_inner())
1667 .contains_key("w1")
1668 {
1669 break;
1670 }
1671 if std::time::Instant::now() > deadline {
1672 panic!("worker table entry for 'w1' was not removed after task_stop aborted the worker");
1673 }
1674 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1675 }
1676
1677 let task = task_registry
1680 .get(&TaskId(task_id.clone()))
1681 .await
1682 .expect("task still registered");
1683 assert_eq!(task.status().await, crate::tasks::TaskStatus::Stopped);
1684
1685 let send_tool = SendMessageTool::new(
1688 worker_registry.clone(),
1689 task_registry.clone(),
1690 worker_table.clone(),
1691 );
1692 let send_result = send_tool
1693 .execute(json!({
1694 "worker_id": "w1",
1695 "message": "are you there?"
1696 }))
1697 .await
1698 .unwrap();
1699 assert!(
1700 send_result.contains("not found"),
1701 "send_message to a stopped worker must report not found, got: {send_result}"
1702 );
1703 assert!(
1704 !send_result.contains("new turn"),
1705 "send_message must not claim it will drive a new turn on a stopped worker: {send_result}"
1706 );
1707
1708 let send_result = send_tool
1711 .execute(json!({
1712 "task_id": task_id,
1713 "message": "anyone home?"
1714 }))
1715 .await
1716 .unwrap();
1717 assert!(
1718 !send_result.contains("will run it as a new turn"),
1719 "stopped worker must not claim a new turn, got: {send_result}"
1720 );
1721 }
1722}