From c288ec2ae15a34fa09cc221af4bedb4f9fd47983 Mon Sep 17 00:00:00 2001 From: Zhiping <2716057626@qq.com> Date: Tue, 12 May 2026 14:15:52 +0800 Subject: [PATCH 1/2] fix(subagent): prevent review agents from spawning sub-agents (#1489) Adds a disallowed-tools filter to `SubAgentToolRegistry::tools_for_model()` so that Review-type agents cannot call `agent_spawn`. This prevents recursive delegation where a review agent spawns further sub-agents instead of performing the review itself. Unlike `allowed_tools()` (which is deprecated and intentionally unused for default agent types), this disallowed filter operates after the full parent registry inheritance and only removes `agent_spawn` for review agents. --- crates/tui/src/tools/subagent/mod.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/tui/src/tools/subagent/mod.rs b/crates/tui/src/tools/subagent/mod.rs index abfb25bd..8c44877a 100644 --- a/crates/tui/src/tools/subagent/mod.rs +++ b/crates/tui/src/tools/subagent/mod.rs @@ -2879,7 +2879,7 @@ async fn run_subagent( unavailable_tools.join(", ") )); } - let tools = tool_registry.tools_for_model(); + let tools = tool_registry.tools_for_model(&agent_type); if let Some(mb) = runtime.mailbox.as_ref() { let _ = mb.send(MailboxMessage::started(&agent_id, agent_type.clone())); } @@ -3838,14 +3838,27 @@ impl SubAgentToolRegistry { } } - fn tools_for_model(&self) -> Vec { + fn tools_for_model(&self, agent_type: &SubAgentType) -> Vec { + let disallowed = match agent_type { + // Review agents should not spawn sub-agents (#1489). + SubAgentType::Review => &["agent_spawn"][..], + _ => &[][..], + }; let api_tools = self.registry.to_api_tools(); - match &self.allowed_tools { + let filtered = match &self.allowed_tools { None => api_tools, Some(list) => api_tools .into_iter() .filter(|tool| list.contains(&tool.name)) - .collect(), + .collect::>(), + }; + if disallowed.is_empty() { + filtered + } else { + filtered + .into_iter() + .filter(|tool| !disallowed.contains(&tool.name.as_str())) + .collect() } } From 3113bdf7a4b9c5e6d7d3f870887aca91fec90eb9 Mon Sep 17 00:00:00 2001 From: Zhiping <2716057626@qq.com> Date: Tue, 12 May 2026 14:30:04 +0800 Subject: [PATCH 2/2] test(subagent): add test for review agent excluding agent_spawn --- crates/tui/src/tools/subagent/tests.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/tui/src/tools/subagent/tests.rs b/crates/tui/src/tools/subagent/tests.rs index 97ae5f8c..f278d22c 100644 --- a/crates/tui/src/tools/subagent/tests.rs +++ b/crates/tui/src/tools/subagent/tests.rs @@ -638,6 +638,26 @@ fn test_subagent_tool_registry_reports_unavailable_tools() { ); } +#[test] +fn test_review_agent_tools_exclude_agent_spawn() { + let tmp = tempdir().expect("tempdir"); + let mut runtime = stub_runtime(); + runtime.context = ToolContext::new(tmp.path().to_path_buf()); + // None = full parent tool inheritance (the default for builtin types). + let registry = SubAgentToolRegistry::new( + runtime, + None, + Arc::new(Mutex::new(TodoList::new())), + Arc::new(Mutex::new(PlanState::default())), + ); + let tools = registry.tools_for_model(&SubAgentType::Review); + let names: Vec<_> = tools.iter().map(|t| t.name.as_str()).collect(); + assert!( + !names.contains(&"agent_spawn"), + "Review agent must not have agent_spawn; tools: {names:?}" + ); +} + #[tokio::test] async fn test_wait_for_result_reports_timeout_when_still_running() { let manager = Arc::new(RwLock::new(SubAgentManager::new(PathBuf::from("."), 2)));