mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
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:
parent
b2ba069cb4
commit
295e20358c
5 changed files with 18 additions and 10 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue