refactor(delegate_task): tasks-only interface + depth-derived delegation (1,201 → 773 tok/call, −36%) (#96424)
* 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)
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+123
-140
@@ -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."
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user