feat(delegation): let subagents use execute_code (#69325)

Children inherit the parent's env, repo, and toolsets but were denied
execute_code ('children should reason step-by-step, not write scripts').
That forces subagents doing mechanical multi-step work (batch file
reads, fetch-N-pages loops, filter-before-context reductions) to burn
reasoning iterations one tool call at a time.

- Remove execute_code from DELEGATE_BLOCKED_TOOLS
- Stop stripping the code_execution toolset from child bundles
- No recursion risk: the sandbox bridges only the 7 SANDBOX_ALLOWED_TOOLS
  (web/file/terminal) — delegate_task and execute_code itself are not
  reachable from inside a sandbox script
- Update schema text, AGENTS.md, and delegation docs
- Tests: blocked-constant, strip, and child-assembly tests updated;
  new test pins execute_code as intentionally unblocked
This commit is contained in:
Teknium 2026-07-22 06:58:45 -07:00 committed by GitHub
parent b2ba069cb4
commit 295e20358c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 18 additions and 10 deletions

View file

@ -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).

View file

@ -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,

View file

@ -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"

View file

@ -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.
---

View file

@ -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)