diff --git a/cli-config.yaml.example b/cli-config.yaml.example index fb6912642ae..81a47dcc665 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -871,7 +871,7 @@ delegation: max_iterations: 50 # Max tool-calling turns per child (default: 50) # max_concurrent_children: 3 # Max parallel child agents per batch (default: 3, floor: 1, no ceiling). # WARNING: values above 10 multiply API cost linearly. - # max_spawn_depth: 1 # Delegation tree depth cap (range: 1-3, default: 1 = flat). + # max_spawn_depth: 1 # Delegation tree depth (floor 1, no ceiling; default: 1 = flat). # Raise to 2 to allow workers to spawn their own subagents. # Requires role="orchestrator" on intermediate agents. # orchestrator_enabled: true # Kill switch for role="orchestrator" children (default: true). diff --git a/hermes_cli/config.py b/hermes_cli/config.py index a37c073c8a4..b18a8994e18 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1677,9 +1677,9 @@ DEFAULT_CONFIG = { # "low", "minimal", "none" (empty = inherit parent's level) "max_concurrent_children": 3, # max parallel children per batch; floor of 1 enforced, no ceiling # Orchestrator role controls (see tools/delegate_tool.py:_get_max_spawn_depth - # and _get_orchestrator_enabled). Values are clamped to [1, 3] with a - # warning log if out of range. - "max_spawn_depth": 1, # depth cap (1 = flat [default], 2 = orchestrator→leaf, 3 = three-level) + # and _get_orchestrator_enabled). Floored at 1, no upper ceiling — + # raise deliberately, each level multiplies API cost. + "max_spawn_depth": 1, # depth (1 = flat [default], 2 = orchestrator→leaf, 3+ = deeper) "orchestrator_enabled": True, # kill switch for role="orchestrator" # When a subagent hits a dangerous-command approval prompt, the parent's # prompt_toolkit TUI owns stdin — a thread-local input() call from the diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 3efe21389c5..b37bb35d9fc 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -838,14 +838,13 @@ class TestBlockedTools(unittest.TestCase): def test_constants(self): from tools.delegate_tool import ( _get_max_spawn_depth, _get_orchestrator_enabled, - _MIN_SPAWN_DEPTH, _MAX_SPAWN_DEPTH_CAP, + _MIN_SPAWN_DEPTH, ) self.assertEqual(_get_max_concurrent_children(), 3) self.assertEqual(MAX_DEPTH, 1) self.assertEqual(_get_max_spawn_depth(), 1) # default: flat self.assertTrue(_get_orchestrator_enabled()) # default self.assertEqual(_MIN_SPAWN_DEPTH, 1) - self.assertEqual(_MAX_SPAWN_DEPTH_CAP, 3) class TestDelegationCredentialResolution(unittest.TestCase): @@ -2084,17 +2083,14 @@ class TestMaxSpawnDepth(unittest.TestCase): with self.assertLogs("tools.delegate_tool", level=logging.WARNING) as cm: result = _get_max_spawn_depth() self.assertEqual(result, 1) - self.assertTrue(any("clamping to 1" in m for m in cm.output)) + self.assertTrue(any("below floor 1" in m for m in cm.output)) @patch("tools.delegate_tool._load_config", return_value={"max_spawn_depth": 99}) - def test_max_spawn_depth_clamped_above_three(self, mock_cfg): - import logging + def test_max_spawn_depth_no_upper_ceiling(self, mock_cfg): + """No upper ceiling — high values pass through unchanged (cost is the limiter).""" from tools.delegate_tool import _get_max_spawn_depth - with self.assertLogs("tools.delegate_tool", level=logging.WARNING) as cm: - result = _get_max_spawn_depth() - self.assertEqual(result, 3) - self.assertTrue(any("clamping to 3" in m for m in cm.output)) + self.assertEqual(_get_max_spawn_depth(), 99) @patch("tools.delegate_tool._load_config", return_value={"max_spawn_depth": "not-a-number"}) diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index d696cab41a9..40e19a0e4da 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -134,7 +134,9 @@ MAX_DEPTH = 1 # flat by default: parent (0) -> child (1); grandchild rejected u # Configurable depth cap consulted by _get_max_spawn_depth; MAX_DEPTH # stays as the default fallback and is still the symbol tests import. _MIN_SPAWN_DEPTH = 1 -_MAX_SPAWN_DEPTH_CAP = 3 +# No upper ceiling on spawn depth — like max_concurrent_children, depth has a +# floor of 1 and no ceiling. Deeper trees multiply API cost, so the default +# stays flat (MAX_DEPTH = 1); raising the config knob is an explicit opt-in. # --------------------------------------------------------------------------- @@ -392,7 +394,7 @@ def _get_child_timeout() -> float: def _get_max_spawn_depth() -> int: - """Read delegation.max_spawn_depth from config, clamped to [1, 3]. + """Read delegation.max_spawn_depth from config, floored at 1 (no ceiling). depth 0 = parent agent. max_spawn_depth = N means agents at depths 0..N-1 can spawn; depth N is the leaf floor. Default 1 is flat: @@ -400,9 +402,11 @@ def _get_max_spawn_depth() -> int: (blocked by this guard AND, for leaf children, by the delegation toolset strip in _strip_blocked_tools). - Raise to 2 or 3 to unlock nested orchestration. role="orchestrator" - removes the toolset strip for depth-1 children when + Raise to 2+ to unlock nested orchestration. role="orchestrator" + removes the toolset strip for spawning children when max_spawn_depth >= 2, enabling them to spawn their own workers. + Like max_concurrent_children, there is no upper ceiling — but each + extra level multiplies API cost, so raise it deliberately. """ cfg = _load_config() val = cfg.get("max_spawn_depth") @@ -417,16 +421,15 @@ def _get_max_spawn_depth() -> int: MAX_DEPTH, ) return MAX_DEPTH - clamped = max(_MIN_SPAWN_DEPTH, min(_MAX_SPAWN_DEPTH_CAP, ival)) - if clamped != ival: + floored = max(_MIN_SPAWN_DEPTH, ival) + if floored != ival: logger.warning( - "delegation.max_spawn_depth=%d out of range [%d, %d]; " "clamping to %d", + "delegation.max_spawn_depth=%d below floor %d; using %d", ival, _MIN_SPAWN_DEPTH, - _MAX_SPAWN_DEPTH_CAP, - clamped, + floored, ) - return clamped + return floored def _get_orchestrator_enabled() -> bool: @@ -1982,7 +1985,8 @@ def delegate_task( f"Delegation depth limit reached (depth={depth}, " f"max_spawn_depth={max_spawn}). Raise " f"delegation.max_spawn_depth in config.yaml if deeper " - f"nesting is required (cap: {_MAX_SPAWN_DEPTH_CAP})." + f"nesting is required (no hard ceiling, but each level " + f"multiplies API cost)." ) } ) diff --git a/website/docs/guides/delegation-patterns.md b/website/docs/guides/delegation-patterns.md index 332282e6d4c..7ff05e2a8a4 100644 --- a/website/docs/guides/delegation-patterns.md +++ b/website/docs/guides/delegation-patterns.md @@ -218,14 +218,14 @@ Restricting toolsets keeps the subagent focused and prevents accidental side eff ## Constraints - **Default 3 parallel tasks**: batches default to 3 concurrent subagents (configurable via `delegation.max_concurrent_children` in config.yaml, no hard ceiling, only a floor of 1) -- **Nested delegation is opt-in**: leaf subagents (default) cannot call `delegate_task`, `clarify`, `memory`, `send_message`, or `execute_code`. Orchestrator subagents (`role="orchestrator"`) retain `delegate_task` for further delegation, but only when `delegation.max_spawn_depth` is raised above the default of 1 (1-3 supported); the other four remain blocked. Disable globally via `delegation.orchestrator_enabled: false`. +- **Nested delegation is opt-in**: leaf subagents (default) cannot call `delegate_task`, `clarify`, `memory`, `send_message`, or `execute_code`. Orchestrator subagents (`role="orchestrator"`) retain `delegate_task` for further delegation, but only when `delegation.max_spawn_depth` is raised above the default of 1 (floor 1, no ceiling); the other four remain blocked. Disable globally via `delegation.orchestrator_enabled: false`. ### Tuning Concurrency and Depth | Config | Default | Range | Effect | |--------|---------|-------|--------| | `max_concurrent_children` | 3 | >=1 | Parallel batch size per `delegate_task` call | -| `max_spawn_depth` | 1 | 1-3 | How many delegation levels can spawn further | +| `max_spawn_depth` | 1 | >=1 | How many delegation levels can spawn further | Example: running 30 parallel workers with nested subagents: diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index d74587432d5..11ed264a8c1 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1672,7 +1672,7 @@ delegation: # api_key: "local-key" # API key for base_url (falls back to OPENAI_API_KEY) # api_mode: "" # Wire protocol for base_url: "chat_completions", "codex_responses", or "anthropic_messages". Empty = auto-detect from URL (e.g. /anthropic suffix → anthropic_messages). Set explicitly for non-standard endpoints the heuristic can't detect. max_concurrent_children: 3 # Parallel children per batch (floor 1, no ceiling). Also via DELEGATION_MAX_CONCURRENT_CHILDREN env var. - max_spawn_depth: 1 # Delegation tree depth cap (1-3, clamped). 1 = flat (default): parent spawns leaves that cannot delegate. 2 = orchestrator children can spawn leaf grandchildren. 3 = three levels. + max_spawn_depth: 1 # Delegation tree depth (floor 1, no ceiling). 1 = flat (default): parent spawns leaves that cannot delegate. 2 = orchestrator children can spawn leaf grandchildren. 3+ = deeper trees. orchestrator_enabled: true # Global kill switch. When false, role="orchestrator" is ignored and every child is forced to leaf regardless of max_spawn_depth. ``` @@ -1686,7 +1686,7 @@ The delegation provider uses the same credential resolution as CLI/gateway start **Precedence:** `delegation.base_url` in config → `delegation.provider` in config → parent provider (inherited). `delegation.model` in config → parent model (inherited). Setting just `model` without `provider` changes only the model name while keeping the parent's credentials (useful for switching models within the same provider like OpenRouter). -**Width and depth:** `max_concurrent_children` caps how many subagents run in parallel per batch (default `3`, floor of 1, no ceiling). Can also be set via the `DELEGATION_MAX_CONCURRENT_CHILDREN` env var. When the model submits a `tasks` array longer than the cap, `delegate_task` returns a tool error explaining the limit rather than silently truncating. `max_spawn_depth` controls the delegation tree depth (clamped to 1-3). At the default `1`, delegation is flat: children cannot spawn grandchildren, and passing `role="orchestrator"` silently degrades to `leaf`. Raise to `2` so orchestrator children can spawn leaf grandchildren; `3` for three-level trees. The agent opts into orchestration per call via `role="orchestrator"`; `orchestrator_enabled: false` forces every child back to leaf regardless. Cost scales multiplicatively — at `max_spawn_depth: 3` with `max_concurrent_children: 3`, the tree can reach 3×3×3 = 27 concurrent leaf agents. See [Subagent Delegation → Depth Limit and Nested Orchestration](features/delegation.md#depth-limit-and-nested-orchestration) for usage patterns. +**Width and depth:** `max_concurrent_children` caps how many subagents run in parallel per batch (default `3`, floor of 1, no ceiling). Can also be set via the `DELEGATION_MAX_CONCURRENT_CHILDREN` env var. When the model submits a `tasks` array longer than the cap, `delegate_task` returns a tool error explaining the limit rather than silently truncating. `max_spawn_depth` controls the delegation tree depth (floor of 1, no upper ceiling). At the default `1`, delegation is flat: children cannot spawn grandchildren, and passing `role="orchestrator"` silently degrades to `leaf`. Raise to `2` so orchestrator children can spawn leaf grandchildren; `3` for three-level trees, and higher for deeper ones. The agent opts into orchestration per call via `role="orchestrator"`; `orchestrator_enabled: false` forces every child back to leaf regardless. Cost scales multiplicatively — at `max_spawn_depth: 3` with `max_concurrent_children: 3`, the tree can reach 3×3×3 = 27 concurrent leaf agents. See [Subagent Delegation → Depth Limit and Nested Orchestration](features/delegation.md#depth-limit-and-nested-orchestration) for usage patterns. ## Clarify diff --git a/website/docs/user-guide/features/delegation.md b/website/docs/user-guide/features/delegation.md index 34d9da817e0..1d19c9fddce 100644 --- a/website/docs/user-guide/features/delegation.md +++ b/website/docs/user-guide/features/delegation.md @@ -214,7 +214,7 @@ delegate_task( ``` - `role="leaf"` (default): child cannot delegate further — identical to the flat-delegation behavior. -- `role="orchestrator"`: child retains the `delegation` toolset. Gated by `delegation.max_spawn_depth` (default **1** = flat, so `role="orchestrator"` is a no-op at defaults). Raise `max_spawn_depth` to 2 to allow orchestrator children to spawn leaf grandchildren; 3 for three levels (cap). +- `role="orchestrator"`: child retains the `delegation` toolset. Gated by `delegation.max_spawn_depth` (default **1** = flat, so `role="orchestrator"` is a no-op at defaults). Raise `max_spawn_depth` to 2 to allow orchestrator children to spawn leaf grandchildren; 3+ for deeper trees. There is no upper ceiling — cost is the practical limit. - `delegation.orchestrator_enabled: false`: global kill switch that forces every child to `leaf` regardless of the `role` parameter. **Cost warning:** With `max_spawn_depth: 3` and `max_concurrent_children: 3`, the tree can reach 3×3×3 = 27 concurrent leaf agents. Each extra level multiplies spend — raise `max_spawn_depth` intentionally. @@ -264,7 +264,7 @@ For **durable long-running work** that must survive interrupts or outlive the cu delegation: max_iterations: 50 # Max turns per child (default: 50) # max_concurrent_children: 3 # Parallel children per batch (default: 3) - # max_spawn_depth: 1 # Tree depth (1-3, default 1 = flat). Raise to 2 to allow orchestrator children to spawn leaves; 3 for three levels. + # max_spawn_depth: 1 # Tree depth (floor 1, no ceiling, default 1 = flat). Raise to 2 to allow orchestrator children to spawn leaves; 3+ for deeper trees. # orchestrator_enabled: true # Disable to force all children to leaf role. model: "google/gemini-3-flash-preview" # Optional provider/model override provider: "openrouter" # Optional built-in provider