Files
hermes-agent/tests/agent/test_iteration_budget_warning.py
Teknium a7198a8855 fix: keep budget checkpoints out of cancelled tool results
Skip checkpoint evaluation when a turn is interrupted so cancellation rows
remain durable without urging continued execution. The existing minimal-agent
interrupt regression also avoids dereferencing an absent iteration budget.

Consolidate the warning coverage into two invariants, including real SQLite
readback and dispatcher/child scope controls. Cold-start tool availability
between construction cases to model independent worker processes. Place ratio
normalization beside the existing iteration budget instead of growing init.

Real cancelled-tool A/B against current main, draft, and fix: three cancelled
rows and zero writes on all arms; persisted checkpoint notices 0 / 1 / 0.
Repeated scripted HTTP/SQLite loop A/B preserves completion opportunity,
ordinary default-off behavior, and blocked/two-failure exhaustion behavior.

Local targeted run initially passed 15 cases with one fixture cache-isolation
failure; corrected target and inherited affected suites remain queued behind
the campaign lock. This commit is not a CI-green or merge-ready claim.
2026-09-07 08:28:43 -07:00

61 lines
2.9 KiB
Python

"""Iteration checkpoints preserve the transcript and the hard budget."""
from copy import deepcopy
import pytest
def _agent(tmp_path, monkeypatch, ratio):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text(
f"agent:\n budget_warning_ratio: {ratio}\n", encoding="utf-8"
)
from run_agent import AIAgent
from hermes_state import SessionDB
from model_tools import _clear_tool_defs_cache
from tools.registry import invalidate_check_fn_cache
# Cases model separate worker processes; their availability caches must not
# survive a change from ordinary to dispatcher-owned construction.
invalidate_check_fn_cache()
_clear_tool_defs_cache()
return AIAgent(session_db=SessionDB(db_path=tmp_path / "proof.db"),
model="test-model", provider="openai-compat", api_key="test",
base_url="http://127.0.0.1:1/v1", max_iterations=4,
quiet_mode=True, skip_context_files=True, skip_memory=True)
@pytest.mark.parametrize("content", ["result", [{"type": "text", "text": "result"}]])
@pytest.mark.parametrize("interrupted", [False, True])
def test_checkpoint_rearms_per_turn_without_changing_budget_or_durable_rows(
tmp_path, monkeypatch, content, interrupted
):
from agent.turn_context import _reset_per_turn_agent_state
from agent.turn_iteration_prep import prepare_iteration
from agent.tool_executor import _flush_session_db_after_tool_progress
agent = _agent(tmp_path, monkeypatch, "0.75")
try:
for turn in range(2):
_reset_per_turn_agent_state(agent)
agent._interrupt_requested = interrupted
for _ in range(3):
agent.iteration_budget.consume()
messages = [{"role": "user", "content": f"work {turn}"},
{"role": "assistant", "tool_calls": [{"id": f"t{turn}", "type": "function", "function": {"name": "read_file", "arguments": "{}"}}]},
{"role": "tool", "tool_call_id": f"t{turn}", "content": deepcopy(content)}]
assert _flush_session_db_after_tool_progress(agent, messages, stage="checkpoint")
persisted = agent._session_db.get_messages(agent.session_id)
notices = sum("iteration budget checkpoint" in str(row["content"]) for row in persisted)
assert notices == (0 if interrupted else turn + 1)
assert ("3 of 4" in str(messages[-1]["content"])) is not interrupted
snapshot = deepcopy(messages)
# A durable result is never rewritten, even after interruption clears
# and a new turn has rearmed the notice latch.
agent._interrupt_requested = False
prepare_iteration(agent, messages=messages, api_call_count=3)
assert messages == snapshot
assert agent.iteration_budget.consume()
assert not agent.iteration_budget.consume()
finally:
agent._session_db.close()