Files
hermes-agent/evals/gateway_status_render/iteration_ceiling_ab.py
T
kshitijk4poor e9bccc90a5 refactor: repoint 10 MessageEvent/MessageType importers that landed on main after the event.py split
Five test files and five evals scripts merged since this branch's base still import the
moved names via gateway.platforms.base. They work (base.py imports the names for its own
use) but the PR's invariant is that in-tree code imports from the defining module.
2026-09-07 22:47:33 +05:30

117 lines
5.0 KiB
Python

"""A/B probe: what the three user-facing gateway status lines render for the iteration counter.
Drives the REAL render paths (busy-ack, long-running heartbeat, inactivity-timeout diagnostic)
with a REAL ``AIAgent`` constructed with its default (unlimited) ``max_iterations`` and a
stub adapter that records the outbound text. Run on origin/main and on the fix branch:
HERMES_HOME=$(mktemp -d) python evals/gateway_status_render/iteration_ceiling_ab.py [--finite 250]
Prints one JSON object per render site with the exact text a user would see.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
def _agent(max_iterations: int | None):
from run_agent import AIAgent
kwargs: dict[str, Any] = dict(base_url="http://127.0.0.1:9/v1", api_key="sk-dummy", model="dummy-model",
quiet_mode=True, enabled_toolsets=[], disabled_toolsets=["*"])
if max_iterations is not None:
kwargs["max_iterations"] = max_iterations
agent = AIAgent(**kwargs)
agent._api_call_count = 3
agent._current_tool = "terminal"
agent._last_activity_desc = "terminal"
agent._last_activity_ts = time.time() - 42
return agent
async def _busy_ack(agent) -> str:
import gateway.run as gr
from gateway.platforms.base import SessionSource, build_session_key
from gateway.platforms.event import MessageEvent, MessageType
gr._load_gateway_config = lambda: {"display": {"platforms": {"telegram": {"busy_ack_detail": True}}}}
runner = object.__new__(gr.GatewayRunner)
runner._running_agents, runner._running_agents_ts = {}, {}
runner._pending_messages, runner._busy_ack_ts, runner._queued_events = {}, {}, {}
runner._draining, runner._busy_text_mode, runner._busy_input_mode = False, "interrupt", "interrupt"
runner.adapters, runner.config, runner.session_store = {}, MagicMock(), None
runner.config.group_sessions_per_user, runner.config.thread_sessions_per_user = True, False
runner.hooks = MagicMock(); runner.hooks.emit = AsyncMock()
runner.pairing_store = MagicMock(); runner.pairing_store.is_approved.return_value = True
runner._is_user_authorized = lambda _s: True
source = SessionSource(platform=MagicMock(value="telegram"), chat_id="123", chat_type="private", user_id="u1")
event = MessageEvent(text="status?", message_type=MessageType.TEXT, source=source, message_id="m1")
sk = build_session_key(source)
adapter = MagicMock()
adapter._pending_messages, adapter._text_debounce, adapter._busy_text_debounce_seconds = {}, {}, 0.6
adapter._send_with_retry = AsyncMock()
adapter.config = MagicMock(); adapter.config.extra = {}
adapter.platform = MagicMock(value="telegram")
runner._running_agents[sk] = agent
runner._running_agents_ts[sk] = time.time() - 600
runner.adapters[source.platform] = adapter
await runner._handle_active_session_busy_message(event, sk)
return adapter._send_with_retry.call_args.kwargs.get("content", "")
async def _heartbeat(agent) -> str:
from gateway.run_turn import GatewayTurnMixin
from gateway.turn_context import TurnContext
os.environ["HERMES_AGENT_NOTIFY_INTERVAL"] = "0.01"
mixin = GatewayTurnMixin()
adapter = MagicMock()
adapter.send = AsyncMock(return_value=SimpleNamespace(success=True, message_id="hb1"))
mixin._adapter_for_source = MagicMock(return_value=adapter)
mixin._should_emit_long_running_notification = MagicMock(side_effect=[True, False])
disp = MagicMock()
disp._display_surface_mode.return_value = "on"
disp.resolve_display_setting.return_value = True
ctx = TurnContext(source=SimpleNamespace(chat_id="c1", platform="telegram"), session_key="s1", agent_holder=[agent])
await mixin._run_agent_notify_long_running(disp, ctx, [None])
return adapter.send.await_args.args[1]
def _timeout(agent) -> str:
import gateway.run as gr
from gateway.run_turn import GatewayTurnMixin
from gateway.turn_context import TurnContext
gr.request_hard_interrupt = MagicMock()
ctx = TurnContext(session_key="s1", agent_holder=[agent])
return GatewayTurnMixin()._run_agent_timeout_result(SimpleNamespace(agent_timeout=1800.0), ctx)["final_response"]
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--finite", type=int, default=None, help="use a finite max_iterations instead of the default")
args = ap.parse_args()
sys.path.insert(0, os.getcwd())
agent = _agent(args.finite)
out = {
"head": os.popen("git rev-parse --short HEAD").read().strip(),
"max_iterations": agent.max_iterations,
"busy_ack": asyncio.run(_busy_ack(agent)),
"heartbeat": asyncio.run(_heartbeat(agent)),
"timeout_diag": _timeout(agent),
}
out["sentinel_leaks"] = sum(str(sys.maxsize) in v for k, v in out.items() if k in ("busy_ack", "heartbeat", "timeout_diag"))
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()