Files
roboco/tests/foundation/test_agent_loop.py
53a028ec04 fix(agents): raise tool-call budget to 300 (halt) / 100 (warn) (#464)
* fix(agents): raise the tool-call budget — 150 halted legitimate work mid-task

Repeated budget-sweep bounces: a dev at the 150 ceiling gets its
container killed seconds after a real commit, burning a spawn and the
resumed agent's re-verification turns. 300 keeps the runaway guard while
clearing a real task's footprint; warn scales to 100.

* chore(docs): reflow hard-wrapped prose from the #401 merge

* chore(foundation): regenerate lifecycle artifacts; reflow inherited prose

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 09:21:19 +02:00

97 lines
3.7 KiB
Python

"""Tier 1 — agent_loop budgets + verb retry limits."""
from __future__ import annotations
import os
import pytest
from roboco.agent_sdk import server
from roboco.foundation.policy import agent_loop
# Canonical defaults the foundation guarantees. Asserting against named
# constants keeps the contract explicit (and keeps PLR2004 happy — these
# are not magic numbers, they are the public API surface).
EXPECTED_TOOL_CALL_WARN_AT = 100
EXPECTED_TOOL_CALL_HALT_AT = 300
EXPECTED_LOOP_THRESHOLD = 3
EXPECTED_LOOP_WINDOW = 10
EXPECTED_PM_RESPAWN_MAX_UNPRODUCTIVE = 3
EXPECTED_VERB_RETRY_MAX_PER_MINUTE = 3
EXPECTED_HANDOFF_VERB_CAP = 3
def test_default_budget_has_canonical_thresholds() -> None:
b = agent_loop.DEFAULT_BUDGET
assert b.tool_call_warn_at == EXPECTED_TOOL_CALL_WARN_AT
assert b.tool_call_halt_at == EXPECTED_TOOL_CALL_HALT_AT
assert b.loop_threshold == EXPECTED_LOOP_THRESHOLD
assert b.loop_window == EXPECTED_LOOP_WINDOW
assert b.loop_action == "halt" # NEW: was "warn-only"
assert b.pm_respawn_max_unproductive == EXPECTED_PM_RESPAWN_MAX_UNPRODUCTIVE
assert b.verb_retry_max_per_minute == EXPECTED_VERB_RETRY_MAX_PER_MINUTE
def test_warn_threshold_below_halt_threshold() -> None:
assert (
agent_loop.DEFAULT_BUDGET.tool_call_warn_at
< agent_loop.DEFAULT_BUDGET.tool_call_halt_at
)
def test_loop_threshold_below_loop_window() -> None:
"""Loop detection requires N repeats in a window of M; N must be < M."""
assert (
agent_loop.DEFAULT_BUDGET.loop_threshold < agent_loop.DEFAULT_BUDGET.loop_window
)
def test_verb_retry_limits_cover_critical_handoff_verbs() -> None:
"""The verbs that surfaced in the 2026-05-10 retry storm are capped."""
assert agent_loop.VERB_RETRY_LIMITS["i_am_done"] == EXPECTED_HANDOFF_VERB_CAP
assert agent_loop.VERB_RETRY_LIMITS["complete"] == EXPECTED_HANDOFF_VERB_CAP
assert agent_loop.VERB_RETRY_LIMITS["submit_up"] == EXPECTED_HANDOFF_VERB_CAP
def test_unlimited_retry_verbs_includes_discovery_verbs() -> None:
"""give_me_work / triage / evidence aren't subject to circuit breaker."""
assert "give_me_work" in agent_loop.UNLIMITED_RETRY_VERBS
assert "triage" in agent_loop.UNLIMITED_RETRY_VERBS
assert "evidence" in agent_loop.UNLIMITED_RETRY_VERBS
assert "i_am_idle" in agent_loop.UNLIMITED_RETRY_VERBS
def test_retry_limit_for_known_verb_returns_int() -> None:
assert agent_loop.retry_limit_for("i_am_done") == EXPECTED_HANDOFF_VERB_CAP
def test_retry_limit_for_unlimited_verb_returns_none() -> None:
assert agent_loop.retry_limit_for("give_me_work") is None
def test_retry_limit_for_unknown_verb_returns_default() -> None:
"""Unknown verbs fall through to the policy default."""
assert (
agent_loop.retry_limit_for("not_a_real_verb")
== agent_loop.DEFAULT_BUDGET.verb_retry_max_per_minute
)
def test_agent_sdk_thresholds_match_foundation_defaults() -> None:
"""Without env overrides, the agent_sdk constants equal foundation defaults."""
# If a test environment sets ROBOCO_AGENT_*, the env override path is in
# effect and this default-parity check no longer applies.
if any(
k in os.environ
for k in (
"ROBOCO_AGENT_TOOL_CALL_WARN",
"ROBOCO_AGENT_TOOL_CALL_HALT",
"ROBOCO_AGENT_LOOP_THRESHOLD",
"ROBOCO_AGENT_LOOP_WINDOW",
)
):
pytest.skip("env override set; skipping default-parity check")
assert agent_loop.DEFAULT_BUDGET.tool_call_warn_at == server._WARN_THRESHOLD
assert agent_loop.DEFAULT_BUDGET.tool_call_halt_at == server._HALT_THRESHOLD
assert agent_loop.DEFAULT_BUDGET.loop_threshold == server._LOOP_THRESHOLD
assert agent_loop.DEFAULT_BUDGET.loop_window == server._LOOP_WINDOW