From 63438b64a9731b7adfd99a5e84f4c988bf33eca8 Mon Sep 17 00:00:00 2001 From: Li Xiangtian <1193027052@qq.com> Date: Fri, 5 Jun 2026 19:46:11 +0800 Subject: [PATCH 1/2] feat: allow disabling delegation tools --- core/src/agent_api/capabilities.rs | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/src/agent_api/capabilities.rs b/core/src/agent_api/capabilities.rs index d7004ee..46287fc 100644 --- a/core/src/agent_api/capabilities.rs +++ b/core/src/agent_api/capabilities.rs @@ -18,6 +18,8 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; +const DISABLE_DELEGATION_TOOLS_ENV: &str = "A3S_CODE_DISABLE_DELEGATION_TOOLS"; + pub(super) struct SessionCapabilityInput<'a> { pub(super) code_config: &'a CodeConfig, pub(super) base_config: &'a AgentConfig, @@ -163,6 +165,9 @@ fn register_task_capability( use crate::tools::register_task_with_mcp; let registry = AgentRegistry::new(); + let disable_delegation_tools = disable_delegation_tools_from_env( + std::env::var(DISABLE_DELEGATION_TOOLS_ENV).ok().as_deref(), + ); let built_in_agent_dirs = built_in_agent_dirs(workspace); for dir in code_config .agent_dirs @@ -178,6 +183,10 @@ fn register_task_capability( registry.register_worker(worker.clone()); } + if disable_delegation_tools { + return Arc::new(registry); + } + let parent_context = ChildRunContext { security_provider: opts.security_provider.clone(), hook_engine: None, @@ -203,6 +212,17 @@ fn register_task_capability( registry } +fn disable_delegation_tools_from_env(value: Option<&str>) -> bool { + value + .map(|value| { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + }) + .unwrap_or(false) +} + fn built_in_agent_dirs(workspace: &Path) -> Vec { let mut dirs = Vec::new(); if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) { @@ -265,6 +285,26 @@ fn group_mcp_tools_by_server(all_tools: Vec<(String, McpTool)>) -> HashMap Date: Mon, 22 Jun 2026 17:20:17 +0800 Subject: [PATCH 2/2] refactor delegation tool gating into config --- CHANGELOG.md | 5 ++++ README.md | 14 ++++++--- core/src/agent_api.rs | 6 ++++ core/src/agent_api/capabilities.rs | 41 +++------------------------ core/src/agent_api/session_builder.rs | 12 +++----- core/src/agent_api/session_config.rs | 23 +++++++++++++++ core/src/agent_api/session_options.rs | 15 ++++++++++ core/src/agent_api/tests.rs | 23 +++++++++++++++ core/src/config/loader.rs | 11 +++++++ core/src/config/mod.rs | 9 ++++++ core/src/config/tests.rs | 2 ++ 11 files changed, 112 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5130d..975a26b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -292,6 +292,11 @@ fields, new `SessionStore` trait methods with default no-op impls). global `auto_parallel` kill switch. Setting `auto_parallel = false` disables automatic parallel child-agent fan-out while keeping manual `parallel_task` available. +- Added `auto_delegation.allow_manual_delegation` and + `SessionOptions::with_manual_delegation_enabled(...)` so hosts can hide the + model-visible `task` / `parallel_task` tools per session while preserving the + child-agent registry for introspection and worker registration. This is an + operational cost/debug control, not a security sandbox. - Added `max_parallel_tasks` as the shared sibling fan-out limit for `parallel_task`, delegated plan waves, and safe parallel write batches. - Added a reusable ordered parallel executor so concurrent child results remain diff --git a/README.md b/README.md index 8ef256c..7ff6cf8 100644 --- a/README.md +++ b/README.md @@ -1527,10 +1527,11 @@ skill_dirs = ["./skills"] mcp_servers = [] auto_delegation { - enabled = false - auto_parallel = false - min_confidence = 0.72 - max_tasks = 4 + enabled = false + auto_parallel = false + allow_manual_delegation = true + min_confidence = 0.72 + max_tasks = 4 } ahp = { @@ -1549,6 +1550,11 @@ safe parallel write batches. `auto_delegation.enabled` controls Claude Code-style automatic subagent delegation. `auto_parallel = false` is a global kill switch for automatic parallel child-agent fan-out; manual `parallel_task` remains available. +Set `allow_manual_delegation = false` to hide the model-visible `task` and +`parallel_task` tools for cost control or debugging while preserving the child +agent registry for introspection and host-managed worker registration. This is +not a security sandbox: the parent agent may still use other registered tools, +MCP servers, or skills. --- diff --git a/core/src/agent_api.rs b/core/src/agent_api.rs index 0977e57..9f607dd 100644 --- a/core/src/agent_api.rs +++ b/core/src/agent_api.rs @@ -258,6 +258,12 @@ pub struct SessionOptions { pub max_parallel_tasks: Option, /// Per-session automatic subagent delegation override. pub auto_delegation: Option, + /// Per-session switch for model-visible manual child-agent tools. + /// + /// This overlays the effective automatic delegation config instead of + /// replacing it, so callers can hide `task` / `parallel_task` while + /// preserving other delegation settings. + pub manual_delegation_enabled: Option, /// Per-session kill switch for automatic parallel child-agent fan-out. /// /// This overlays the effective automatic delegation config instead of diff --git a/core/src/agent_api/capabilities.rs b/core/src/agent_api/capabilities.rs index 46287fc..1bb2507 100644 --- a/core/src/agent_api/capabilities.rs +++ b/core/src/agent_api/capabilities.rs @@ -18,8 +18,6 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; -const DISABLE_DELEGATION_TOOLS_ENV: &str = "A3S_CODE_DISABLE_DELEGATION_TOOLS"; - pub(super) struct SessionCapabilityInput<'a> { pub(super) code_config: &'a CodeConfig, pub(super) base_config: &'a AgentConfig, @@ -165,9 +163,7 @@ fn register_task_capability( use crate::tools::register_task_with_mcp; let registry = AgentRegistry::new(); - let disable_delegation_tools = disable_delegation_tools_from_env( - std::env::var(DISABLE_DELEGATION_TOOLS_ENV).ok().as_deref(), - ); + let auto_delegation = super::session_config::resolve_auto_delegation_config(code_config, opts); let built_in_agent_dirs = built_in_agent_dirs(workspace); for dir in code_config .agent_dirs @@ -183,7 +179,9 @@ fn register_task_capability( registry.register_worker(worker.clone()); } - if disable_delegation_tools { + if !auto_delegation.allow_manual_delegation { + // Keep the registry populated for introspection and host-managed worker + // registration even when the model-visible delegation tools are hidden. return Arc::new(registry); } @@ -212,17 +210,6 @@ fn register_task_capability( registry } -fn disable_delegation_tools_from_env(value: Option<&str>) -> bool { - value - .map(|value| { - matches!( - value.trim().to_ascii_lowercase().as_str(), - "1" | "true" | "yes" | "on" - ) - }) - .unwrap_or(false) -} - fn built_in_agent_dirs(workspace: &Path) -> Vec { let mut dirs = Vec::new(); if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) { @@ -285,26 +272,6 @@ fn group_mcp_tools_by_server(all_tools: Vec<(String, McpTool)>) -> HashMap SessionOptions { @@ -135,13 +137,7 @@ pub(super) fn build_agent_session( let init_warning = resolved_memory.init_warning; let base = agent.config.clone(); - let mut auto_delegation = opts - .auto_delegation - .clone() - .unwrap_or_else(|| base.auto_delegation.clone()); - if let Some(auto_parallel) = opts.auto_parallel_delegation { - auto_delegation.auto_parallel = auto_parallel; - } + let auto_delegation = resolve_auto_delegation_config(&agent.code_config, opts); let config = AgentConfig { prompt_slots, tools: tool_defs, diff --git a/core/src/agent_api/session_config.rs b/core/src/agent_api/session_config.rs index 9eb4ace..e4acbb0 100644 --- a/core/src/agent_api/session_config.rs +++ b/core/src/agent_api/session_config.rs @@ -5,6 +5,29 @@ use crate::llm::LlmClient; use anyhow::Context; use std::sync::Arc; +pub(super) fn resolve_auto_delegation_config( + code_config: &CodeConfig, + opts: &SessionOptions, +) -> crate::config::AutoDelegationConfig { + let mut auto_delegation = if let Some(config) = opts.auto_delegation.clone() { + config + } else { + let mut config = code_config.auto_delegation.clone(); + if let Some(auto_parallel) = code_config.auto_parallel { + config.auto_parallel = auto_parallel; + } + config + }; + if let Some(enabled) = opts.manual_delegation_enabled { + auto_delegation.allow_manual_delegation = enabled; + } + if let Some(auto_parallel) = opts.auto_parallel_delegation { + auto_delegation.auto_parallel = auto_parallel; + } + + auto_delegation +} + pub(super) fn resolve_session_llm_client( code_config: &CodeConfig, opts: &SessionOptions, diff --git a/core/src/agent_api/session_options.rs b/core/src/agent_api/session_options.rs index 0ce84bc..9d0a45f 100644 --- a/core/src/agent_api/session_options.rs +++ b/core/src/agent_api/session_options.rs @@ -54,6 +54,7 @@ impl std::fmt::Debug for SessionOptions { .field("max_tool_rounds", &self.max_tool_rounds) .field("max_parallel_tasks", &self.max_parallel_tasks) .field("auto_delegation", &self.auto_delegation) + .field("manual_delegation_enabled", &self.manual_delegation_enabled) .field("auto_parallel_delegation", &self.auto_parallel_delegation) .field("prompt_slots", &self.prompt_slots.is_some()) .finish() @@ -494,6 +495,20 @@ impl SessionOptions { self } + /// Enable or disable model-visible manual child-agent tools for this session. + /// + /// When false, `task` and `parallel_task` are not registered in the session + /// tool surface. Worker agents remain registered for introspection and hosts + /// that manage them directly. This is for cost control or debugging; it is + /// not a security sandbox for the parent agent. + pub fn with_manual_delegation_enabled(mut self, enabled: bool) -> Self { + if let Some(config) = &mut self.auto_delegation { + config.allow_manual_delegation = enabled; + } + self.manual_delegation_enabled = Some(enabled); + self + } + /// Globally enable or disable automatic parallel child-agent fan-out. /// /// Manual `parallel_task` calls remain available when this is false. diff --git a/core/src/agent_api/tests.rs b/core/src/agent_api/tests.rs index 2b42621..cbbd233 100644 --- a/core/src/agent_api/tests.rs +++ b/core/src/agent_api/tests.rs @@ -1807,6 +1807,26 @@ async fn test_session_uses_single_delegation_tool_surface() { assert!(!names.contains(&"run_team".to_string())); } +#[tokio::test] +async fn test_session_can_disable_manual_delegation_tools_without_dropping_registry() { + let agent = Agent::from_config(test_config()).await.unwrap(); + let opts = SessionOptions::new() + .with_worker_agent(crate::subagent::WorkerAgentSpec::planner( + "release-planner", + "Plan releases", + )) + .with_manual_delegation_enabled(false); + let session = agent + .session("/tmp/test-workspace-no-manual-delegation", Some(opts)) + .unwrap(); + let names = session.tool_names(); + + assert!(!names.contains(&"task".to_string())); + assert!(!names.contains(&"parallel_task".to_string())); + assert!(session.agent_registry.exists("release-planner")); + assert!(!session.config.auto_delegation.allow_manual_delegation); +} + #[tokio::test(flavor = "multi_thread")] async fn test_session_with_queue_config() { let agent = Agent::from_config(test_config()).await.unwrap(); @@ -2621,13 +2641,16 @@ async fn test_session_options_builders() { .with_auto_save(true) .with_max_parallel_tasks(3) .with_auto_delegation_enabled(true) + .with_manual_delegation_enabled(false) .with_auto_parallel_delegation(false); assert_eq!(opts.session_id, Some("test-id".to_string())); assert!(opts.auto_save); assert_eq!(opts.max_parallel_tasks, Some(3)); + assert_eq!(opts.manual_delegation_enabled, Some(false)); assert_eq!(opts.auto_parallel_delegation, Some(false)); let auto = opts.auto_delegation.expect("auto delegation config"); assert!(auto.enabled); + assert!(!auto.allow_manual_delegation); assert!(!auto.auto_parallel); } diff --git a/core/src/config/loader.rs b/core/src/config/loader.rs index 85a987f..79618c8 100644 --- a/core/src/config/loader.rs +++ b/core/src/config/loader.rs @@ -72,6 +72,17 @@ fn parse_auto_delegation_block( { config.auto_parallel = auto_parallel; } + if let Some(allow_manual_delegation) = acl_bool_attr( + block, + &[ + "allow_manual_delegation", + "allowManualDelegation", + "manual_delegation", + "manualDelegation", + ], + ) { + config.allow_manual_delegation = allow_manual_delegation; + } if let Some(min_confidence) = acl_f32_attr(block, &["min_confidence", "minConfidence"]) { config.min_confidence = min_confidence.clamp(0.0, 1.0); } diff --git a/core/src/config/mod.rs b/core/src/config/mod.rs index 9a7079a..d02dcca 100644 --- a/core/src/config/mod.rs +++ b/core/src/config/mod.rs @@ -61,6 +61,14 @@ pub struct AutoDelegationConfig { /// Manual `parallel_task` calls remain available when this is false. #[serde(alias = "auto_parallel")] pub auto_parallel: bool, + /// Allow model-visible manual `task` and `parallel_task` delegation tools. + /// + /// Set this to false for cost control or debugging when child-agent tools + /// should be absent from the session tool surface. This is not a security + /// sandbox: the parent agent may still have other tools such as `bash`, + /// MCP tools, or skills. + #[serde(alias = "allow_manual_delegation")] + pub allow_manual_delegation: bool, /// Minimum local confidence required to auto-delegate a child task. pub min_confidence: f32, /// Maximum number of automatic child tasks per user request. @@ -72,6 +80,7 @@ impl Default for AutoDelegationConfig { Self { enabled: false, auto_parallel: true, + allow_manual_delegation: true, min_confidence: 0.72, max_tasks: 4, } diff --git a/core/src/config/tests.rs b/core/src/config/tests.rs index f3ed62b..11abf0f 100644 --- a/core/src/config/tests.rs +++ b/core/src/config/tests.rs @@ -54,6 +54,7 @@ fn test_config_with_storage_backend() { auto_delegation { enabled = true auto_parallel = true + allow_manual_delegation = false min_confidence = 0.8 max_tasks = 2 } @@ -67,6 +68,7 @@ fn test_config_with_storage_backend() { assert_eq!(config.max_parallel_tasks, Some(3)); assert!(config.auto_delegation.enabled); assert!(!config.auto_delegation.auto_parallel); + assert!(!config.auto_delegation.allow_manual_delegation); assert!((config.auto_delegation.min_confidence - 0.8).abs() < f32::EPSILON); assert_eq!(config.auto_delegation.max_tasks, 2); }