Skip to content

feat: non-blocking background agent delegation (async_delegation toolset) - #8482

Closed
iRonin wants to merge 2 commits into
NousResearch:mainfrom
iRonin:ironin/async-delegation
Closed

feat: non-blocking background agent delegation (async_delegation toolset)#8482
iRonin wants to merge 2 commits into
NousResearch:mainfrom
iRonin:ironin/async-delegation

Conversation

@iRonin

@iRonin iRonin commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Adds non-blocking background agent execution as agent-callable tools. Unlike delegate_task which blocks the parent until all children complete, this lets the parent agent spawn long-running background agents and continue working immediately.\n\n### New tools (async_delegation toolset)\n\n| Tool | Description |\n|---|---|\n| delegate_task_async(goal, ...) | Spawn a background agent, return task_id immediately |\n| check_task(task_id) | Non-blocking: status + last 10 lines of output |\n| collect_task(task_id, timeout) | Block until done, return full result |\n| steer_task(task_id, message) | Inject steering mid-run |\n| cancel_task(task_id) | Stop a running background agent |\n| list_tasks() | All async tasks for this session |\n\n### Tests\n68 unit tests covering all 6 tools and the AsyncTask dataclass.\n\nCloses #5586

@easyvibecoding

Copy link
Copy Markdown
Contributor

Related: #9556 proposes a synchronous delegate_task_stream primitive for in-flight observability. The async tools in this PR and the streaming primitive are complementary — the same underlying child-process status channel serves both ("is the child done?" for async vs "what is the child saying right now?" for streaming).

@gthieleb

Copy link
Copy Markdown

PR #8482 Review: Non-blocking Background Agent Delegation (async_delegation)

Reviewer: Gunnar (via Hermes Agent)
Date: 2026-04-25
Branch: ironin/async-delegation
Base: main (at commit 4eecaf06, 1,892 commits behind current main)


Summary

This PR introduces 6 new tools under the async_delegation toolset for non-blocking background agent execution:

Tool Purpose
delegate_task_async Spawn a background agent, return task_id immediately
check_task Non-blocking status + output preview (last 10 lines)
collect_task Block until completion, return full result
steer_task Inject a steering message into a running task
cancel_task Stop a running task via child_agent.interrupt()
list_tasks List all async tasks for the current session

New file: tools/async_delegate_tool.py (681 lines)
Modified: toolsets.py (adds async_delegation toolset definition)
Tests: tests/tools/test_async_delegate.py (92 tests, all passing)


Bugs Found & Fixed

🔴 Critical: Missing _load_skill_for_subagent function

The PR imports _load_skill_for_subagent from tools.delegate_tool (line 92 of async_delegate_tool.py), but this function did not exist in delegate_tool.py. This caused an ImportError at module load time, making the entire async_delegation toolset unusable.

Fix: Implemented _load_skill_for_subagent() in tools/delegate_tool.py (25 lines). It uses the existing skill_view() from tools.skills_tool to load skill content and extract model/provider overrides.

🟡 Design Issue: MAX_DEPTH divergence

The PR uses MAX_DEPTH = 2 (from delegate_tool.py), but current main has changed this to MAX_DEPTH = 1 with a configurable _get_max_spawn_depth() function. This needs reconciliation during merge.

🟡 No task cleanup / memory leak

Completed, failed, and cancelled tasks remain in the parent's _async_tasks dict indefinitely. For long-running sessions with many spawned tasks, this is a memory leak. Consider adding a cleanup mechanism (e.g., auto-prune tasks older than N minutes).


Test Coverage

✅ Well Covered (68 original + 24 new = 92 total)

Category Tests Status
AsyncTask dataclass 7 ✅ elapsed, status, done_event
_get_task_registry 4 ✅ creation, idempotency, preservation
delegate_task_async 14 ✅ validation, spawning, errors, completion
check_task 9 ✅ status, preview, unknown task
collect_task 10 ✅ blocking, timeout, already completed
steer_task 10 ✅ queue injection, status checks
cancel_task 11 ✅ interrupt, status, cleanup
list_tasks 7 ✅ empty, multiple, truncation
Output truncation 4 ✅ _MAX_OUTPUT_LINES=200, _MAX_LINE_CHARS=500
Concurrency 3 ✅ registry race, concurrent steer+cancel, multiple tasks
Skill loading 6 ✅ valid/invalid/empty skill, exception handling
Thread lifecycle 4 ✅ thread cleanup, _active_children removal
Multiple tasks 3 ✅ spawn 3, collect individually, cancel one
Registry persistence 4 ✅ memory leak documentation

