From 9dfbde19db7b108f9e961eec367ca5b54c8ad7d6 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 27 Aug 2026 07:38:53 -0700 Subject: [PATCH] =?UTF-8?q?refactor(delegate=5Ftask):=20tasks-only=20inter?= =?UTF-8?q?face=20+=20depth-derived=20delegation=20(1,201=20=E2=86=92=2077?= =?UTF-8?q?3=20tok/call,=20=E2=88=9236%)=20(#96424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(delegate_task): depth-derived delegation (role param retired), session-filtered restrictions, background unadvertised — 1,201->819 tok/call * refactor(delegate_task): tasks[] is the only advertised shape — single task = one-entry array (legacy goal/context/output_schema stay handler-accepted) --- model_tools.py | 48 ++++ tests/tools/test_delegate.py | 51 +++- tests/tools/test_delegate_batch_validation.py | 16 +- tests/tools/test_delegate_control_actions.py | 12 +- tests/tools/test_delegate_output_schema.py | 10 +- tools/delegate_tool.py | 263 ++++++++---------- 6 files changed, 231 insertions(+), 169 deletions(-) diff --git a/model_tools.py b/model_tools.py index 0a5216bbb8..20ce327a2a 100644 --- a/model_tools.py +++ b/model_tools.py @@ -592,6 +592,54 @@ def _compute_tool_definitions( ] available_tool_names.discard("browser_exec") + # delegate_task's child-restrictions rule names sibling tools (clarify, + # memory, cronjob). Warning about tools this session doesn't even have + # teaches ghost vocabulary — filter the list to tools actually present + # and drop the line entirely when none apply. Two source variants exist + # (depth-derived): the depth-off line also names delegate_task itself; + # the depth-on line lists only the siblings. Pattern order matters — + # the sibling list is a substring of the full list. + # Same session-level seam as the browser_exec gate above. + if "delegate_task" in available_tool_names: + blocked_present = [ + t for t in ("clarify", "memory", "cronjob") if t in available_tool_names + ] + if len(blocked_present) < 3: + full_offvariant = "delegate_task, clarify, memory, or cronjob" + full_onvariant = "clarify, memory, or cronjob" + for i, td in enumerate(filtered_tools): + fn = td.get("function", {}) + desc = fn.get("description", "") + if fn.get("name") != "delegate_task": + continue + if full_offvariant in desc: + full, keep_self = full_offvariant, True + elif full_onvariant in desc: + full, keep_self = full_onvariant, False + else: + break + names = (["delegate_task"] if keep_self else []) + blocked_present + if blocked_present: + if len(names) == 1: + replacement = names[0] + elif len(names) == 2: + replacement = f"{names[0]} or {names[1]}" + else: + replacement = ", ".join(names[:-1]) + ", or " + names[-1] + desc = desc.replace(full, replacement) + else: + # No sibling tools here — drop the restriction line + # (both variants end at the following "\n"). + start = desc.find("- Children cannot call " + full) + if start != -1: + end = desc.index("\n", start) + 1 + desc = desc[:start] + desc[end:] + filtered_tools[i] = { + **td, + "function": {**fn, "description": desc}, + } + break + if not quiet_mode: if filtered_tools: tool_names = [t["function"]["name"] for t in filtered_tools] diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 0d7030ddb0..c896af7ada 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -62,9 +62,17 @@ class TestDelegateRequirements(unittest.TestCase): def test_schema_valid(self): self.assertEqual(DELEGATE_TASK_SCHEMA["name"], "delegate_task") props = DELEGATE_TASK_SCHEMA["parameters"]["properties"] - self.assertIn("goal", props) + # tasks[] is the only advertised spawn shape (single task = one-entry + # array); legacy top-level goal/context/output_schema stay + # handler-accepted but unadvertised. self.assertIn("tasks", props) - self.assertIn("context", props) + self.assertNotIn("goal", props) + self.assertNotIn("context", props) + self.assertNotIn("output_schema", props) + task_props = props["tasks"]["items"]["properties"] + self.assertIn("goal", task_props) + self.assertIn("context", task_props) + self.assertIn("output_schema", task_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. @@ -101,17 +109,18 @@ class TestDelegateRequirements(unittest.TestCase): "context", # pass-everything-via-context rule "respond in Chinese", # language example (weak models regress without it) "SELF-REPORTS", # verification contract - "fetch the URL", # concrete verification verbs - "clarify", # leaf blocked-tool list - "send_message", + "clarify", # child blocked-tool list "delegation.provider", # model inheritance / pinning ): self.assertIn(keyword, desc, f"top-level description lost: {keyword!r}") + # send_message must NOT be named: gateway-internal vocabulary most + # sessions never see (still enforced via DELEGATE_BLOCKED_TOOLS). + self.assertNotIn("send_message", desc) def test_dynamic_limits_moved_to_param_descriptions(self): - """Concurrency and nesting ceilings must reach the model through the - tasks/role parameter descriptions (the top-level text no longer - carries them).""" + """Concurrency reaches the model through the tasks parameter + description; the depth ceiling lives in the top-level description's + depth-derived recursion rule (role param is gone).""" from tools.delegate_tool import _build_dynamic_schema_overrides from tools.registry import registry @@ -125,12 +134,11 @@ class TestDelegateRequirements(unittest.TestCase): for parameters in (overrides["parameters"], definition["parameters"]): self.assertIn("up to 7", parameters["properties"]["tasks"]["description"]) - self.assertIn( - "max_spawn_depth=4", parameters["properties"]["role"]["description"] - ) - # Static top-level text must not embed stale limits. + self.assertNotIn("role", parameters["properties"]) + # Depth ceiling now rides the depth-derived recursion rule in the + # top-level text (only rendered when nesting is available). + self.assertIn("max_spawn_depth=4", overrides["description"]) self.assertNotIn("up to 7", overrides["description"]) - self.assertNotIn("max_spawn_depth", overrides["description"]) class TestChildSystemPrompt(unittest.TestCase): def test_goal_only(self): @@ -1553,10 +1561,23 @@ class TestOrchestratorRoleSchema(unittest.TestCase): delegate_task(**kwargs) return mock_child - def test_default_role_is_leaf(self): + def test_role_is_depth_derived_not_caller_declared(self): + """With max_spawn_depth=2 (mocked), a depth-1 child has depth budget + left, so it becomes an orchestrator automatically — no role arg + needed, and a passed legacy role arg is ignored either way.""" child = self._run_with_mock_child(_SENTINEL) - self.assertEqual(child._delegate_role, "leaf") + self.assertEqual(child._delegate_role, "orchestrator") + # Legacy explicit role='leaf' does not override the depth derivation. + child = self._run_with_mock_child("leaf") + self.assertEqual(child._delegate_role, "orchestrator") + def test_schema_no_longer_advertises_role(self): + """`role` left the advertised schema (capability is depth-derived); + the handler still accepts it for wire compat.""" + from tools.delegate_tool import DELEGATE_TASK_SCHEMA + props = DELEGATE_TASK_SCHEMA["parameters"]["properties"] + self.assertNotIn("role", props) + self.assertNotIn("role", props["tasks"]["items"]["properties"]) def test_schema_omits_acp_transport_fields(self): from tools.delegate_tool import DELEGATE_TASK_SCHEMA diff --git a/tests/tools/test_delegate_batch_validation.py b/tests/tools/test_delegate_batch_validation.py index 912c1b699e..dc77b85058 100644 --- a/tests/tools/test_delegate_batch_validation.py +++ b/tests/tools/test_delegate_batch_validation.py @@ -144,11 +144,17 @@ class TestBatchPlaceholderGoals(unittest.TestCase): class TestSingleTaskBatch(unittest.TestCase): - def test_one_task_batch_rejected_pointing_to_goal_form(self): - result = _call([{"goal": GOOD_A}]) - self.assertIn("error", result) - self.assertIn("goal", result["error"]) - self.assertIn("2", result["error"]) # "at least 2" + def test_one_task_batch_is_valid_single_task_shape(self): + """A one-entry tasks[] array is the canonical single-task call (the + advertised interface is tasks-only), so it must NOT be rejected — + and short goals are legitimate for a single task.""" + with patch("tools.delegate_tool._run_single_child") as mock_run: + mock_run.return_value = { + "task_index": 0, "status": "completed", "summary": "done", + "api_calls": 1, "duration_seconds": 1.0, "_child_role": None, + } + result = _call([{"goal": GOOD_A}]) + self.assertNotIn("error", result) class TestValidBatchStillRuns(unittest.TestCase): diff --git a/tests/tools/test_delegate_control_actions.py b/tests/tools/test_delegate_control_actions.py index 9a6cd2182d..052e993c9c 100644 --- a/tests/tools/test_delegate_control_actions.py +++ b/tests/tools/test_delegate_control_actions.py @@ -274,7 +274,8 @@ def test_delegate_task_unknown_action_is_an_error(): def test_delegate_task_spawn_action_still_validates_goal(): out = delegate_task(action="spawn", parent_agent=_StubParent()) - assert "Provide either 'goal'" in out + assert "No tasks provided" in out + assert "one-entry" in out # teaching error carries the canonical shape def test_delegate_task_requires_parent_agent_for_control(): @@ -283,12 +284,11 @@ def test_delegate_task_requires_parent_agent_for_control(): def test_empty_tasks_array_with_goal_is_single_task_not_batch_error(): - """Small models emit tasks=[] alongside goal; that must not trip the - 'Batch mode requires at least 2 tasks' gate (observed live with - gpt-5.4-mini on Nous Portal).""" + """Small models emit tasks=[] alongside goal; that must not trip a + batch-count gate (observed live with gpt-5.4-mini on Nous Portal) — + it falls through to the no-tasks teaching error.""" out = delegate_task(tasks=[], goal="", parent_agent=_StubParent()) - # Falls through to the single-goal validation, not the batch gate. - assert "Provide either 'goal'" in out + assert "No tasks provided" in out assert "at least 2 tasks" not in out diff --git a/tests/tools/test_delegate_output_schema.py b/tests/tools/test_delegate_output_schema.py index 49dfb9e57a..bf59965c6b 100644 --- a/tests/tools/test_delegate_output_schema.py +++ b/tests/tools/test_delegate_output_schema.py @@ -133,10 +133,14 @@ class TestToolSchemaSurface: "properties" ]["tasks"]["items"]["required"] - def test_output_schema_on_top_level_goal_form(self): + def test_output_schema_advertised_per_task_only(self): + """output_schema is advertised inside tasks[] items (the only spawn + shape); the legacy top-level param stays handler-accepted but out + of the schema.""" props = DELEGATE_TASK_SCHEMA["parameters"]["properties"] - assert "output_schema" in props - assert props["output_schema"]["type"] == "object" + assert "output_schema" not in props + task_props = props["tasks"]["items"]["properties"] + assert task_props["output_schema"]["type"] == "object" # --------------------------------------------------------------------------- diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 250c4dc62c..d3c2a2dda8 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -1640,15 +1640,15 @@ def _build_child_agent( import uuid as _uuid # ── Role resolution ───────────────────────────────────────────────── - # Honor the caller's role only when BOTH the kill switch and the - # child's depth allow it. This is the single point where role - # degrades to 'leaf' — keeps the rule predictable. Callers pass - # the normalised role (_normalize_role ran in delegate_task) so - # we only deal with 'leaf' or 'orchestrator' here. + # Depth-derived, not caller-declared: a child may delegate iff the + # kill switch is on and depth budget remains below max_spawn_depth. + # The legacy `role` arg no longer participates (it asked the caller + # to guess a fact the config already knows); it is still accepted and + # normalised for wire compat, but capability comes from depth alone. child_depth = getattr(parent_agent, "_delegate_depth", 0) + 1 max_spawn = _get_max_spawn_depth() orchestrator_ok = _get_orchestrator_enabled() and child_depth < max_spawn - effective_role = role if (role == "orchestrator" and orchestrator_ok) else "leaf" + effective_role = "orchestrator" if orchestrator_ok else "leaf" # ── Subagent identity (stable across events, 0-indexed for TUI) ───── # subagent_id is generated here so the progress callback, the @@ -3580,19 +3580,16 @@ def _validate_batch_tasks(task_list: List[Dict[str, Any]]) -> Optional[str]: """Validate a tasks=[...] batch beyond per-task goal presence. Returns an actionable error string, or None when the batch is valid. - Batch-only by design: the single-`goal` form legitimately uses short - goals, so these checks must never run on it. + + A one-entry array is the canonical single-task shape (the advertised + interface is tasks-only; legacy top-level `goal` is wrapped into a + one-entry batch), so no minimum count is enforced. The placeholder/ + template checks below still run on every entry. Duplicate goals are deliberately NOT rejected: identical-goal fan-outs are a legitimate pattern (best-of-N / ensemble sampling), and blocking them broke real workflows (post-merge audit of #81141). """ - if len(task_list) < 2: - return ( - "Batch mode requires at least 2 tasks. For a single task, use " - "the `goal` parameter instead of `tasks`: " - 'delegate_task(goal="...", context="...").' - ) for i, task in enumerate(task_list): goal = str(task.get("goal", "")).strip() @@ -3612,7 +3609,11 @@ def _validate_batch_tasks(task_list: List[Dict[str, Any]]) -> Optional[str]: "calling delegate_task — subagents cannot resolve " "placeholders." ) - if len(goal) < _MIN_BATCH_GOAL_LEN: + if len(goal) < _MIN_BATCH_GOAL_LEN and len(task_list) >= 2: + # Multi-task fan-outs with terse goals are usually unexpanded + # templates; a SINGLE task legitimately uses short goals + # ("Fix the tests"), so one-entry arrays keep the historical + # single-`goal` exemption. return ( f"Task {i} goal is too short ({goal!r}). Write a specific, " "self-contained goal of at least " @@ -3772,7 +3773,11 @@ def delegate_task( single_task["output_schema"] = output_schema task_list = [single_task] else: - return tool_error("Provide either 'goal' (single task) or 'tasks' (batch).") + return tool_error( + "No tasks provided. Pass tasks=[{goal: '...', context: '...'}, " + "...] — one entry per subagent (a single task is a one-entry " + "array)." + ) if not task_list: return tool_error("No tasks provided.") @@ -4648,20 +4653,45 @@ def _build_top_level_description() -> str: top-level text stays static and duplication-free. If you add text here, check it is not already stated in a parameter description. """ + try: + orchestration_available = _get_max_spawn_depth() >= 2 and _get_orchestrator_enabled() + except Exception: + orchestration_available = False + + # The child-restrictions rule renders per config: on nesting-enabled + # installs the orchestrator clause is load-bearing; on depth-1/disabled + # installs (the default) it would describe an unreachable state — the + # role param already explains that 'orchestrator' is inert there. + # send_message is deliberately not named: it's gateway-internal + # vocabulary most sessions never see. The list below is the fail-safe + # superset; model_tools session-filters it to the tools the session + # actually has, dropping the whole line when none apply. + # Delegation capability is depth-derived (no role param): mention + # recursion only where it's actually available. + if orchestration_available: + restrictions_rule = ( + "- Children cannot call clarify, memory, or cronjob.\n" + "- Children can themselves delegate while depth remains " + f"(max_spawn_depth={_get_max_spawn_depth()}); the runtime " + "derives this from depth automatically.\n" + ) + else: + restrictions_rule = ( + "- Children cannot call delegate_task, clarify, memory, or " + "cronjob.\n" + ) + return ( "Spawn subagents in isolated contexts; each gets its own conversation, " "terminal session, and toolset, and only its final summary returns to " - "you. Provide 'goal' for a single task or 'tasks' for a parallel batch " - "(limits and nesting rules are in the parameter descriptions).\n\n" + "you. Pass every task in `tasks` — one entry spawns one subagent, " + "several run in parallel (limit in the tasks description).\n\n" "Runs in the background: dispatch returns immediately with live " - "transcript paths, and the completed result (one consolidated message " - "for a batch) re-enters the conversation on its own. Do NOT wait or " - "poll; continue other work.\n\n" - "LIVE ORCHESTRATION: while children run, this tool also controls " - "them — action='list' (live children + ids), action='steer' " - "(subagent_id + message, redirect without stopping), action='stop' " - "(subagent_id, end early; partial result still returns). Steer when " - "a live transcript shows a child drifting.\n\n" + "transcript paths, and the completed result (one consolidated message, " + "results in task order) re-enters the conversation on its own. Do NOT " + "wait or poll; continue other work. While children run, `action` " + "(list/steer/stop) controls them live — steer when a transcript shows " + "a child drifting.\n\n" "USE FOR: reasoning-heavy subtasks, work that would flood your context " "with intermediate data, or independent parallel workstreams.\n" "DO NOT USE FOR (use these instead):\n" @@ -4669,7 +4699,7 @@ def _build_top_level_description() -> str: "- A single tool call -> call the tool directly\n" "- Tasks needing user interaction -> subagents cannot ask questions\n" "- Durable work that must survive this session -> cronjob or " - "terminal(background=True, notify_on_complete=True); /stop, /new, or " + "terminal(background=True, notify=True); /stop, /new, or " "process exit discards running subagents.\n\n" "RULES:\n" "- Children know nothing of this conversation: pass everything needed " @@ -4679,14 +4709,10 @@ def _build_top_level_description() -> str: "claiming \"uploaded successfully\" or \"file written\" may be wrong. " "For external side effects (uploads, remote writes, publishing), " "require a verifiable handle (URL, ID, absolute path) and verify it " - "yourself — fetch the URL, stat the file, read back the content — " - "before telling the user the operation succeeded.\n" - "- Leaf children (the default) cannot call delegate_task, clarify, " - "memory, send_message, or cronjob; orchestrators regain only " - "delegate_task.\n" - "- Children inherit the parent model and fallback chain unless pinned " - "globally via delegation.provider / delegation.model in config.yaml. " - "Results are returned as an array, one entry per task." + "yourself before telling the user the operation succeeded.\n" + + restrictions_rule + + "- Children inherit the parent model unless pinned via " + "delegation.provider / delegation.model in config.yaml." ) @@ -4697,47 +4723,31 @@ def _build_tasks_param_description() -> str: except Exception: max_children = _DEFAULT_MAX_CONCURRENT_CHILDREN return ( - f"Batch mode: tasks to run in parallel (up to {max_children} for this " - f"user, set via delegation.max_concurrent_children). Each gets " - "its own subagent with isolated context and terminal session. " - "When provided, top-level goal/context/role are ignored." + f"The task(s), up to {max_children} in parallel for this user (set " + "via delegation.max_concurrent_children). Each entry spawns one " + "subagent with isolated context and terminal session; a single task " + "is a one-entry array. Required when spawning." ) def _build_role_param_description() -> str: - """Compose the 'role' parameter description with current spawn-depth limit.""" + """Legacy helper — the `role` param is no longer advertised. + + Delegation capability is depth-derived (see the role-resolution block in + _build_child_agent): a child may itself delegate iff + delegation.orchestrator_enabled and its depth < max_spawn_depth. The + handler still accepts role for wire compat (old transcripts, kanban + dispatcher) but ignores it. Kept because external callers import this + symbol; returns the depth story for any such use. + """ try: max_depth = _get_max_spawn_depth() except Exception: max_depth = MAX_DEPTH - try: - orchestrator_on = _get_orchestrator_enabled() - except Exception: - orchestrator_on = True - - if max_depth >= 2 and orchestrator_on: - nesting_note = ( - f"Nesting IS enabled for this user (max_spawn_depth={max_depth}): " - f"orchestrator children can themselves delegate up to {max_depth - 1} " - "more level(s) deep." - ) - elif max_depth >= 2 and not orchestrator_on: - nesting_note = ( - "Nesting is currently disabled " - "(delegation.orchestrator_enabled=false); 'orchestrator' is " - "silently forced to 'leaf'." - ) - else: - nesting_note = ( - f"Nesting is OFF for this user (max_spawn_depth={max_depth}); " - "'orchestrator' is silently forced to 'leaf'. Raise " - "delegation.max_spawn_depth in config.yaml to enable." - ) - return ( - "Role of the child agent. 'leaf' (default) = focused " - "worker, cannot delegate further. 'orchestrator' = can " - f"use delegate_task to spawn its own workers. {nesting_note}" + "Legacy parameter, ignored: whether a child can delegate is derived " + f"from delegation config (max_spawn_depth={max_depth}), not declared " + "by the caller." ) @@ -4756,7 +4766,6 @@ def _build_dynamic_schema_overrides() -> dict: k: dict(v) for k, v in DELEGATE_TASK_SCHEMA["parameters"]["properties"].items() } overrides_params["properties"]["tasks"]["description"] = _build_tasks_param_description() - overrides_params["properties"]["role"]["description"] = _build_role_param_description() return { "description": _build_top_level_description(), @@ -4782,48 +4791,44 @@ DELEGATE_TASK_SCHEMA = { "parameters": { "type": "object", "properties": { - "goal": { - "type": "string", - "description": ( - "What the subagent should accomplish. Be specific and " - "self-contained -- the subagent knows nothing about your " - "conversation history." - ), - }, - "context": { - "type": "string", - "description": ( - "Background information the subagent needs: file paths, " - "error messages, project structure, constraints. The more " - "specific you are, the better the subagent performs." - ), - }, + # NOTE: the handler also accepts the legacy single-goal shape — + # top-level `goal` (string), `context` (string), `output_schema` + # (object) — wrapped into a one-entry batch at dispatch. Legacy, + # unadvertised (old transcripts/callers only); tasks=[...] is the + # only advertised shape. Do not re-add these to the schema. "tasks": { "type": "array", + "minItems": 1, "items": { "type": "object", "properties": { - "goal": {"type": "string", "description": "Task goal"}, + "goal": { + "type": "string", + "description": ( + "What this subagent should accomplish. Be " + "specific and self-contained — it knows " + "nothing about your conversation history." + ), + }, "context": { "type": "string", - "description": "Task-specific context", - }, - "role": { - "type": "string", - "enum": ["leaf", "orchestrator"], - "description": "Per-task role override. See top-level 'role' for semantics.", + "description": ( + "Background THIS child needs: file paths, " + "error messages, constraints. Each child " + "sees only its own context — repeat shared " + "background in every task that needs it." + ), }, "output_schema": { "type": "object", "description": ( - "Optional JSON Schema the subagent's final " - "answer must validate against. The child is " - "told the contract up front; the parent " - "validates the final answer and allows one " - "bounded correction retry. The result entry " - "gains schema_valid (and schema_errors on " - "final failure). Keep schemas forgiving: " - "require only fields you will actually read." + "Optional JSON Schema this child's final " + "answer must validate against (told to the " + "child up front; parent validates with one " + "bounded correction retry; result gains " + "schema_valid, plus schema_errors on " + "failure). Keep it forgiving — require only " + "fields you will read." ), }, }, @@ -4832,63 +4837,41 @@ DELEGATE_TASK_SCHEMA = { # No maxItems — the runtime limit is configurable via # delegation.max_concurrent_children (default 3) and # enforced with a clear error in delegate_task(). + # NOTE: the handler also accepts a per-task `role` — legacy, + # ignored: delegation capability is depth-derived, not + # caller-declared. Unadvertised on purpose; do not re-add. "description": "(rebuilt at get_definitions() time)", }, - "role": { - "type": "string", - "enum": ["leaf", "orchestrator"], - "description": "(rebuilt at get_definitions() time)", - }, - "output_schema": { - "type": "object", - "description": ( - "Optional JSON Schema for the single-goal form — the " - "subagent's final answer must validate against it " - "(same semantics as tasks[].output_schema)." - ), - }, - "background": { - "type": "boolean", - "description": ( - "DEPRECATED / IGNORED. Top-level single and batch " - "delegations run in the background automatically — you do " - "not need to (and cannot) opt in or out. A single result or " - "consolidated batch result re-enters the conversation when " - "the work finishes; just continue working in the meantime. " - "Setting this has no effect; the parameter remains only for " - "backward compatibility." - ), - }, + # NOTE: the handler also accepts `background` (bool) — DEPRECATED, + # ignored: top-level delegations always run in the background. + # Deliberately unadvertised (old transcripts/callers only); do not + # re-add to the schema. "action": { "type": "string", "enum": ["spawn", "list", "steer", "stop"], "description": ( - "Default 'spawn' (omit for normal delegation). Live " - "orchestration of running subagents: 'list' shows this " - "conversation's live children (ids, goals, status, " - "transcript paths); 'steer' queues course-correction text " - "into one child (requires subagent_id + message) without " - "stopping it; 'stop' ends one child early (requires " - "subagent_id) — its partial result still returns as a " - "completion message. Control actions return immediately; " - "goal/tasks are ignored when action is not 'spawn'." + "Default 'spawn'. Live control of running children: " + "'list' = ids/goals/status/transcripts; 'steer' = queue " + "course-correction text into one child (subagent_id + " + "message) without stopping it; 'stop' = end one child " + "early (subagent_id; partial result still returns). " + "Control actions return immediately; goal/tasks are " + "ignored unless spawning." ), }, "subagent_id": { "type": "string", "description": ( - "Target for action='steer'/'stop'. Ids are returned in the " - "spawn dispatch response (subagent_ids) and by " - "action='list'." + "Target for action='steer'/'stop' (ids from the spawn " + "response or action='list')." ), }, "message": { "type": "string", "description": ( - "For action='steer': the course correction. Be directive " - "and specific — the child sees it appended to its next " - "tool result mid-run (e.g. \"Stop exploring X; focus on Y " - "and return early results\")." + "For action='steer': the course correction, appended to " + "the child's next tool result mid-run. Be directive and " + "specific." ), }, },