Skip to content

Commit a268030

Browse files
committed
feat(delegation): add per-task toolsets, persona, and timeout fields
Three new per-task fields on delegate_task batch items: - **toolsets** (string[]): restrict which tools a subagent loads. Previously hardcoded to None (inherit parent), now the model can narrow toolsets per task. Intersection with parent toolsets and blocked-tool stripping are handled by the existing validation chain. - **persona** (string): injected into the child's system prompt as a YOUR ROLE block to specialize behavior (e.g. 'web researcher', 'senior engineer') without cramming instructions into context. - **timeout** (number): per-task wall-clock cap in seconds. Overrides the global delegation.child_timeout_seconds for that subagent. None = inherit the global default (no timeout by default). All three are backward-compatible: single-task mode is unchanged, and batch tasks without the new fields default to None with existing inheritance behaviour preserved.
1 parent a4c0640 commit a268030

1 file changed

Lines changed: 46 additions & 5 deletions

File tree

tools/delegate_tool.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ def _build_child_system_prompt(
667667
role: str = "leaf",
668668
max_spawn_depth: int = 2,
669669
child_depth: int = 1,
670+
persona: Optional[str] = None,
670671
) -> str:
671672
"""Build a focused system prompt for a child agent.
672673
@@ -675,12 +676,18 @@ def _build_child_system_prompt(
675676
inspiration/openclaw/src/agents/subagent-system-prompt.ts:63-95).
676677
The depth note is literal truth (grounded in the passed config) so
677678
the LLM doesn't confabulate nesting capabilities that don't exist.
679+
680+
When ``persona`` is provided, it is injected as a "YOUR ROLE" block
681+
that specializes the subagent's behavior (e.g. "web researcher",
682+
"senior engineer", "code reviewer").
678683
"""
679684
parts = [
680685
"You are a focused subagent working on a specific delegated task.",
681686
"",
682687
f"YOUR TASK:\n{goal}",
683688
]
689+
if persona and persona.strip():
690+
parts.append(f"\nYOUR ROLE:\n{persona.strip()}")
684691
if context and context.strip():
685692
parts.append(f"\nCONTEXT:\n{context}")
686693
if workspace_path and str(workspace_path).strip():
@@ -1065,6 +1072,13 @@ def _build_child_agent(
10651072
# 'leaf' (default) cannot; 'orchestrator' retains the delegation
10661073
# toolset subject to depth/kill-switch bounds applied below.
10671074
role: str = "leaf",
1075+
# Per-task persona — injected into the child's system prompt as
1076+
# a "YOUR ROLE" block to specialize behavior.
1077+
persona: Optional[str] = None,
1078+
# Per-task wall-clock timeout in seconds. Overrides the global
1079+
# delegation.child_timeout_seconds for this child. None = inherit
1080+
# the global config default (which itself defaults to no timeout).
1081+
timeout: Optional[float] = None,
10681082
):
10691083
"""
10701084
Build a child AIAgent on the main thread (thread-safe construction).
@@ -1152,6 +1166,7 @@ def _build_child_agent(
11521166
role=effective_role,
11531167
max_spawn_depth=max_spawn,
11541168
child_depth=child_depth,
1169+
persona=persona,
11551170
)
11561171
# Extract parent's API key so subagents inherit auth (e.g. Nous Portal).
11571172
parent_api_key = getattr(parent_agent, "api_key", None)
@@ -1373,6 +1388,8 @@ def _child_thinking(text: str) -> None:
13731388
child._parent_subagent_id = parent_subagent_id
13741389
child._subagent_goal = goal
13751390
child._parent_turn_id = getattr(parent_agent, "_current_turn_id", "") or ""
1391+
# Per-task timeout override (None = inherit global config default)
1392+
child._delegate_timeout = timeout
13761393
# Stable sidebar marker: delegate subagent sessions must stay out of
13771394
# session pickers even when a parent delete orphans them (parent_session_id
13781395
# → NULL). Mirrors /branch's ``_branched_from`` pattern — see
@@ -1927,7 +1944,9 @@ def _heartbeat_loop():
19271944
# Run child with an optional hard timeout (off by default —
19281945
# result(timeout=None) blocks until the child finishes). Stuck-child
19291946
# protection comes from the heartbeat staleness monitor instead.
1930-
child_timeout = _get_child_timeout()
1947+
# Per-task timeout (child._delegate_timeout) beats the global config.
1948+
_per_task_timeout = getattr(child, "_delegate_timeout", None)
1949+
child_timeout = _per_task_timeout if _per_task_timeout is not None else _get_child_timeout()
19311950
# Daemon worker (tools.daemon_pool): a timed-out child is abandoned
19321951
# below; a stdlib non-daemon worker would then block interpreter
19331952
# exit at atexit-join time if the child never unwinds.
@@ -2527,13 +2546,16 @@ def delegate_task(
25272546
# Per-task role beats top-level; normalise again so unknown
25282547
# per-task values warn and degrade to leaf uniformly.
25292548
effective_role = _normalize_role(t.get("role") or top_role)
2549+
# Per-task toolsets: when provided, the model can narrow which
2550+
# tools a subagent gets. Intersection with parent + blocked-tool
2551+
# stripping is handled inside _build_child_agent. None = inherit
2552+
# the parent's full toolset (existing behaviour).
2553+
per_task_toolsets = t.get("toolsets")
25302554
child = _build_child_agent(
25312555
task_index=i,
25322556
goal=t["goal"],
25332557
context=t.get("context"),
2534-
# Subagents always inherit the parent's toolsets; the model
2535-
# cannot choose or narrow them (no model-facing toolsets arg).
2536-
toolsets=None,
2558+
toolsets=per_task_toolsets,
25372559
model=creds["model"],
25382560
max_iterations=effective_max_iter,
25392561
task_count=n_tasks,
@@ -2547,6 +2569,8 @@ def delegate_task(
25472569
override_acp_command=creds.get("command"),
25482570
override_acp_args=creds.get("args"),
25492571
role=effective_role,
2572+
persona=t.get("persona"),
2573+
timeout=t.get("timeout"),
25502574
)
25512575
# Override with correct parent tool names (before child construction mutated global)
25522576
child._delegate_saved_tool_names = _parent_tool_names
@@ -3324,6 +3348,7 @@ def _build_top_level_description() -> str:
33243348
"delegation.orchestrator_enabled=false.\n"
33253349
"- Subagent model is NOT selectable per call: children inherit the parent model (plus its fallback chain) unless you pin all subagents to a model via delegation.provider / delegation.model in config.yaml.\n"
33263350
"- Each subagent gets its own terminal session (separate working directory and state).\n"
3351+
"- Per-task fields on batch items: 'toolsets' (restrict which tools the subagent loads), 'persona' (specialize its behavior via a role description), 'timeout' (per-task wall-clock cap in seconds).\n"
33273352
"- Results are always returned as an array, one entry per task."
33283353
)
33293354

@@ -3338,7 +3363,10 @@ def _build_tasks_param_description() -> str:
33383363
f"Batch mode: tasks to run in parallel (up to {max_children} for this "
33393364
f"user, set via delegation.max_concurrent_children). Each gets "
33403365
"its own subagent with isolated context and terminal session. "
3341-
"When provided, top-level goal/context/role are ignored."
3366+
"Per-task fields: 'goal' (required), 'context', 'role', 'toolsets' "
3367+
"(restrict which tools the subagent loads), 'persona' (specialize "
3368+
"its behavior), 'timeout' (per-task wall-clock cap in seconds). "
3369+
"When provided, top-level goal/context/toolsets are ignored."
33423370
)
33433371

33443372

@@ -3451,6 +3479,19 @@ def _build_dynamic_schema_overrides() -> dict:
34513479
"enum": ["leaf", "orchestrator"],
34523480
"description": "Per-task role override. See top-level 'role' for semantics.",
34533481
},
3482+
"toolsets": {
3483+
"type": "array",
3484+
"items": {"type": "string"},
3485+
"description": "Optional toolset names to restrict this subagent to (e.g. [\"web\", \"terminal\", \"file\"]). When set, only tools from these toolsets are loaded, significantly reducing input token overhead. When omitted, the subagent inherits the parent's full toolset. Infer from the task — e.g. use [\"web\"] for research, [\"terminal\", \"file\"] for coding, [\"web\", \"terminal\", \"file\", \"delegation\"] for orchestrator tasks.",
3486+
},
3487+
"persona": {
3488+
"type": "string",
3489+
"description": "Optional role/persona description injected into the subagent's system prompt (e.g. 'web researcher — focus on finding authoritative sources, cite URLs', 'senior engineer — prioritize correctness, add error handling'). Helps specialize subagent behavior without cramming instructions into context.",
3490+
},
3491+
"timeout": {
3492+
"type": "number",
3493+
"description": "Optional per-task wall-clock timeout in seconds. Overrides the global delegation.child_timeout_seconds for this subagent. Use a short timeout for quick lookups (e.g. 30) and a longer one for deep analysis (e.g. 300). Omit to inherit the global default (no timeout by default).",
3494+
},
34543495
},
34553496
"required": ["goal"],
34563497
},

0 commit comments

Comments
 (0)