⚠️ Still Missing

  • Integration test with real AIAgent (requires API key)
  • Merge conflict resolution with main's delegate_tool.py changes
  • ACP transport compatibility (main added acp_command/acp_args to delegate_task)
  • Subagent approval callbacks (main added _subagent_auto_deny/_subagent_auto_approve)

Compatibility with Main (1,892 commits behind)

Major Changes in delegate_tool.py on main:

  1. File grew from 1,103 → 2,458 lines (+1,355 lines)
  2. MAX_DEPTH changed from 2 → 1 with new _get_max_spawn_depth() function
  3. Subagent approval callbacks (_subagent_auto_deny, _subagent_auto_approve) — prevents TUI deadlocks
  4. Active subagent registry (_active_subagents dict, interrupt_subagent(), list_active_subagents())
  5. File state coordination (new tools/file_state.py module)
  6. Delegate events (DelegateEvent enum for progress callbacks)
  7. _extract_output_tail() for error detection in output
  8. _get_child_timeout() bumped default to 600s
  9. _get_orchestrator_enabled() for nested delegation
  10. MCP toolset inheritance (_get_inherit_mcp_toolsets())

Merge Strategy

❌ Direct merge is NOT feasiblemodel_tools.py has a content conflict in the _discover_tools() module list, and delegate_tool.py has diverged too significantly.

✅ Recommended: Rebase onto current main

  1. Rebase ironin/async-delegation onto origin/main
  2. Resolve the single conflict in model_tools.py (add tools.async_delegate_tool to the import list)
  3. Update async_delegate_tool.py to use main's new APIs:
    • Use _get_max_spawn_depth() instead of hardcoded MAX_DEPTH
    • Wire subagent approval callbacks into spawned threads
    • Register with _active_subagents for TUI observability
    • Use _get_child_timeout() for timeout defaults
  4. Consider extracting async delegation into its own module that imports from the refactored delegate_tool.py rather than reusing internal functions

Recommendation

🔄 Request Changes — The PR needs a rebase onto current main before it can be merged. The core design is sound and the test coverage is excellent (92 tests), but the 1,892-commit divergence means significant integration work is needed to align with main's delegation infrastructure.

Action Items for Author:

  1. Rebase onto origin/main
  2. Adopt _get_max_spawn_depth() / _get_child_timeout() / approval callbacks
  3. Register async tasks with _active_subagents for TUI observability
  4. Add task cleanup mechanism (auto-prune old tasks)
  5. Consider the relationship with [Feature]: delegate_task_stream with mid-flight interrupt for synchronous delegation #9556 (streaming delegate primitive) — shared status channel

Files to Include in Merge:

tools/async_delegate_tool.py    (new, 681 lines)
tools/delegate_tool.py          (modified, +25 lines for _load_skill_for_subagent)
tests/tools/test_async_delegate.py  (new, ~1420 lines, 92 tests)
toolsets.py                     (modified, adds async_delegation toolset)
model_tools.py                  (modified, adds async_delegate_tool to discovery)

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets tool/delegate Subagent delegation labels Apr 25, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #5586 (the tracking issue this closes) and prior attempts #5587, #6813 (both closed). Also related to #7701 (another open attempt at non-blocking delegation).

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the substantial async-delegation implementation. This is now redundant with the integrated background lifecycle on current main.

  • run_agent.py:5694 forces top-level delegate_task calls into background execution and documents immediate handle return plus result reinjection.
  • tools/delegate_tool.py:2766 dispatches through the shared async registry; tools/delegate_tool.py:2880 returns status: dispatched, mode: background, and a delegation_id without blocking.
  • tools/async_delegation.py:211 runs detached work on the daemon executor, and tools/async_delegation.py:288 sends its completion through the shared queue.
  • tests/tools/test_async_delegation.py:230 verifies that the call returns while the child is still gated, proving the non-blocking contract.
  • The integrated implementation landed in c66ecf0bc30f333eac25113b38eca6b5197e7518 and shipped in v2026.6.19.

This is an automated hermes-sweeper review.

@teknium1 teknium1 closed this Jul 12, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have sweeper:implemented-on-main Sweeper: behavior already present on current main tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants