Skip to content

feat: non-blocking background delegation for delegate_task - #6813

Closed
murapadev wants to merge 2 commits into
NousResearch:mainfrom
murapadev:feat/non-blocking-delegate
Closed

feat: non-blocking background delegation for delegate_task#6813
murapadev wants to merge 2 commits into
NousResearch:mainfrom
murapadev:feat/non-blocking-delegate

Conversation

@murapadev

Copy link
Copy Markdown

Summary

Adds background=True and session_id parameters to delegate_task tool for non-blocking subagent execution.

What

  • background=True: launches subagent in a daemon thread, returns immediately with a session_id (UUID prefix)
  • session_id: retrieve results from a previous background delegation (pass session_id alone, no goal/tasks needed)
  • Results saved to ~/.hermes/delegation_results/<session_id>.json

Why

Currently delegate_task is fully blocking — the parent agent cannot respond until the subagent completes. This is problematic for messaging platforms (Telegram, Discord) where blocking the main agent makes it unresponsive for minutes.

How it works

delegate_task refactored into:

  1. delegate_task() — main entry, validates, builds children, decides sync or async
  2. _run_delegation_background() — new function executed in daemon thread, saves result to file on completion
  3. Background retrieval via session_id parameter (no goal/tasks = read mode)

~/.hermes/delegation_results/ is created automatically.

Backward compatibility

  • background defaults to False — existing calls behave identically
  • session_id only used when passed without goal/tasks
  • No breaking changes to existing behavior

Example

# Launch without blocking
delegate_task(goal="Research X", toolsets=["web"], background=True)
# → {background: true, session_id: "a1b2c3d4"}

# Later, retrieve results
delegate_task(session_id="a1b2c3d4")
# → {results: [...], total_duration_seconds: 142.5}

- Add background=True param to delegate_task: launches subagent in a daemon
  thread and returns immediately with a session_id (UUID prefix)
- Add session_id param: retrieve results from a previous background
  delegation by passing the session_id (no goal/tasks needed)
- Results stored in ~/.hermes/delegation_results/<session_id>.json
- Background thread saves result atomically on completion
- Thread-safe: _last_resolved_tool_names restored in finally block
- Schema updated with NON-BLOCKING MODE section and both new params

This enables parent agent to stay responsive while subagents run,
supporting real concurrent workflows (e.g. research + coding in parallel
while continuing to chat with user).
Copilot AI review requested due to automatic review settings April 9, 2026 21:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a non-blocking/asynchronous execution mode to the delegate_task tool, enabling the parent agent to remain responsive while delegated subagent work runs in the background and results can be retrieved later via a session_id.

Changes:

  • Added background and session_id parameters to delegate_task to support async execution and later result retrieval.
  • Implemented persistence of background results under ~/.hermes/delegation_results/<session_id>.json.
  • Added a background runner function and updated tool schema/docs to describe the new behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/delegate_tool.py Outdated
Comment on lines +624 to +761
"background": True,
"session_id": bg_session_id,
"message": "Delegation started in background. Use session_id to retrieve results.",
})

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

background=True currently does not make delegation non-blocking: the function runs all child tasks (building children, executing _run_single_child, and populating results) before reaching the if background: branch. This makes the call blocking and then starts a second delegation run in the background thread (double work / inconsistent side effects). Move the background branching earlier (before executing children), and have the background thread perform the delegation work end-to-end (including child creation) while the foreground returns the session_id immediately.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 64c206e. The branch now executes BEFORE any child construction or execution. The thread receives raw input parameters (goal, context, toolsets, tasks, max_iterations, acp_command, acp_args, parent_agent) and performs ALL work (validation, credential resolution, child building, execution, result saving) internally. The main thread returns session_id immediately with no delegation work done upfront.

