Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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.

---

Expand Down
6 changes: 6 additions & 0 deletions core/src/agent_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ pub struct SessionOptions {
pub max_parallel_tasks: Option<usize>,
/// Per-session automatic subagent delegation override.
pub auto_delegation: Option<crate::config::AutoDelegationConfig>,
/// 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<bool>,
/// Per-session kill switch for automatic parallel child-agent fan-out.
///
/// This overlays the effective automatic delegation config instead of
Expand Down
7 changes: 7 additions & 0 deletions core/src/agent_api/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ fn register_task_capability(
use crate::tools::register_task_with_mcp;

let registry = AgentRegistry::new();
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
Expand All @@ -178,6 +179,12 @@ fn register_task_capability(
registry.register_worker(worker.clone());
}

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);
}

let parent_context = ChildRunContext {
security_provider: opts.security_provider.clone(),
hook_engine: None,
Expand Down
12 changes: 4 additions & 8 deletions core/src/agent_api/session_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ use std::sync::{Arc, RwLock};
use super::capabilities::{
build_session_capabilities, register_skill_capability, SessionCapabilityInput,
};
use super::session_config::{resolve_session_memory, resolve_session_store};
use super::session_config::{
resolve_auto_delegation_config, resolve_session_memory, resolve_session_store,
};
use super::session_runtime::{build_session_runtime, SessionRuntimeInput};

pub(super) fn prepare_session_options(agent: &Agent, opts: SessionOptions) -> SessionOptions {
Expand Down Expand Up @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions core/src/agent_api/session_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions core/src/agent_api/session_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions core/src/agent_api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}

Expand Down
11 changes: 11 additions & 0 deletions core/src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 9 additions & 0 deletions core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
}
Expand Down
2 changes: 2 additions & 0 deletions core/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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);
}
Expand Down