diff --git a/AGENTS.md b/AGENTS.md index 49596b9b41be..cb53e95eb0b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -998,7 +998,8 @@ Two shapes: Roles: - `role="leaf"` (default) — focused worker. Cannot call `delegate_task`, - `clarify`, `memory`, `send_message`, `execute_code`. + `clarify`, `memory`, `send_message`, `cronjob`. Retains `execute_code` + (programmatic tool calling). - `role="orchestrator"` — retains `delegate_task` so it can spawn its own workers. Gated by `delegation.orchestrator_enabled` (default true) and bounded by `delegation.max_spawn_depth` (default 2). diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 96d895038260..e17a0c77e24f 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -159,7 +159,7 @@ class TestChildSystemPrompt(unittest.TestCase): class TestStripBlockedTools(unittest.TestCase): def test_removes_blocked_toolsets(self): result = _strip_blocked_tools(["terminal", "file", "delegation", "clarify", "memory", "code_execution"]) - self.assertEqual(sorted(result), ["file", "terminal"]) + self.assertEqual(sorted(result), ["code_execution", "file", "terminal"]) def test_preserves_allowed_toolsets(self): result = _strip_blocked_tools(["terminal", "file", "web", "browser"]) @@ -235,10 +235,12 @@ class TestStripBlockedTools(unittest.TestCase): "clarify", "cronjob", "delegation", - "code_execution", "memory", ): self.assertIn(toolset_name, disabled) + # code_execution is deliberately NOT denied — children keep + # execute_code for programmatic tool calling (Teknium, Jul 2026). + self.assertNotIn("code_execution", disabled) definitions = model_tools.get_tool_definitions( enabled_toolsets=kwargs["enabled_toolsets"], @@ -1152,9 +1154,15 @@ class TestSubagentCostRollup(unittest.TestCase): class TestBlockedTools(unittest.TestCase): def test_blocked_tools_constant(self): - for tool in ["delegate_task", "clarify", "memory", "send_message", "execute_code"]: + for tool in ["delegate_task", "clarify", "memory", "send_message", "cronjob"]: self.assertIn(tool, DELEGATE_BLOCKED_TOOLS) + def test_execute_code_not_blocked(self): + """Children retain execute_code (programmatic tool calling) so they + can batch mechanical work instead of burning reasoning iterations + (Teknium, Jul 2026).""" + self.assertNotIn("execute_code", DELEGATE_BLOCKED_TOOLS) + def test_constants(self): from tools.delegate_tool import ( _get_max_spawn_depth, _get_orchestrator_enabled, diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 6a0e4c0dcd6a..f8f1be7246af 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -49,7 +49,6 @@ DELEGATE_BLOCKED_TOOLS = frozenset( "clarify", # no user interaction "memory", # no writes to shared MEMORY.md "send_message", # no cross-platform side effects - "execute_code", # children should reason step-by-step, not write scripts "cronjob", # no scheduling more work in the parent's name ] ) @@ -774,7 +773,7 @@ def _strip_blocked_tools(toolsets: List[str]) -> List[str]: """ # Composite toolsets that should never pass through to children, even # though their individual tools aren't all in DELEGATE_BLOCKED_TOOLS. - _COMPOSITE_BLOCKED_TOOLSETS = frozenset({"delegation", "code_execution"}) + _COMPOSITE_BLOCKED_TOOLSETS = frozenset({"delegation"}) blocked_toolset_names = { name for name, defn in TOOLSETS.items() @@ -3427,10 +3426,10 @@ def _build_top_level_description() -> str: "status) and verify it yourself — fetch the URL, stat the file, read " "back the content — before telling the user the operation succeeded.\n" "- Leaf subagents (role='leaf', the default) CANNOT call: " - "delegate_task, clarify, memory, send_message, execute_code.\n" + "delegate_task, clarify, memory, send_message.\n" "- Orchestrator subagents (role='orchestrator') retain " "delegate_task so they can spawn their own workers, but still " - "cannot use clarify, memory, send_message, or execute_code. " + "cannot use clarify, memory, or send_message. " f"Orchestrators are bounded by max_spawn_depth={max_depth} for this " f"user and can be disabled globally via " "delegation.orchestrator_enabled=false.\n" diff --git a/website/docs/guides/delegation-patterns.md b/website/docs/guides/delegation-patterns.md index bc147f73c089..9f16bb34999f 100644 --- a/website/docs/guides/delegation-patterns.md +++ b/website/docs/guides/delegation-patterns.md @@ -194,7 +194,7 @@ This is often the most efficient pattern: `execute_code` handles the 10+ sequent ## Inherited Tool Access -Subagents inherit the parent's enabled toolsets. `delegate_task` does not accept a model-facing `toolsets` parameter, so delegated work cannot grant itself capabilities that the parent does not have. Configure the parent's tools before starting the conversation when a delegated task needs web, terminal, file, or other access. Hermes still strips child-blocked tools such as `clarify`, `memory`, and `execute_code`. +Subagents inherit the parent's enabled toolsets. `delegate_task` does not accept a model-facing `toolsets` parameter, so delegated work cannot grant itself capabilities that the parent does not have. Configure the parent's tools before starting the conversation when a delegated task needs web, terminal, file, or other access. Hermes still strips child-blocked tools such as `clarify`, `memory`, and `send_message`; children keep `execute_code` for programmatic tool calling. --- diff --git a/website/docs/user-guide/features/delegation.md b/website/docs/user-guide/features/delegation.md index 36dddd819d4c..4790ce3b4200 100644 --- a/website/docs/user-guide/features/delegation.md +++ b/website/docs/user-guide/features/delegation.md @@ -264,7 +264,7 @@ For **durable execution** that must survive session closure or process restart, - Each subagent gets its **own terminal session** (separate from the parent) - Subagents inherit the parent's enabled toolsets; the model cannot select or widen them per call - **Nested delegation is opt-in** — only `role="orchestrator"` children can delegate further, and only when `max_spawn_depth` is raised from its default of 1 (flat). Disable globally with `orchestrator_enabled: false`. -- Leaf subagents **cannot** call: `delegate_task`, `clarify`, `memory`, `execute_code`. Orchestrator subagents retain `delegate_task` but still cannot use the other three. +- Leaf subagents **cannot** call: `delegate_task`, `clarify`, `memory`, `send_message`, `cronjob`. Orchestrator subagents retain `delegate_task` but keep the other blocks. Both roles retain `execute_code` (programmatic tool calling) so children can batch mechanical work instead of burning reasoning iterations. - **Cancellation follows ownership** — `/stop` or closing/resetting the owning session cancels its background children; synchronous descendants under orchestrators follow their parent's interrupt state - Only the final summary enters the parent's context, keeping token usage efficient - Subagents inherit the parent's **API key, provider configuration, and credential pool** (enabling key rotation on rate limits)