Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/evaluator/termination.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,9 @@ def claude_code_result_error(ctx: TerminationContext) -> str | None:
Background work can produce several ``result`` events. The final result
must follow the last conversation activity; an earlier success cannot
finalize a truncated follow-up turn. Malformed streams and execution-error
results are INFRA_ERROR. ``error_max_turns`` remains a turn-budget LIMIT,
not infrastructure, when it closes the stream.
results are INFRA_ERROR, including ``is_error`` on a ``success`` subtype.
``error_max_turns`` remains a turn-budget LIMIT, not infrastructure, when
it closes the stream.
"""
if ctx.backend != "claude_code":
return None
Expand All @@ -261,7 +262,7 @@ def claude_code_result_error(ctx: TerminationContext) -> str | None:
# No result closes the latest activity, including background turns.
return TerminationReason.INFRA_ERROR
subtype = last_result.get("subtype", "")
if subtype.startswith("error") and subtype != "error_max_turns":
if subtype != "error_max_turns" and (last_result.get("is_error") is True or subtype.startswith("error")):
return TerminationReason.INFRA_ERROR
return None

Expand Down
42 changes: 42 additions & 0 deletions tests/evaluator/test_termination.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from evaluator.backends import get_backend
from evaluator.backends.agentic import AgenticBackend
from evaluator.score import is_non_genuine
from evaluator.termination import (
INFRA_RULES,
TerminationContext,
Expand Down Expand Up @@ -1019,6 +1020,47 @@ def test_cc_exec_error_is_infra(tmp_path):
assert classify(_ctx(p, backend="claude_code")) == TerminationReason.INFRA_ERROR


@pytest.mark.parametrize("agent_exit", [0, 1])
def test_cc_success_subtype_with_error_flag_is_infra(tmp_path, agent_exit):
events = [
*CC_SUCCESS,
{"type": "system", "subtype": "task_notification"},
{"type": "rate_limit_event", "rate_limit_info": {"status": "rejected", "rateLimitType": "five_hour"}},
{
"type": "assistant",
"error": "rate_limit",
"message": {"content": [{"type": "text", "text": "You've hit your session limit"}]},
},
{"type": "result", "subtype": "success", "is_error": True, "stop_reason": "stop_sequence"},
]
p = _write_jsonl(tmp_path / "quota-after-background.jsonl", events)
reason = classify(_ctx(p, backend="claude_code", agent_exit=agent_exit))
assert reason == TerminationReason.INFRA_ERROR
assert is_non_genuine({"termination_reason": reason, "check_verdict": "TIMEOUT"})


def test_cc_error_flag_does_not_require_quota_text(tmp_path):
p = _write_jsonl(tmp_path / "flagged-error.jsonl", [{"type": "result", "subtype": "success", "is_error": True}])
assert classify(_ctx(p, backend="claude_code")) == TerminationReason.INFRA_ERROR


def test_cc_error_flag_recovered_by_later_success_is_ok(tmp_path):
events = [
{"type": "result", "subtype": "success", "is_error": True},
*CC_SUCCESS,
]
p = _write_jsonl(tmp_path / "recovered-error-flag.jsonl", events)
assert classify(_ctx(p, backend="claude_code")) == TerminationReason.OK


def test_cc_runner_timeout_precedes_result_error_flag(tmp_path):
p = _write_jsonl(
tmp_path / "timeout-error-flag.jsonl", [{"type": "result", "subtype": "success", "is_error": True}]
)
ctx = _ctx(p, backend="claude_code", agent_exit=-1, error="claude_code timeout after 7200s")
assert classify(ctx) == TerminationReason.TIMEOUT


def test_cc_max_turns_is_not_infra(tmp_path):
# error_max_turns is a turn-budget LIMIT, not infrastructure — must NOT flag.
p = _write_jsonl(tmp_path / "maxturns.jsonl", CC_MAX_TURNS)
Expand Down
Loading