From 985350dd858eef3d855b45327f187367355a0ffd Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 25 Jun 2026 19:57:58 -0500 Subject: [PATCH] feat(cli): note background delegate_task dispatch in _on_tool_complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A top-level delegate_task dispatches in the background and re-enters as a fresh turn when done. Print a one-line dispatch-time note — no spinner, nothing to poll — so the idle prompt doesn't read as "nothing happened." --- cli.py | 15 ++++ .../test_cli_delegate_background_notice.py | 69 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests/cli/test_cli_delegate_background_notice.py diff --git a/cli.py b/cli.py index 7442bfe5ca2..1a92ae93778 100644 --- a/cli.py +++ b/cli.py @@ -10499,6 +10499,21 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): def _on_tool_complete(self, tool_call_id: str, function_name: str, function_args: dict, function_result: str): """Render file edits with inline diff after write-capable tools complete.""" + # A top-level delegate_task dispatches in the background and re-enters as + # a fresh turn when done. Say so once — no spinner, nothing to poll — so + # the idle prompt doesn't read as "nothing happened" (⛓ tracks the work). + if function_name == "delegate_task": + try: + parsed = json.loads(function_result) if isinstance(function_result, str) else (function_result or {}) + except Exception: + parsed = {} + if isinstance(parsed, dict) and parsed.get("status") == "dispatched" and parsed.get("mode") == "background": + n = parsed.get("count") or 1 + noun, tail = ("task", "it finishes") if n == 1 else (f"{n} tasks", "they finish") + try: + _cprint(f"\033[2m\u21a9 Background {noun} running — I'll resume when {tail}. Keep chatting.\033[0m") + except Exception: + pass snapshot = self._pending_edit_snapshots.pop(tool_call_id, None) try: from agent.display import render_edit_diff_with_delta diff --git a/tests/cli/test_cli_delegate_background_notice.py b/tests/cli/test_cli_delegate_background_notice.py new file mode 100644 index 00000000000..23f293c1957 --- /dev/null +++ b/tests/cli/test_cli_delegate_background_notice.py @@ -0,0 +1,69 @@ +"""The CLI spells out auto-resume when a delegate_task goes to the background. + +A top-level ``delegate_task`` returns a handle immediately and runs the subagent +in the background; the result re-enters the conversation as a fresh turn when it +finishes. ``_on_tool_complete`` prints a one-line, no-spinner reassurance at +dispatch so the idle prompt doesn't read as "nothing happened". +""" + +import json + +import cli +from cli import HermesCLI + + +def _make_cli(): + cli_obj = HermesCLI.__new__(HermesCLI) + cli_obj._pending_edit_snapshots = {} + return cli_obj + + +def _capture(monkeypatch): + printed: list[str] = [] + monkeypatch.setattr(cli, "_cprint", lambda text: printed.append(text)) + return printed + + +def test_background_dispatch_prints_resume_notice(monkeypatch): + cli_obj = _make_cli() + printed = _capture(monkeypatch) + + result = json.dumps({"status": "dispatched", "mode": "background", "count": 1}) + cli_obj._on_tool_complete("tc1", "delegate_task", {"goal": "x"}, result) + + joined = "\n".join(printed) + assert "resume" in joined.lower() + assert "it finishes" in joined + + +def test_background_batch_dispatch_pluralizes(monkeypatch): + cli_obj = _make_cli() + printed = _capture(monkeypatch) + + result = json.dumps({"status": "dispatched", "mode": "background", "count": 3}) + cli_obj._on_tool_complete("tc2", "delegate_task", {"tasks": []}, result) + + joined = "\n".join(printed) + assert "3 tasks" in joined + assert "they finish" in joined + + +def test_synchronous_delegate_result_prints_no_notice(monkeypatch): + """A non-background result (e.g. the stateless sync fallback) must not claim + a background dispatch.""" + cli_obj = _make_cli() + printed = _capture(monkeypatch) + + result = json.dumps({"results": [{"status": "completed", "summary": "done"}]}) + cli_obj._on_tool_complete("tc3", "delegate_task", {"goal": "x"}, result) + + assert not any("resume" in p.lower() for p in printed) + + +def test_non_delegate_tool_prints_no_notice(monkeypatch): + cli_obj = _make_cli() + printed = _capture(monkeypatch) + + cli_obj._on_tool_complete("tc4", "read_file", {"path": "a"}, '{"ok": true}') + + assert not any("resume" in p.lower() for p in printed)