Skip to content

feat: Orchestrator Subagents - #11215

Closed
pefontana wants to merge 22 commits into
NousResearch:mainfrom
pefontana:orchestrator-role
Closed

feat: Orchestrator Subagents#11215
pefontana wants to merge 22 commits into
NousResearch:mainfrom
pefontana:orchestrator-role

Conversation

@pefontana

Copy link
Copy Markdown
Contributor

Summary

  • Subagents can now spawn their own subagents. Opt in with role="orchestrator" on delegate_task — the child retains the delegation toolset and can parallelize its own workers. Bounded by delegation.max_spawn_depth (1–3, default 2); flip delegation.orchestrator_enabled: false to disable globally.
  • Higher default parallelism. Batch mode now runs up to 5 concurrent subagents (was 3), hard cap 8. Tune via delegation.max_concurrent_children.
  • Dead config removed. delegation.default_toolsets was 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.
image

Delegation plumbing cleanup

  • Both delegate_task call sites in run_agent.py now route through a single _dispatch_delegate_task helper. Fixes a silent drop of acp_command / acp_args on the main agent loop — those fields were in the schema but never forwarded.
  • DelegateEvent enum with back-compat aliases for the existing progress event strings consumed by the gateway SSE, ACP adapter, and CLI spinner.
  • Default max_concurrent_children: 3 → 5, with an absolute cap of 8 (aligned with OpenClaw's DEFAULT_SUBAGENT_MAX_CONCURRENT). Values above the cap clamp with a warning log.

Remove dead default_toolsets config

  • delegation.default_toolsets was declared in cli.py's CLI_CONFIG, documented in cli-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 hardcoded DEFAULT_TOOLSETS module constant in tools/delegate_tool.py.
  • Removed from all three surfaces.
  • Regression test in tests/hermes_cli/test_config_drift.py guards against re-introduction.

Orchestrator role

  • New role: "leaf" | "orchestrator" parameter on delegate_task (top-level and per-task in batch mode). Leaf children are unchanged; orchestrator children retain the delegation toolset 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 (default true) is a global kill switch that forces every child to leaf regardless of the per-call role.
  • End-to-end test covers parent → orchestrator (depth 1) → two leaves (depth 2) nesting with full role/toolset/depth invariants.

Follow-ups from review

  • TASK_PROGRESS events relayed upward by nested orchestrators were falling through to the TASK_TOOL_STARTED renderer, which treated the batched summary string as if it were a tool name. Added an explicit TASK_PROGRESS branch with pass-through relay and a distinct render. Reachable only once nesting is enabled.
  • _build_child_progress_callback now accepts DelegateEvent enum values and new-style "delegate.*" strings in addition to the legacy strings.
  • website/docs/guides/delegation-patterns.md updated to match features/delegation.md on nested-delegation opt-in.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • ♻️ Refactor (no behavior change)
  • 📝 Documentation update
  • ✅ Tests

How to Test

  1. pytest tests/tools/test_delegate.py tests/hermes_cli/test_config_drift.py -v — 102 passing.
  2. Schema: python -c "from tools.delegate_tool import DELEGATE_TASK_SCHEMA as S; assert S['parameters']['properties']['role']['enum'] == ['leaf', 'orchestrator']; print('schema OK')"
  3. Defaults: 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')"
  4. Back-compat: python -c "from tools.delegate_tool import MAX_DEPTH; assert MAX_DEPTH == 2; print('back-compat OK')"
  5. Docs: grep default_toolsets in tools/, cli*.yaml*, and website/docs/**/*.md — only audit-only references remain (class variables on Atropos environments, local var in hermes_cli/dump.py, the regression test itself).

Backward Compatibility

  • role defaults to "leaf" — no existing caller changes behavior.
  • MAX_DEPTH = 2 constant remains as the hardcoded fallback and is still exported for tests.
  • Progress event consumers get both old string names AND new enum values during the deprecation window.
  • default_toolsets was never functional, so removing it changes no observable behavior.

Checklist

  • Tests added (102 passing)
  • Docs updated (website/docs/user-guide/features/delegation.md, website/docs/guides/delegation-patterns.md)
  • cli-config.yaml.example updated for new config keys
  • Conventional Commits (feat(delegate): / refactor(delegate): / fix(delegate): / docs(delegate): / test(delegate): / chore(delegate):)
  • No unrelated changes

… 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
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged the 3 default_toolsets cleanup commits onto current main in #13681 (merged — commits 631e879 / baaf49e / 7c3c7e5, your authorship preserved via rebase-merge). Thanks!

The larger pieces — concurrency bump 3→5, role="orchestrator", max_spawn_depth, DelegateEvent enum, nested delegation — are still open here for separate review.

teknium1 pushed a commit that referenced this pull request Apr 21, 2026
…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>
@teknium1

Copy link
Copy Markdown
Contributor

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 default_toolsets cleanup is already on main from #13681 (also your authorship).

Two deliberate deviations from your original defaults, per Teknium:

  • max_concurrent_children stays at 3 (your PR bumped to 5 + cap 8 — we kept the floor only, no upper ceiling so users can raise it as high as they want)
  • max_spawn_depth defaults to 1 / flat (your PR defaulted to 2 which silently enabled one level of orchestration for every user — we made nested an explicit opt-in via config.yaml)

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.

@teknium1 teknium1 closed this Apr 21, 2026
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 21, 2026
@alt-glitch alt-glitch added the tool/delegate Subagent delegation label Apr 21, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #13691 — salvaged and merged as #13691 by teknium1.

@pefontana

Copy link
Copy Markdown
Contributor Author

Great!

teknium1 added a commit that referenced this pull request Apr 21, 2026
…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.
aj-nt pushed a commit to aj-nt/hermes-agent that referenced this pull request May 1, 2026
…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>
aj-nt pushed a commit to aj-nt/hermes-agent that referenced this pull request May 1, 2026
…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.
Luminet2023 pushed a commit to Luminet2023/hermes-agent that referenced this pull request May 1, 2026
…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>
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…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>
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…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.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…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>
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…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.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…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>
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…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.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…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>
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…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.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…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>
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants