Files
hermes-agent/tests/agent/test_background_review_tool_call_guard.py
Alexander Prendota 37fd61d13b fix(background-review): skip the fork when the provider can't emit tool calls
The review fork's entire job is to emit `memory` / `skill_manage` tool calls,
and by default it inherits the parent's live runtime. A provider that IS an
autonomous agent reaches Hermes through a client shim; if that shim cannot
carry Hermes tool calls back, the fork is a guaranteed no-op that still pays
for a full agent spawn — a whole CLI process, sometimes a JVM — on every
review cadence.

A client declares `SUPPORTS_HERMES_TOOL_CALLS = False` and the fork is
skipped with a warning naming the `auxiliary.background_review.{provider,model}`
override that routes the review to a normal model instead. Anything that says
nothing is assumed capable, so ordinary providers are untouched.

The check runs before the thread-scoped silence so the warning is not
swallowed, and only resolves the review runtime once the cheap capability
test has already failed, so the normal path does not resolve it twice.
2026-08-26 10:10:11 -07:00

126 lines
4.2 KiB
Python

"""The background-review fork must not spawn when it could only no-op.
The review fork's whole job is to emit ``memory`` / ``skill_manage`` tool calls,
and by default it inherits the parent's live runtime. When the parent provider IS
an autonomous agent reached through a client shim that cannot carry Hermes tool
calls back, that fork is a guaranteed no-op — one that still pays for a full
agent spawn (a whole CLI process, sometimes a JVM) on every review cadence.
So: a client declaring ``SUPPORTS_HERMES_TOOL_CALLS = False`` skips the fork with
a log line pointing at the ``auxiliary.background_review`` override. A client
that can emit tool calls is unaffected, as are ordinary providers whose clients
say nothing at all.
"""
from __future__ import annotations
import logging
import os
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
import agent.background_review as bg # noqa: E402
def _fake_parent(client, *, runtime=None) -> SimpleNamespace:
"""The minimal parent-agent surface _run_review_in_thread touches pre-fork."""
return SimpleNamespace(
provider="acp-agent",
model="acp-agent",
client=client,
session_id="s1",
platform="cli",
request_overrides={},
max_tokens=None,
acp_command="acp-agent",
acp_args=["--acp"],
enabled_toolsets=None,
disabled_toolsets=None,
reasoning_config=None,
_credential_pool=None,
_current_main_runtime=lambda: runtime or {
"api_key": "k",
"base_url": "acp://agent",
"api_mode": "chat_completions",
},
_emit_auxiliary_failure=lambda *_a, **_k: None,
_safe_print=lambda *_a, **_k: None,
background_review_callback=None,
)
def _run(agent, task_cfg=None):
"""Run the worker with AIAgent patched; return the AIAgent mock."""
with (
patch("hermes_cli.config.load_config", return_value={}),
patch("run_agent.AIAgent") as mock_aiagent,
patch("tools.terminal_tool.set_approval_callback"),
):
bg._run_review_in_thread(
agent, [{"role": "user", "content": "hi"}], "review please", task_cfg
)
return mock_aiagent
def test_fork_is_skipped_when_the_provider_cannot_emit_tool_calls(caplog):
client = MagicMock()
client.SUPPORTS_HERMES_TOOL_CALLS = False
with caplog.at_level(logging.WARNING, logger=bg.logger.name):
mock_aiagent = _run(_fake_parent(client))
mock_aiagent.assert_not_called()
# The user needs to know which knob makes the review work again.
assert "auxiliary.background_review" in caplog.text
def test_fork_is_spawned_when_the_provider_can_emit_tool_calls():
client = MagicMock()
client.SUPPORTS_HERMES_TOOL_CALLS = True
assert _run(_fake_parent(client)).called
def test_ordinary_providers_are_unaffected():
# A plain OpenAI-style client says nothing about the capability.
class _PlainClient:
pass
assert _run(_fake_parent(_PlainClient())).called
def test_the_capability_is_read_off_the_class_too():
"""Clients declare it as a class attribute; an instance need not set it."""
class _IncapableClient:
SUPPORTS_HERMES_TOOL_CALLS = False
assert bg._parent_can_emit_tool_calls(_fake_parent(_IncapableClient())) is False
assert bg._parent_can_emit_tool_calls(_fake_parent(None)) is True
def test_an_incapable_provider_still_reviews_when_the_review_is_routed_away():
"""``auxiliary.background_review.{provider,model}`` sends the fork to a normal
model, so the parent's shim no longer matters."""
class _IncapableClient:
SUPPORTS_HERMES_TOOL_CALLS = False
routed = {
"provider": "openai",
"model": "gpt-5",
"api_key": "k",
"base_url": None,
"api_mode": "chat_completions",
"credential_pool": None,
"request_overrides": {},
"max_tokens": None,
"command": None,
"args": [],
"routed": True,
}
with patch.object(bg, "_resolve_review_runtime", return_value=routed):
assert _run(_fake_parent(_IncapableClient())).called