From 0c513d315b05fea0e848545ae2f186cb45ba74d0 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:11:21 -0700 Subject: [PATCH 1/2] fix: respect disabled auto-compaction on context overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port from anomalyco/opencode#30749. When compression.enabled is false, NO automatic compaction trigger may fire. The proactive token-threshold paths (preflight + post-response should_compress gate) already honoured the setting, but the three provider-overflow recovery paths in the agent loop — long-context-tier 429, 413 payload-too-large, and context-overflow — called _compress_context() unconditionally, silently compressing and rotating the session against the user's explicit choice. Add a single guard at the top of the overflow-recovery dispatch: when compression is disabled and the error is one of those three overflow classes, surface a terminal error (compaction_disabled: True) telling the user to /compress manually, /new, switch to a larger-context model, or reduce attachments. Manual /compress (force=True) is unaffected — it never enters this loop. Tests: new TestOverflowWithCompactionDisabled (413 + 400 overflow don't compress when disabled; control case still compresses when enabled). Existing overflow-recovery tests updated to enable compaction explicitly (they verify the recovery fires); fixture defaults flipped to True to match production (compression.enabled defaults to True). --- agent/conversation_loop.py | 55 +++++++++++++ tests/run_agent/test_413_compression.py | 105 +++++++++++++++++++++++- tests/run_agent/test_run_agent.py | 3 + 3 files changed, 162 insertions(+), 1 deletion(-) diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 8be763513fff..d01d5d4a8448 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -2720,6 +2720,61 @@ def _perform_api_call(next_api_kwargs): # compress history and retry, not abort immediately. status_code = getattr(api_error, "status_code", None) + # ── Respect disabled auto-compaction on overflow ────── + # Ported from anomalyco/opencode#30749. When the user has + # turned auto-compaction off (``compression.enabled: false``), + # NO automatic compaction trigger may fire — including the + # provider/request-size overflow recovery paths below + # (long-context-tier 429, 413 payload-too-large, and + # context-overflow). Without this guard the proactive + # threshold path correctly honours the setting (see the + # preflight check and the post-response ``should_compress`` + # gate) but a provider overflow error would still silently + # compress + rotate the session, bypassing the user's + # explicit choice. Surface a terminal error instead so the + # user can compact manually (``/compress``), start fresh + # (``/new``), switch to a larger-context model, or reduce + # attachments. Forced compaction via ``/compress`` + # (``force=True``) is unaffected — it never reaches this loop. + _overflow_reasons = { + FailoverReason.long_context_tier, + FailoverReason.payload_too_large, + FailoverReason.context_overflow, + } + if ( + classified.reason in _overflow_reasons + and not getattr(agent, "compression_enabled", True) + ): + agent._flush_status_buffer() + agent._vprint( + f"{agent.log_prefix}❌ Context overflow, but auto-compaction is disabled " + f"(compression.enabled: false).", + force=True, + ) + agent._vprint( + f"{agent.log_prefix} 💡 Run /compress to compact manually, /new to start fresh, " + f"switch to a larger-context model, or reduce attachments.", + force=True, + ) + logger.error( + f"{agent.log_prefix}Context overflow ({classified.reason.value}) with " + f"auto-compaction disabled — not compressing." + ) + agent._persist_session(messages, conversation_history) + return { + "messages": messages, + "completed": False, + "api_calls": api_call_count, + "error": ( + "Context overflow and auto-compaction is disabled " + "(compression.enabled: false). Run /compress to compact manually, " + "/new to start fresh, or switch to a larger-context model." + ), + "partial": True, + "failed": True, + "compaction_disabled": True, + } + # ── Anthropic Sonnet long-context tier gate ─────────── # Anthropic returns HTTP 429 "Extra usage is required for # long context requests" when a Claude Max (or similar) diff --git a/tests/run_agent/test_413_compression.py b/tests/run_agent/test_413_compression.py index cadb26c449b3..2b8c32e297bf 100644 --- a/tests/run_agent/test_413_compression.py +++ b/tests/run_agent/test_413_compression.py @@ -94,7 +94,11 @@ def agent(): a._cached_system_prompt = "You are helpful." a._use_prompt_caching = False a.tool_delay = 0 - a.compression_enabled = False + # Default matches production (`compression.enabled` defaults to True). + # Overflow-recovery tests below verify that 413 / context-overflow + # errors DO trigger compression; the disabled-path behavior is + # covered explicitly by TestOverflowWithCompactionDisabled. + a.compression_enabled = True a.save_trajectories = False return a @@ -415,6 +419,13 @@ class TestPreflightCompression: def test_compress_context_emits_lifecycle_status_before_work(self, agent): """Direct context compression should tell gateway users why the turn paused.""" + # This test calls _compress_context directly and asserts the FIRST + # status event is the lifecycle "Compacting context" message. With + # compaction enabled the lazy feasibility probe would emit an + # aux-provider warning first (no aux key in the hermetic test env), + # displacing events[0]. The flag value is irrelevant to what this + # test asserts, so disable it to suppress the probe. + agent.compression_enabled = False events = [] agent.status_callback = lambda ev, msg: events.append((ev, msg)) @@ -802,3 +813,95 @@ def test_anthropic_prompt_too_long_safety_net(self, agent): mock_compress.assert_called_once() assert result["completed"] is True + + +# --------------------------------------------------------------------------- +# Disabled auto-compaction on overflow (port of anomalyco/opencode#30749) +# --------------------------------------------------------------------------- + +class TestOverflowWithCompactionDisabled: + """When ``compression.enabled`` is False, NO automatic compaction may + fire — including the provider/request-size overflow recovery paths. + + Ported from anomalyco/opencode#30749: the proactive token-threshold + path already honoured the setting, but provider overflow errors + (413 payload-too-large, context-overflow, long-context-tier 429) still + silently compressed + rotated the session. The fix surfaces a terminal + error so the user can compact manually, start fresh, or switch models. + """ + + @staticmethod + def _prefill(): + return [ + {"role": "user", "content": "previous question"}, + {"role": "assistant", "content": "previous answer"}, + ] + + def test_413_does_not_compress_when_disabled(self, agent): + """413 must NOT call _compress_context when compaction is disabled.""" + agent.compression_enabled = False + err_413 = _make_413_error() + # If the guard fails, a second (success) response would be consumed. + agent.client.chat.completions.create.side_effect = [err_413, _mock_response()] + + with ( + patch.object(agent, "_compress_context") as mock_compress, + patch.object(agent, "_persist_session") as mock_persist, + patch.object(agent, "_save_trajectory"), + patch.object(agent, "_cleanup_task_resources"), + ): + result = agent.run_conversation("hello", conversation_history=self._prefill()) + + mock_compress.assert_not_called() + mock_persist.assert_called() + assert result.get("failed") is True + assert result.get("compaction_disabled") is True + assert "auto-compaction is disabled" in result["error"] + + def test_context_overflow_does_not_compress_when_disabled(self, agent): + """400 'prompt is too long' must NOT compress when compaction disabled.""" + agent.compression_enabled = False + err_400 = Exception( + "Error code: 400 - {'type': 'error', 'error': {'type': " + "'invalid_request_error', 'message': 'prompt is too long: " + "233153 tokens > 200000 maximum'}}" + ) + err_400.status_code = 400 + agent.client.chat.completions.create.side_effect = [err_400, _mock_response()] + + with ( + patch.object(agent, "_compress_context") as mock_compress, + patch.object(agent, "_persist_session"), + patch.object(agent, "_save_trajectory"), + patch.object(agent, "_cleanup_task_resources"), + ): + result = agent.run_conversation("hello", conversation_history=self._prefill()) + + mock_compress.assert_not_called() + assert result.get("compaction_disabled") is True + + def test_413_still_compresses_when_enabled(self, agent): + """Control: with compaction enabled, 413 still triggers compression. + + Guards against the disabled-path guard accidentally swallowing the + enabled path. + """ + agent.compression_enabled = True + err_413 = _make_413_error() + ok_resp = _mock_response(content="Recovered", finish_reason="stop") + agent.client.chat.completions.create.side_effect = [err_413, ok_resp] + + with ( + patch.object(agent, "_compress_context") as mock_compress, + patch.object(agent, "_persist_session"), + patch.object(agent, "_save_trajectory"), + patch.object(agent, "_cleanup_task_resources"), + ): + mock_compress.return_value = ( + [{"role": "user", "content": "hello"}], "compressed", + ) + result = agent.run_conversation("hello", conversation_history=self._prefill()) + + mock_compress.assert_called_once() + assert result["completed"] is True + assert result.get("compaction_disabled") is not True diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index e9e7011dd1ed..ea66789fdd38 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -3903,6 +3903,7 @@ def test_context_compression_triggered(self, agent): def test_glm_prompt_exceeds_max_length_triggers_compression(self, agent): """GLM/Z.AI uses 'Prompt exceeds max length' for context overflow.""" self._setup_agent(agent) + agent.compression_enabled = True # this test verifies overflow→compression fires err_400 = Exception( "Error code: 400 - {'error': {'code': '1261', 'message': 'Prompt exceeds max length'}}" ) @@ -3937,6 +3938,7 @@ def test_minimax_delta_overflow_keeps_known_context_length(self, agent): to the generic 128K fallback tier. """ self._setup_agent(agent) + agent.compression_enabled = True # this test verifies overflow→compression fires agent.provider = "minimax" agent.model = "MiniMax-M2.7-highspeed" agent.base_url = "https://api.minimax.io/anthropic" @@ -3982,6 +3984,7 @@ def test_non_minimax_overflow_without_provider_limit_keeps_context(self, agent): rely on compression — see #33669 / PR #33826. """ self._setup_agent(agent) + agent.compression_enabled = True # this test verifies overflow→compression fires agent.provider = "openrouter" agent.model = "some/unknown-model" agent.base_url = "https://openrouter.ai/api/v1" From 8a190c12dc0a5e12f509a155a24f61526d67b2aa Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 5 Jun 2026 04:21:39 -0700 Subject: [PATCH 2/2] feat(delegation): uncap max_spawn_depth to match max_concurrent_children Removed the hard ceiling of 3 on delegation.max_spawn_depth. Depth now has a floor of 1 and no upper limit, mirroring max_concurrent_children. Cost (each level multiplies API spend) is the practical limiter, not a constant. - delegate_tool.py: drop _MAX_SPAWN_DEPTH_CAP, _get_max_spawn_depth() floors at 1 instead of clamping to [1,3]; depth-limit error string reworded - config.py / cli-config.yaml.example: doc comments say floor 1, no ceiling - docs (configuration, delegation, delegation-patterns): range 1-3 -> >=1 - tests: convert clamp-above-3 change-detector into a no-ceiling invariant, drop the _MAX_SPAWN_DEPTH_CAP==3 snapshot assert, fix warning-text assert --- cli-config.yaml.example | 2 +- hermes_cli/config.py | 6 ++--- tests/tools/test_delegate.py | 14 ++++------ tools/delegate_tool.py | 26 +++++++++++-------- website/docs/guides/delegation-patterns.md | 4 +-- website/docs/user-guide/configuration.md | 4 +-- .../docs/user-guide/features/delegation.md | 4 +-- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cli-config.yaml.example b/cli-config.yaml.example index fb6912642ae9..81a47dcc6659 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 d0b4493ddd55..04fcdbbe23ec 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1677,9 +1677,9 @@ def _ensure_hermes_home_managed(home: Path): # "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 3efe21389c59..b37bb35d9fc7 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -838,14 +838,13 @@ def test_blocked_tools_constant(self): 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 @@ def test_max_spawn_depth_clamped_below_one(self, mock_cfg): 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 d696cab41a94..40e19a0e4da9 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -134,7 +134,9 @@ def _get_subagent_approval_callback(): # 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 332282e6d4cf..7ff05e2a8a4b 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 d74587432d5b..11ed264a8c1e 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 34d9da817e0b..1d19c9fddce9 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