diff --git a/run_agent.py b/run_agent.py index f62229deb..497197f76 100644 --- a/run_agent.py +++ b/run_agent.py @@ -5621,7 +5621,6 @@ class AIAgent: return _delegate_task( goal=function_args.get("goal"), context=function_args.get("context"), - toolsets=function_args.get("toolsets"), tasks=function_args.get("tasks"), max_iterations=function_args.get("max_iterations"), acp_command=function_args.get("acp_command"), diff --git a/skills/autonomous-ai-agents/hermes-agent/SKILL.md b/skills/autonomous-ai-agents/hermes-agent/SKILL.md index e8505128f..de6f398df 100644 --- a/skills/autonomous-ai-agents/hermes-agent/SKILL.md +++ b/skills/autonomous-ai-agents/hermes-agent/SKILL.md @@ -706,7 +706,7 @@ here; full developer notes live in `AGENTS.md`, user-facing docs under Spawn a subagent with an isolated context + terminal session. -- **Single:** `delegate_task(goal, context, toolsets)`. +- **Single:** `delegate_task(goal, context)`. - **Batch:** `delegate_task(tasks=[{goal, ...}, ...])` runs children in parallel, capped by `delegation.max_concurrent_children` (default 3). - **Background:** `delegate_task(background=true)` returns a handle diff --git a/tests/tools/test_async_delegation.py b/tests/tools/test_async_delegation.py index 8c3f2e7c6..0cbd9313c 100644 --- a/tests/tools/test_async_delegation.py +++ b/tests/tools/test_async_delegation.py @@ -262,7 +262,7 @@ def test_delegate_task_background_routes_async_and_does_not_block(monkeypatch): monkeypatch.setattr(dt, "_run_single_child", slow_child) monkeypatch.setattr(dt, "_resolve_delegation_credentials", lambda *a, **k: creds) out = dt.delegate_task( - goal="the real task", context="ctx", toolsets=["web"], + goal="the real task", context="ctx", background=True, parent_agent=parent, ) @@ -422,6 +422,30 @@ def test_run_agent_dispatch_forces_background(): assert captured["background"] is False +def test_dispatch_never_forwards_model_toolsets(): + """The model has no toolsets argument — subagents always inherit the + parent's toolsets. Even if a model smuggles a `toolsets` key into the + tool-call args, the live dispatch path must NOT forward it to + delegate_task (which no longer accepts it) and must not crash.""" + from unittest.mock import patch + import run_agent + + class _FakeAgent: + _delegate_depth = 0 + + captured = {} + + def _fake_delegate(**kwargs): + captured.update(kwargs) + return "{}" + + with patch("tools.delegate_tool.delegate_task", _fake_delegate): + run_agent.AIAgent._dispatch_delegate_task( + _FakeAgent(), {"goal": "x", "toolsets": ["web", "terminal"]} + ) + assert "toolsets" not in captured + + def test_delegate_task_background_detaches_child_from_parent(monkeypatch): """A background child must NOT remain in parent._active_children — otherwise parent-turn interrupts / cache evicts / session close would diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index ac3790849..7cd6bf500 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -69,7 +69,11 @@ class TestDelegateRequirements(unittest.TestCase): self.assertIn("goal", props) self.assertIn("tasks", props) self.assertIn("context", props) - self.assertIn("toolsets", props) + # toolsets is intentionally NOT exposed to the model — subagents always + # inherit the parent's toolsets. Letting the model name toolsets was a + # capability-selection surface the model should not control. + self.assertNotIn("toolsets", props) + self.assertNotIn("toolsets", props["tasks"]["items"]["properties"]) # max_iterations is intentionally NOT exposed to the model — it's # config-authoritative via delegation.max_iterations so users get # predictable budgets. diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 17b9435a0..893502ec0 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -111,24 +111,9 @@ def _get_subagent_approval_callback(): return _subagent_auto_approve return _subagent_auto_deny -# Build a description fragment listing toolsets available for subagents. -# Excludes toolsets where ALL tools are blocked, composite/platform toolsets -# (hermes-* prefixed), and scenario toolsets. -# -# NOTE: "delegation" is in this exclusion set so the subagent-facing -# capability hint string (_TOOLSET_LIST_STR) doesn't advertise it as a -# toolset to request explicitly — the correct mechanism for nested -# delegation is role='orchestrator', which re-adds "delegation" in -# _build_child_agent regardless of this exclusion. -_EXCLUDED_TOOLSET_NAMES = frozenset({"debugging", "safe", "delegation", "rl"}) -_SUBAGENT_TOOLSETS = sorted( - name - for name, defn in TOOLSETS.items() - if name not in _EXCLUDED_TOOLSET_NAMES - and not name.startswith("hermes-") - and not all(t in DELEGATE_BLOCKED_TOOLS for t in defn.get("tools", [])) -) -_TOOLSET_LIST_STR = ", ".join(f"'{n}'" for n in _SUBAGENT_TOOLSETS) +# NOTE: nested delegation is granted by role='orchestrator' (which re-adds the +# "delegation" toolset in _build_child_agent), NOT by the model naming toolsets +# — the model has no toolsets argument. Subagents inherit the parent's toolsets. _DEFAULT_MAX_CONCURRENT_CHILDREN = 3 # One-shot guard: the high-concurrency cost advisory is emitted at most once @@ -2354,7 +2339,6 @@ def _recover_tasks_from_json_string( def delegate_task( goal: Optional[str] = None, context: Optional[str] = None, - toolsets: Optional[List[str]] = None, tasks: Optional[List[Dict[str, Any]]] = None, max_iterations: Optional[int] = None, acp_command: Optional[str] = None, @@ -2461,9 +2445,7 @@ def delegate_task( ) task_list = tasks elif goal and isinstance(goal, str) and goal.strip(): - task_list = [ - {"goal": goal, "context": context, "toolsets": toolsets, "role": top_role} - ] + task_list = [{"goal": goal, "context": context, "role": top_role}] else: return tool_error("Provide either 'goal' (single task) or 'tasks' (batch).") @@ -2507,7 +2489,9 @@ def delegate_task( task_index=i, goal=t["goal"], context=t.get("context"), - toolsets=t.get("toolsets") or toolsets, + # Subagents always inherit the parent's toolsets; the model + # cannot choose or narrow them (no model-facing toolsets arg). + toolsets=None, model=creds["model"], max_iterations=effective_max_iter, task_count=n_tasks, @@ -2848,7 +2832,9 @@ def delegate_task( dispatch = dispatch_async_delegation_batch( goals=_goals, context=context, - toolsets=toolsets, + # Metadata for the completion block only; subagents inherit the + # parent's toolsets (no model-facing toolsets arg). + toolsets=None, role=top_role, model=creds["model"], session_key=_session_key, @@ -3387,18 +3373,6 @@ DELEGATE_TASK_SCHEMA = { "specific you are, the better the subagent performs." ), }, - "toolsets": { - "type": "array", - "items": {"type": "string"}, - "description": ( - "Toolsets to enable for this subagent. " - "Default: inherits your enabled toolsets. " - f"Available toolsets: {_TOOLSET_LIST_STR}. " - "Common patterns: ['terminal', 'file'] for code work, " - "['web'] for research, ['browser'] for web interaction, " - "['terminal', 'file', 'web'] for full-stack tasks." - ), - }, "tasks": { "type": "array", "items": { @@ -3409,11 +3383,6 @@ DELEGATE_TASK_SCHEMA = { "type": "string", "description": "Task-specific context", }, - "toolsets": { - "type": "array", - "items": {"type": "string"}, - "description": f"Toolsets for this specific task. Available: {_TOOLSET_LIST_STR}. Use 'web' for network access, 'terminal' for shell, 'browser' for web interaction.", - }, "acp_command": { "type": "string", "description": ( @@ -3512,7 +3481,6 @@ registry.register( handler=lambda args, **kw: delegate_task( goal=args.get("goal"), context=args.get("context"), - toolsets=args.get("toolsets"), tasks=args.get("tasks"), max_iterations=args.get("max_iterations"), acp_command=args.get("acp_command"), diff --git a/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md b/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md index ea3ed2e69..caa66b64e 100644 --- a/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md +++ b/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md @@ -647,7 +647,7 @@ here; full developer notes live in `AGENTS.md`, user-facing docs under Synchronous subagent spawn — the parent waits for the child's summary before continuing its own loop. Isolated context + terminal session. -- **Single:** `delegate_task(goal, context, toolsets)`. +- **Single:** `delegate_task(goal, context)`. - **Batch:** `delegate_task(tasks=[{goal, ...}, ...])` runs children in parallel, capped by `delegation.max_concurrent_children` (default 3). - **Roles:** `leaf` (default; cannot re-delegate) vs `orchestrator` diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md index 52e09c326..196fdda00 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md @@ -633,7 +633,7 @@ terminal(command="tmux new-session -d -s resumed 'hermes --resume 20260225_14305 同步子 agent 生成——父 agent 等待子 agent 的摘要后再继续自身循环。隔离的上下文和终端会话。 -- **单个:** `delegate_task(goal, context, toolsets)`。 +- **单个:** `delegate_task(goal, context)`。 - **批量:** `delegate_task(tasks=[{goal, ...}, ...])` 并行运行子任务,上限由 `delegation.max_concurrent_children`(默认 3)控制。 - **角色:** `leaf`(默认;不能再委派)vs `orchestrator`(可以生成自己的 worker,受 `delegation.max_spawn_depth` 限制)。 - **非持久化。** 如果父 agent 被中断,子 agent 会被取消。对于必须在当前轮次之后继续的工作,使用 `cronjob` 或 `terminal(background=True, notify_on_complete=True)`。