Comment thread tools/delegate_tool.py
Comment on lines +61 to +70
def _load_background_result(session_id: str) -> Optional[dict]:
"""Load a previously saved delegation result."""
results_dir = _get_background_results_dir()
result_file = os.path.join(results_dir, f"{session_id}.json")
if not os.path.exists(result_file):
return None
try:
with open(result_file, encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

session_id is interpolated directly into a filename. Because session_id comes from user input in retrieval mode, a value containing path separators (e.g. ../...) can escape delegation_results and read arbitrary JSON files the process can access. Validate/sanitize session_id (e.g., allow only [a-f0-9]{8} or a full UUID) and reject anything else before building the path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 64c206e. Added session_id format validation using re.fullmatch(r'[a-f0-9]{8}', session_id) at the start of both _save_background_result and _load_background_result. Invalid formats raise ValueError immediately. Path traversal is blocked.

Comment thread tools/delegate_tool.py
Comment on lines +55 to +59
results_dir = _get_background_results_dir()
result_file = os.path.join(results_dir, f"{session_id}.json")
with open(result_file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Saving background results is not atomic: writing JSON directly to the final path means a concurrent reader can observe a partially-written file and _load_background_result will return None (JSONDecodeError), indistinguishable from “still running”. Consider writing to a temporary file in the same directory and os.replace to the final name, so reads are always consistent.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 64c206e. Replaced direct file write with tempfile.mkstemp() + os.fdopen() + os.replace() for atomic writes. Reader will always see either the old complete file or the new complete file, never a partial write.

Comment thread tools/delegate_tool.py Outdated
Comment on lines +780 to +783
# Re-import for thread safety
import model_tools as _model_tools
_parent_tool_names = list(_model_tools._last_resolved_tool_names)

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

_run_delegation_background mutates and restores the process-global model_tools._last_resolved_tool_names. In a multi-threaded scenario (the entire point of background mode), this shared global can be concurrently modified by other agent sessions, so restoring it here can corrupt tool resolution for other threads. Consider protecting mutations with a lock, or refactoring so background delegation doesn’t rely on the process-global (e.g., pass enabled_tools explicitly to the places that need it).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 64c206e. Removed the finally block that restored _last_resolved_tool_names. The background thread saves the parent's tool names at thread start for its own use during child construction, but does not restore anything — avoiding any race condition with the main thread or other concurrent threads.

Comment thread tools/delegate_tool.py Outdated
Comment on lines +766 to +778
def _run_delegation_background(
session_id: str,
task_list: list,
children: list,
n_tasks: int,
task_labels: list,
parent_agent,
effective_max_iter: int,
creds: dict,
acp_command: Optional[str],
acp_args: Optional[List[str]],
toolsets: Optional[List[str]],
) -> None:

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

_run_delegation_background takes effective_max_iter, creds, acp_command, acp_args, and toolsets but does not use them. This suggests the refactor is incomplete and makes the API harder to understand/maintain. Either remove these parameters or use them by moving child construction/config resolution into the background thread (which also aligns with true non-blocking behavior).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 64c206e. Removed effective_max_iter, creds, acp_command, acp_args, and toolsets from _run_delegation_background signature. The function now receives raw inputs and performs its own credential resolution and config loading internally — consistent with true non-blocking behavior where the thread does all work end-to-end.

Comment thread tools/delegate_tool.py
Comment on lines 541 to 552
def delegate_task(
goal: Optional[str] = None,
context: Optional[str] = None,
toolsets: Optional[List[str]] = None,
tasks: Optional[List[Dict[str, Any]]] = None,
max_iterations: Optional[int] = None,
acp_command: Optional[str] = None,
acp_args: Optional[List[str]] = None,
background: bool = False,
session_id: Optional[str] = None,
parent_agent=None,
) -> str:

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The runtime call sites that invoke delegate_task also need to pass through the new background and session_id parameters; otherwise the feature is effectively unreachable in normal tool execution. For example, run_agent.py currently calls delegate_task(..., max_iterations=..., parent_agent=self) without these new args (verified via repo search). Update the dispatcher/call sites accordingly so the tool arguments actually take effect.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already handled. The tool schema includes background and session_id parameters, and the registry handler already passes them to delegate_task: background=args.get("background", False), session_id=args.get("session_id"). The parameters flow through the tool call mechanism correctly.

Comment thread tools/delegate_tool.py
Comment on lines +573 to +581
# Handle result retrieval mode (non-blocking)
if session_id and not goal and not tasks:
result = _load_background_result(session_id)
if result is None:
return json.dumps({
"error": f"No delegation result found for session_id '{session_id}'. "
"The delegation may still be running or the result was discarded."
})
return json.dumps(result)

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

New behavior for background execution/result retrieval (background / session_id) is introduced here, but there are existing unit tests for delegate_task (see tests/tools/test_delegate.py) and none cover the async path. Add tests that assert: (1) background=True returns quickly with a session_id without running _run_single_child synchronously, and (2) delegate_task(session_id=...) returns saved results / a clear “still running” response.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 64c206e. Added 4 new tests in tests/tools/test_delegate.py: (1) test_background_returns_immediately verifies background=True returns within 2s even with a 5s simulated task, (2) test_session_id_retrieval saves a result file and retrieves it via session_id, (3) test_invalid_session_id_raises asserts ValueError for path traversal attempts, (4) test_background_branch_prevents_child_construction_in_main_thread verifies AIAgent is never called in the main thread when background=True.

- Restructure: branch on background=True BEFORE any child construction
- Add session_id format validation (8 hex chars only, path traversal fix)
- Atomic writes via temp file + os.replace()
- Remove unnecessary global restore in background thread
- Remove unused parameters from _run_delegation_background
- Add tests for background=True, session_id retrieval, invalid session_id
@teknium1

Copy link
Copy Markdown
Contributor

Closing — valuable concept (non-blocking delegation for messaging platforms), but the implementation needs fundamental rework: ~130 lines of copy-pasted delegation pipeline creates a maintenance burden, no graceful shutdown handling, no result file cleanup, and thread safety concerns with parent agent attribute access. Consider refactoring to run the existing synchronous delegate_task() in a thread rather than duplicating the pipeline. Happy to review a v2.

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.

3 participants