Skip to content

feat(delegate): add phase-based delegation routing - #3

Open
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-55885
Open

feat(delegate): add phase-based delegation routing#3
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-55885

Conversation

@hashbender

Copy link
Copy Markdown
Owner

What does this PR do?

Adds config-driven phase-based routing for delegated subagents, so orchestrators can pass a semantic phase to delegate_task and Hermes can select the configured provider/model/effort for that phase while preserving the existing default fallback behavior.

This keeps routing deterministic and cache-friendly: the model only passes a phase label, while concrete provider/model credentials stay in config.yaml.

Related Issue

Refs NousResearch#54839

Related work checked before opening this PR:

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • tools/delegate_tool.py — adds the optional phase parameter, resolves delegation.phase_assignments, and preserves backwards-compatible fallback to the current delegation config.
  • hermes_cli/config.py — documents the new default delegation.phase_assignments config key.
  • run_agent.py — preserves phase when live delegate dispatch converts tool calls into child tasks.
  • tools/async_delegation.py and tools/process_registry.py — carry/render mixed async child metadata when a batch uses different routed models/providers.
  • tests/tools/test_delegate.py — covers phase routing, invalid config warnings, fallback behavior, and provider/direct-endpoint precedence.
  • tests/tools/test_async_delegation.py — covers async metadata for mixed routed child tasks.
  • website/docs/user-guide/configuration.md and website/docs/user-guide/features/delegation.md — add user-facing configuration and usage docs.

How to Test

  1. python -m pytest tests/tools/test_delegate.py tests/tools/test_async_delegation.py -q
  2. git diff --check origin/main...HEAD

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: WSL / Linux

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

N/A — this PR does not add a skill.

Screenshots / Logs

Targeted verification:

python -m pytest tests/tools/test_delegate.py tests/tools/test_async_delegation.py -q
183 passed in 15.58s

git diff --check origin/main...HEAD
# clean

python -m pytest tests/ -q
# attempted; timed out after 600s around 18% in the existing large suite, so this PR relies on the targeted delegation suite above.

Mirror-of: NousResearch#55885
NousResearch#55885

@tenki-reviewer

tenki-reviewer Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Complete

Files Reviewed: 9
Findings: 2

By Severity:

  • 🟠 High: 1
  • 🟢 Low: 1

Three bugs found in the per-phase delegation routing: a restrictive reasoning_effort guard that silently drops 'minimal' and 'none' levels (high), a per-phase api_key being silently ignored when a named provider is specified (medium), and orphaned child agents left registered on batch credential failures (low).

Files Reviewed (9 files)
hermes_cli/config.py
run_agent.py
tests/tools/test_async_delegation.py
tests/tools/test_delegate.py
tools/async_delegation.py
tools/delegate_tool.py
tools/process_registry.py
website/docs/user-guide/configuration.md
website/docs/user-guide/features/delegation.md

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: 🟠 High (68/100) — 1 high finding, 1 low · 744 LOC across 9 files


Summary

This PR introduces per-phase delegation routing (model, provider, reasoning config per phase) and adds a process_registry for enhanced process tracking. The review found 3 bugs, one high severity.

Findings

High — reasoning_effort guard is too restrictive (tools/delegate_tool.py:3156)

The _resolve_phase_reasoning_config() function uses a hardcoded whitelist ({"low", "medium", "high", "xhigh"}) to gate whether a phase's reasoning_effort is used. This excludes "minimal" and "none", both valid values accepted by parse_reasoning_effort. Users who configure these levels in a phase assignment will silently fall through to the global/parent reasoning config with a misleading warning logged.

Medium — Per-phase api_key silently dropped for named providers (tools/delegate_tool.py:3124)

When a phase assigns both a named provider (e.g., openrouter) and a custom api_key, _resolve_delegation_credentials unconditionally uses runtime.get("api_key") in the provider-resolution path, ignoring the configured_api_key extracted from the merged config. The key is silently dropped with no warning.

Low — Orphaned children on batch credential failure (tools/delegate_tool.py:2528)

Per-task credential resolution means a ValueError from a later task in the batch loop causes an immediate tool_error() return, but children built for earlier tasks remain registered in parent_agent._active_children with no cleanup. Previously credential resolution happened once before any children were built.

Risk Assessment

The api_key handling bug (finding-002) is the most concerning — it could cause delegation to silently use the wrong API key, leading to billing/rate-limit issues or, in the worst case, cross-tenant credential leakage if profiles share a provider. The reasoning_effort bug is a correctness regression for users of the new per-phase feature.

Comment thread tools/delegate_tool.py
Comment on lines +3156 to +3164
if phase_effort:
if phase_effort.lower() in {"low", "medium", "high", "xhigh"}:
parsed = parse_reasoning_effort(phase_effort)
if parsed is not None:
return parsed
logger.warning(
"Unknown delegation.phase_assignments reasoning_effort %r; falling back",
phase_effort,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Phase reasoning_effort guard excludes valid 'minimal' and 'none' levels, causing silent fallback to global/parent reasoning (bug)

In _resolve_phase_reasoning_config() at tools/delegate_tool.py:3156-3164, the pre-check phase_effort.lower() in {"low", "medium", "high", "xhigh"} gates whether a phase assignment's reasoning_effort is parsed and applied. This set excludes "minimal" (a standard effort level in hermes_constants.VALID_REASONING_EFFORTS) and "none" (accepted by parse_reasoning_effort, returning {"enabled": False}). When a user configures delegation.phase_assignments.<phase>.reasoning_effort: minimal or none, the phase-specific resolution rejects it as unknown, logs a spurious warning, and falls back to the global delegation.reasoning_effort or parent agent's reasoning config. The global reasoning path in _build_child_agent correctly calls parse_reasoning_effort directly without the restrictive guard, so this is an inconsistency introduced only in the phase-specific resolver. The per-phase routing correctly selects the phase's model/provider/base_url, but the reasoning effort silently diverges from the user's explicit configuration.

💡 Suggestion: Replace the hardcoded whitelist guard with a direct call to parse_reasoning_effort, matching the approach already used in _build_child_agent for global delegation.reasoning_effort. If parse_reasoning_effort returns non-None, use it; otherwise warn and fall through.

📋 Prompt for AI Agents

In tools/delegate_tool.py, function _resolve_phase_reasoning_config around lines 3156-3164, replace the restrictive guard if phase_effort.lower() in {"low", "medium", "high", "xhigh"}: with a direct call to parse_reasoning_effort(phase_effort). If the result is not None, return it immediately. Only log the warning when parse_reasoning_effort returns None. This removes the incomplete whitelist and delegates validation entirely to parse_reasoning_effort, which already handles all valid levels including "minimal" and "none". Add a test case for reasoning_effort: "minimal" in a phase assignment to prevent regression.

Comment thread tools/delegate_tool.py
Comment on lines +2528 to +2530
creds = _resolve_task_delegation_routing(cfg, t.get("phase"), parent_agent)
except ValueError as exc:
return tool_error(str(exc))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Orphaned children in _active_children on per-task credential failure inside batch loop (bug)

In delegate_task (tools/delegate_tool.py:2528), credential resolution now happens per-task inside the child-construction loop via _resolve_task_delegation_routing. If a ValueError is raised for task N (where N >= 1), the function returns tool_error() immediately via the try/except at lines 2528-2530. However, children 0..N-1 were already built by _build_child_agent, which unconditionally registers them to parent_agent._active_children (line 1418-1424) and fires subagent_start hooks (line 1437-1443). These children are never cleaned up — no removal from _active_children, no subagent_end hooks, and no run_conversation is ever called on them. Previously (pre-PR), credential resolution happened once before the loop, so any failure meant zero children were built. The finally block at lines 2561-2563 only restores _last_resolved_tool_names and does not address the orphaned children.

💡 Suggestion: Add a pre-validation pass that calls _resolve_task_delegation_routing for every task in task_list before building any children. If any task fails, return tool_error before constructing child agents. This restores the all-or-nothing behavior for credential failures.

📋 Prompt for AI Agents

In tools/delegate_tool.py, delegate_task function, before the child-construction loop (around line 2525), add a pre-validation pass that iterates over task_list and calls _resolve_task_delegation_routing(cfg, t.get("phase"), parent_agent) for each task. Collect any ValueError raised. If any task fails, return tool_error with the error message before building any children. This ensures that credential failures are detected atomically before any children are constructed, restoring the pre-PR all-or-nothing behavior. Example:

# Pre-validate all task credentials before building children
for t in task_list:
    try:
        _resolve_task_delegation_routing(cfg, t.get("phase"), parent_agent)
    except ValueError as exc:
        return tool_error(str(exc))

hashbender pushed a commit that referenced this pull request Jul 27, 2026
…d curator

The skill-authoring guide and curator prompt both reference
descriptions as the primary discovery mechanism but never mentioned
the 57-char system prompt truncation. Add explicit guidance:

- Authoring guide: frontmatter docs, template comment, size limits,
  pitfall #3 with good/bad examples, verification checklist
- Curator prompt: parenthetical noting the 57-char window when
  writing umbrella skill descriptions
hashbender pushed a commit that referenced this pull request Aug 4, 2026
… (re-review #3)

The last_activity_at/description/provenance columns already live in
SCHEMA_SQL and the column reconciler; existing DBs heal via the
reconciler, but the version stamp must advance so downgrade/upgrade
tooling sees the new layout. No version-literal test assertions exist
(tests compare against the imported constant).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant