feat: Orchestrator Subagents - #11215
Conversation
… params Both delegate_task call sites in run_agent.py hardcoded a subset of the schema params (goal, context, toolsets, tasks, max_iterations) and silently dropped acp_command and acp_args. Any future schema additions would hit the same drift. Replace both call sites with _dispatch_delegate_task() which forwards the entire validated schema from function_args. Also threads the conversation messages reference through for future context inheritance plumbing (M1).
Replace informal progress event strings (_thinking, tool.started, etc.) with a DelegateEvent enum. The callback normalises incoming legacy strings through _LEGACY_EVENT_MAP. External consumers (gateway SSE, ACP adapter, CLI) continue to receive legacy string names during the deprecation window — no consumer changes required.
Raise _DEFAULT_MAX_CONCURRENT_CHILDREN from 3 to 5. Add an absolute cap of 8 (aligned with OpenClaw's DEFAULT_SUBAGENT_MAX_CONCURRENT) — values above 8 from config or env are clamped with a warning log. Update schema description, cli-config.yaml.example, and delegation docs to reflect the new defaults.
- TestDispatchDelegateTask: verifies acp_command/acp_args forwarding and that _dispatch_delegate_task threads messages through - TestDelegateEventEnum: enum values, legacy map coverage, normalisation in the progress callback, unknown event rejection - TestConcurrencyDefaults: default=5, cap at 8, warning log on clamp, env var cap, within-range passthrough - Fix pre-existing test bug: test_task_index_prefix_in_batch_mode was passing tool_name as event_type (wrong signature) - Update test_constants assertion from 3 to 5
Extract duplicated cap-check logic from _get_max_concurrent_children into _clamp_concurrency helper. Fix stale "default 3" comment in the schema. Simplify test_acp_args_forwarded assertion.
… enum members Add test_progress_callback_normalises_thinking (both _thinking and reasoning.available), test_progress_callback_tool_completed_is_noop. Document that TASK_SPAWNED/COMPLETED/FAILED are reserved for M3. Rename test_progress_callback_normalises_legacy_events for clarity.
Grep found 4 more places still saying "up to 3" — schema description, tips.py, delegation-patterns.md, overview.md. Updated to match new default of 5 (max 8).
…tring The `messages` kwarg was threaded through `_dispatch_delegate_task` into `delegate_task` but never referenced inside the function body. Readers (including reviewers) kept assuming parent conversation history was being forwarded to child agents, which it is not. Remove the dead parameter and the test that asserted forwarding so the code matches the behavior. Also rephrase `_dispatch_delegate_task`'s docstring: the consolidation gives us a single call site, not automatic param forwarding — new schema fields still need to be added in one place.
delegation.default_toolsets was declared in cli.py's CLI_CONFIG default dict and documented in cli-config.yaml.example, but never read: none of tools/delegate_tool.py, _load_config(), or any call site ever looked it up. The live fallback is the DEFAULT_TOOLSETS module constant at tools/delegate_tool.py:101, which stays as-is. hermes_cli/config.py's DEFAULT_CONFIG["delegation"] already omits the key — this commit aligns cli.py with that. Adds a regression test in tests/hermes_cli/test_config_drift.py so a future refactor that re-adds the key without wiring it up to _load_config() fails loudly. Part of Initiative 2 / M0.5.
Matches the default-config removal in the preceding commit. default_toolsets was documented for users to set but was never actually read at runtime, so showing it in the example config and the delegation user guide was misleading. No deprecation note is added: the key was always a no-op, so users who copied it from the example continue to see no behavior change. Their config.yaml still parses; the key is just silently unused, same as before. Part of Initiative 2 / M0.5.
…config
The prior form of this test asserted on CLI_CONFIG["delegation"] after
importing cli, which only passed by accident of pytest-xdist worker
scheduling. cli._hermes_home is frozen at module import time (cli.py:76),
before the tests/conftest.py autouse HERMES_HOME-isolation fixture can
fire, so CLI_CONFIG ends up populated by deep-merging the contributor's
actual ~/.hermes/config.yaml over the defaults (cli.py:359-366). Any
contributor (like me) who still has the legacy key set in their own
config causes a false failure the moment another test file in the same
xdist worker imports cli at module level.
Asserting on the source of load_cli_config() instead sidesteps all of
that: the test now checks the defaults literal directly and is
independent of user config, HERMES_HOME, import order, and worker
scheduling.
Demonstrated failure mode before this fix:
pytest tests/hermes_cli/test_config_drift.py \
tests/hermes_cli/test_skills_hub.py -o addopts=""
-> FAILED (CLI_CONFIG["delegation"] contained "default_toolsets"
from the user's ~/.hermes/config.yaml)
Part of Initiative 2 / M0.5.
Introduces the configurable depth cap and global kill switch for the M3 orchestrator-role feature. No behavior change on defaults: max_spawn_depth=2 matches the legacy MAX_DEPTH=2 hard-coded value; orchestrator_enabled=True is a no-op until M3 commit 3 wires up role. Changes: - tools/delegate_tool.py: _MIN_SPAWN_DEPTH, _MAX_SPAWN_DEPTH_CAP, _get_max_spawn_depth() (clamps to [1, 3] with warning log, mirrors existing _clamp_concurrency pattern), _get_orchestrator_enabled() with bool/string YAML coercion. Depth guard at delegate_task now reads _get_max_spawn_depth() instead of MAX_DEPTH directly. MAX_DEPTH stays as the hardcoded default fallback and test import. - hermes_cli/config.py: DEFAULT_CONFIG["delegation"] seeds the two new keys. Not seeded in cli.py:CLI_CONFIG — follows the delegation.reasoning_effort precedent; cli.py's deep-merge picks up user overrides regardless. - tests/tools/test_delegate.py: TestMaxSpawnDepth (4 cases — default, clamp-low, clamp-high, invalid-falls-back).
Wires the 'role' param through schema -> delegate_task() -> dispatch ->
_build_child_agent -> stashed on child. No behavior change yet: Commit 3
adds the toolset re-add + role-aware prompt. Commit 2 verifies the
plumbing reaches the child and the schema description signals the
feature to the parent LLM.
Changes:
- tools/delegate_tool.py:
- Module-level _normalize_role(r) (near _clamp_concurrency), returns
'leaf' or 'orchestrator'; unknown strings warn and coerce to 'leaf'.
- DELEGATE_TASK_SCHEMA: new 'role' property at top level AND per-task
under tasks[].items. Top-level description text split into leaf vs
orchestrator capability statements so the parent LLM discovers that
role='orchestrator' unlocks nested delegation.
- delegate_task(): accepts role=Optional[str]; normalises top_role;
single-task dict at :738 now includes 'role' for batch/single
uniformity; child-build loop resolves effective_role = normalise(
t.get('role') or top_role) and forwards to _build_child_agent.
- _build_child_agent(): accepts role='leaf' kwarg; stashes
child._delegate_role for introspection (commit 3 will overwrite
with effective_role post-degrade).
- Registry handler lambda: forwards role=args.get('role') for the
Atropos dispatch path (dead for run_agent.py which short-circuits
to _dispatch_delegate_task).
- run_agent.py:_dispatch_delegate_task: forwards role through to
tools.delegate_tool.delegate_task.
- tests/tools/test_delegate.py:TestOrchestratorRoleSchema (4 cases —
default→leaf, explicit orchestrator stashed, nonsense→leaf+warning,
schema shape assertions for top-level and per-task 'role' properties).
The behavior change. Orchestrator children (role='orchestrator',
allowed by delegation.orchestrator_enabled and child_depth <
max_spawn_depth) retain the 'delegation' toolset and receive a
role-aware system prompt derived from OpenClaw's buildSubagentSystemPrompt
canSpawn branch. Leaf children are unchanged from pre-M3 behavior.
Changes:
- tools/delegate_tool.py:
- _build_child_agent: role resolution block at the top — computes
child_depth, max_spawn, orchestrator_ok (kill switch AND depth),
effective_role (single degrade point). Toolset re-add appends
"delegation" when effective_role == 'orchestrator' (runs after the
existing _strip_blocked_tools branches — unconditional on parent
toolset membership since orchestrator capability is granted by role
not inheritance; documented in test_intersection_preserves_delegation_bound).
child._delegate_role now stashes effective_role (post-degrade).
- _build_child_system_prompt: new role/max_spawn_depth/child_depth
kwargs; leaf prompt unchanged; orchestrator appends a spawning
block with WHEN/WHEN NOT to delegate guidance + literal depth
note that branches between "children MUST be leaves" (at the
floor) and "children can themselves be orchestrators" (below it).
Per-call role model means "can be", not "will be" — orchestrators
explicitly pass role='orchestrator' for nested delegation.
- _EXCLUDED_TOOLSET_NAMES: comment explaining the "delegation"
entry is an advertising exclusion, not a runtime block; the
role-driven re-add in _build_child_agent overrides it.
- tests/tools/test_delegate.py: TestOrchestratorRoleBehavior (9 cases)
- Role resolution: _keeps_delegation_at_depth_1,
_blocked_at_max_spawn_depth, _enabled_false_forces_leaf
- Prompt content: _leaf_does_not_mention_delegation,
_orchestrator_mentions_delegation_capability,
_at_depth_floor_says_children_are_leaves,
_below_floor_allows_more_nesting
- Batch + intersection: _batch_mode_per_task_role_override,
_intersection_preserves_delegation_bound (documents design choice)
Satisfies parent plan §7 item 3 acceptance: parent delegates to an
orchestrator child, which delegates to two leaf grandchildren; results
bubble up correctly.
Mocking strategy (plan §3.6 G3 sketch): single run_agent.AIAgent patch
with a side_effect factory that keys on the child's
ephemeral_system_prompt — orchestrator prompts contain the string
"Orchestrator Role" (see _build_child_system_prompt), leaves don't.
The orchestrator mock's run_conversation recursively calls
delegate_task with tasks=[{goal:...},{goal:...}] to spawn two leaves.
This keeps the whole test in one patch context and avoids depth-indexed
nesting patterns that are fragile.
Also updates test_constants to cover the two new config getters
(_get_max_spawn_depth, _get_orchestrator_enabled) and the two new
bound constants (_MIN_SPAWN_DEPTH=1, _MAX_SPAWN_DEPTH_CAP=3), as
called for by plan §4 Commit 4.
Assertions: MockAgent called exactly 3 times (1 orchestrator + 2
leaves); orchestrator got 'delegation' in its toolset and an
orchestrator prompt; both grandchildren did NOT get 'delegation' and
received leaf prompts.
- cli-config.yaml.example: two commented-out lines in the delegation
block advertising max_spawn_depth and orchestrator_enabled.
- website/docs/user-guide/features/delegation.md:
- Replace "Depth Limit" section with "Depth Limit and Nested
Orchestration": role='leaf' vs 'orchestrator' usage example,
max_spawn_depth bounds, orchestrator_enabled kill switch, and the
125-leaf cost warning for max_spawn_depth=3.
- "Key Properties" bullets updated to reflect opt-in nested
delegation and to split leaf/orchestrator capability statements.
- Configuration YAML example: two commented-out lines for the new
keys, matching the cli-config.yaml.example style.
Pure text; independently revertable.
…child callback
Addresses two bugs surfaced by codex in the M3 PR review
(.multi-agent/review-20260415-173416/reviews/codex.md):
1. HIGH — TASK_PROGRESS fall-through to TASK_TOOL_STARTED rendering.
_LEGACY_EVENT_MAP maps "subagent_progress" to DelegateEvent.TASK_PROGRESS,
but _build_child_progress_callback had no TASK_PROGRESS branch. Any
TASK_PROGRESS event fell through to the TASK_TOOL_STARTED display/batch
block, which treated the pre-batched summary string (in the tool_name
positional slot) as if it were a tool name — rendering
'├─ ⚡ 🔀 [1] terminal, file' and re-batching accumulated emoji prefixes
on each upward hop. This path is newly reachable in M3: nested
orchestrators relay subagent_progress from grandchildren upward via
this callback. Before M3 the toolset strip blocked the nesting that
produces this traffic.
Fix: explicit TASK_PROGRESS branch. Renders with a distinct 🔀 prefix
(no get_tool_emoji lookup) and relays upward as-is without re-batching
(the payload is already a batched summary).
2. MEDIUM — DelegateEvent enum values silently dropped. The callback
only did _LEGACY_EVENT_MAP.get(event_type), so cb(DelegateEvent.TASK_THINKING,
...) or cb('delegate.task_thinking', ...) produced no output. The enum
was added in M0 as the "new normalized event type" but no call path
accepted it.
Fix: normalize enum instances directly, then fall back to the legacy
map, then to DelegateEvent(str) construction for new-style "delegate.*"
strings.
Tests added (tests/tools/test_delegate.py, in TestDelegateEventEnum):
- test_progress_callback_accepts_enum_value_directly
- test_progress_callback_accepts_new_style_string
- test_progress_callback_task_progress_not_misrendered
Addresses the hermes reviewer finding in the M3 PR review (.multi-agent/review-20260415-173416/reviews/hermes.md §warnings #3): the Constraints section's "No nesting" bullet was stale against M3. Replaces with a "Nested delegation is opt-in" bullet that mirrors the wording already landed in website/docs/user-guide/features/delegation.md in the M3 docs commit (55aecde). Covers role='leaf' vs 'orchestrator', the max_spawn_depth bound, and the orchestrator_enabled kill switch — matching the feature doc's leaf-vs-orchestrator capability distinction.
Removes internal milestone references (M0, M0.5, M3) from code, tests, and docs in the delegation PR surface. Milestone tags were useful for tracking the rollout but carry no meaning for upstream readers or future maintainers — the feature and its rationale should stand on its own. Mechanical substitutions only — no behavior change, no docstring rewrites, no test renames. 102 tests still pass.
Two small follow-ups after the orchestrator-role PR review: 1. hermes_cli/config.py:DEFAULT_CONFIG["delegation"] now seeds max_concurrent_children=5 alongside max_spawn_depth and orchestrator_enabled. cli-config.yaml.example, docs, and the schema all advertise this key; the canonical default dict was the only surface that still omitted it. 2. website/docs/user-guide/features/delegation.md's "always blocked for subagents" bullet list was stale against the new orchestrator role: delegation is retained for role="orchestrator" children (see _build_child_agent re-add). Softened the heading from "always blocked" to "blocked", and rewrote the delegation bullet to point at the Depth Limit and Nested Orchestration section. Tests still pass (123).
# Conflicts: # tests/agent/test_subagent_progress.py # tools/delegate_tool.py
|
Salvaged the 3 The larger pieces — concurrency bump 3→5, |
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR #11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
|
Closing in favor of #13691, which salvaged the orchestrator role + configurable spawn depth pieces from this PR onto current main (your authorship preserved via rebase-merge, commit 48ecb98). The separately-merged Two deliberate deviations from your original defaults, per Teknium:
Everything else — role plumbing, DelegateEvent enum with legacy back-compat, role-aware system prompt, TASK_PROGRESS render fix, dispatch helper acp_args fix, the 102 tests — landed as you wrote them. Thanks for the substantial work here. |
|
Great! |
…bagents (#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from #11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
Summary
role="orchestrator"ondelegate_task— the child retains thedelegationtoolset and can parallelize its own workers. Bounded bydelegation.max_spawn_depth(1–3, default 2); flipdelegation.orchestrator_enabled: falseto disable globally.delegation.max_concurrent_children.delegation.default_toolsetswas documented but never read; removed from the example config and docs. No behavior change — existing configs still parse.Three delegation-related changes, each self-contained and reviewable in isolation.

Delegation plumbing cleanup
delegate_taskcall sites inrun_agent.pynow route through a single_dispatch_delegate_taskhelper. Fixes a silent drop ofacp_command/acp_argson the main agent loop — those fields were in the schema but never forwarded.DelegateEventenum with back-compat aliases for the existing progress event strings consumed by the gateway SSE, ACP adapter, and CLI spinner.max_concurrent_children: 3 → 5, with an absolute cap of 8 (aligned with OpenClaw'sDEFAULT_SUBAGENT_MAX_CONCURRENT). Values above the cap clamp with a warning log.Remove dead
default_toolsetsconfigdelegation.default_toolsetswas declared incli.py'sCLI_CONFIG, documented incli-config.yaml.example, and documented in the delegation feature docs, but never consulted at runtime._load_config()ignored it entirely; the live fallback is the hardcodedDEFAULT_TOOLSETSmodule constant intools/delegate_tool.py.tests/hermes_cli/test_config_drift.pyguards against re-introduction.Orchestrator role
role: "leaf" | "orchestrator"parameter ondelegate_task(top-level and per-task in batch mode). Leaf children are unchanged; orchestrator children retain thedelegationtoolset and receive a role-aware system prompt telling them they can spawn their own workers.delegation.max_spawn_depth(1-3, default 2) bounds the delegation tree — orchestrator requests are silently coerced to leaf when the child would exceed the depth cap.delegation.orchestrator_enabled(defaulttrue) is a global kill switch that forces every child to leaf regardless of the per-callrole.Follow-ups from review
TASK_PROGRESSevents relayed upward by nested orchestrators were falling through to theTASK_TOOL_STARTEDrenderer, which treated the batched summary string as if it were a tool name. Added an explicitTASK_PROGRESSbranch with pass-through relay and a distinct render. Reachable only once nesting is enabled._build_child_progress_callbacknow acceptsDelegateEventenum values and new-style"delegate.*"strings in addition to the legacy strings.website/docs/guides/delegation-patterns.mdupdated to matchfeatures/delegation.mdon nested-delegation opt-in.Type of Change
How to Test
pytest tests/tools/test_delegate.py tests/hermes_cli/test_config_drift.py -v— 102 passing.python -c "from tools.delegate_tool import DELEGATE_TASK_SCHEMA as S; assert S['parameters']['properties']['role']['enum'] == ['leaf', 'orchestrator']; print('schema OK')"python -c "from hermes_cli.config import DEFAULT_CONFIG as C; assert C['delegation']['max_spawn_depth'] == 2 and C['delegation']['orchestrator_enabled'] is True; print('defaults OK')"python -c "from tools.delegate_tool import MAX_DEPTH; assert MAX_DEPTH == 2; print('back-compat OK')"default_toolsetsintools/,cli*.yaml*, andwebsite/docs/**/*.md— only audit-only references remain (class variables on Atropos environments, local var inhermes_cli/dump.py, the regression test itself).Backward Compatibility
roledefaults to"leaf"— no existing caller changes behavior.MAX_DEPTH = 2constant remains as the hardcoded fallback and is still exported for tests.default_toolsetswas never functional, so removing it changes no observable behavior.Checklist
website/docs/user-guide/features/delegation.md,website/docs/guides/delegation-patterns.md)cli-config.yaml.exampleupdated for new config keysfeat(delegate):/refactor(delegate):/fix(delegate):/docs(delegate):/test(delegate):/chore(delegate):)