mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(delegate): route subagent progress lines through _safe_print for ACP stdio
delegate_task's per-task completion display emitted lines like "✓ [1/3] Research done (17.92s)" via a bare print(). Under ACP (and any headless JSON-RPC stdio host where AIAgent routes human output to stderr via a custom _print_fn), these landed on stdout and corrupted the protocol frame stream, surfacing as "Failed to parse JSON message: ✓ [3/3] …" in the ACP adapter. Add _emit_parent_console() which prefers parent_agent._safe_print (the same hook AIAgent uses for every other user-facing print) and falls back to print() only when no router is wired up or it raises. CLI behavior is unchanged. The PR's other fix (preset toolset expansion) is already covered on main by _expand_parent_toolsets(), so only the stdio-safe printing change is salvaged here.
This commit is contained in:
parent
eeb4735078
commit
cd9f5cc671
3 changed files with 63 additions and 3 deletions
|
|
@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
|
|||
|
||||
# Auto-extracted from noreply emails + manual overrides
|
||||
AUTHOR_MAP = {
|
||||
"phanvanhoa@gmail.com": "theAgenticBuilder", # PR #14180 salvage (route delegate_task progress lines through _safe_print so ACP stdio JSON-RPC frames stay clean)
|
||||
"cypher@augmentl.com": "Nickperillo", # PR #8008 salvage (Discord channel-name matching + flush pending sends on shutdown)
|
||||
"tenoryang@outlook.com": "MarioYounger", # PR #9028 salvage (bash/sh heredoc approval, NFKC homograph fold, execute_code CREDS/BEARER/APIKEY env filter)
|
||||
"peet.wannasarnmetha@gmail.com": "peetwan", # PR #51841 salvage (loopback ws-ping tuning + token-frame coalescing + loop heartbeat; #48445/#50005)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ arbitrary toolsets.
|
|||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from tools.delegate_tool import _strip_blocked_tools
|
||||
from tools.delegate_tool import _strip_blocked_tools, _emit_parent_console
|
||||
|
||||
|
||||
class TestToolsetIntersection:
|
||||
|
|
@ -63,3 +63,44 @@ class TestToolsetIntersection:
|
|||
scoped = [t for t in requested if t in parent_toolsets]
|
||||
|
||||
assert scoped == []
|
||||
|
||||
|
||||
class TestEmitParentConsole:
|
||||
"""Progress lines (e.g. ``✓ [N/M] …``) must route through the parent's
|
||||
configured ``_safe_print`` in headless stdio hosts (ACP, gateway) so
|
||||
they don't land on stdout and corrupt JSON-RPC frames. Regression for a
|
||||
bug where delegate_task completion lines pushed to stdout caused
|
||||
``Failed to parse JSON message: ✓ [3/3] …`` errors in the ACP adapter."""
|
||||
|
||||
def test_routes_through_parent_safe_print_when_available(self, capsys):
|
||||
captured_lines = []
|
||||
parent = SimpleNamespace(_safe_print=lambda line: captured_lines.append(line))
|
||||
|
||||
_emit_parent_console(parent, " ✓ [1/3] Research done (11.55s)")
|
||||
|
||||
assert captured_lines == [" ✓ [1/3] Research done (11.55s)"]
|
||||
stdout_stderr = capsys.readouterr()
|
||||
assert stdout_stderr.out == ""
|
||||
assert stdout_stderr.err == ""
|
||||
|
||||
def test_falls_back_to_stdout_when_no_safe_print(self, capsys):
|
||||
parent = SimpleNamespace()
|
||||
_emit_parent_console(parent, " ✓ [1/3] fallback path")
|
||||
captured = capsys.readouterr()
|
||||
assert "fallback path" in captured.out
|
||||
|
||||
def test_falls_back_to_stdout_when_safe_print_raises(self, capsys):
|
||||
def raiser(_line):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
parent = SimpleNamespace(_safe_print=raiser)
|
||||
_emit_parent_console(parent, " ✓ [2/3] fallback on exception")
|
||||
captured = capsys.readouterr()
|
||||
assert "fallback on exception" in captured.out
|
||||
|
||||
def test_non_callable_safe_print_is_ignored(self, capsys):
|
||||
"""Defensive: if _safe_print is set but not callable, fall back."""
|
||||
parent = SimpleNamespace(_safe_print="not-a-function")
|
||||
_emit_parent_console(parent, " ✓ [3/3] non-callable guard")
|
||||
captured = capsys.readouterr()
|
||||
assert "non-callable guard" in captured.out
|
||||
|
|
|
|||
|
|
@ -800,6 +800,24 @@ def _strip_blocked_tools(toolsets: List[str]) -> List[str]:
|
|||
return [t for t in toolsets if t not in blocked_toolset_names]
|
||||
|
||||
|
||||
def _emit_parent_console(parent_agent, line: str) -> None:
|
||||
"""Emit a human-readable progress line to the parent's console.
|
||||
|
||||
Routes through ``parent_agent._safe_print`` when available so headless
|
||||
stdio hosts (ACP, gateway API) can redirect non-protocol output to
|
||||
stderr via their configured ``_print_fn``. A bare ``print()`` would
|
||||
otherwise land on stdout and corrupt JSON-RPC framing.
|
||||
"""
|
||||
printer = getattr(parent_agent, "_safe_print", None)
|
||||
if callable(printer):
|
||||
try:
|
||||
printer(line)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
print(line)
|
||||
|
||||
|
||||
def _build_child_progress_callback(
|
||||
task_index: int,
|
||||
goal: str,
|
||||
|
|
@ -2610,9 +2628,9 @@ def delegate_task(
|
|||
try:
|
||||
spinner_ref.print_above(completion_line)
|
||||
except Exception:
|
||||
print(f" {completion_line}")
|
||||
_emit_parent_console(parent_agent, f" {completion_line}")
|
||||
else:
|
||||
print(f" {completion_line}")
|
||||
_emit_parent_console(parent_agent, f" {completion_line}")
|
||||
|
||||
# Update spinner text to show remaining count
|
||||
if spinner_ref and remaining > 0:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue