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
1 change: 1 addition & 0 deletions src/autoskillit/execution/session/_exit_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def classify_infra_exit(
if capabilities is None or capabilities.supports_context_exhaustion_detection:
if session._is_context_exhausted():
return InfraExitCategory.CONTEXT_EXHAUSTED
# Codex turn.failed arrives on stdout (NDJSON); this stderr branch is non-Codex fallback.
if _CODEX_CONTEXT_EXHAUSTION_PATTERN.search(result.stderr):
return InfraExitCategory.CONTEXT_EXHAUSTED
# Rate limit detection β€” must precede all API_ERROR checks (including
Expand Down
50 changes: 48 additions & 2 deletions tests/execution/test_exit_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ def _sr(
)


def _turn_failed_ndjson(error_message: str) -> str:
return json.dumps({"type": "turn.failed", "error": {"message": error_message}})
def _turn_failed_ndjson(error_message: str, *, error_code: str = "") -> str:
error: dict[str, str] = {"message": error_message}
if error_code:
error["code"] = error_code
return json.dumps({"type": "turn.failed", "error": error})


class TestClassifyInfraExit:
Expand Down Expand Up @@ -412,6 +415,49 @@ def test_turn_failed_rate_limit_exceeded_jsonl_context_exhausted_false(self) ->
adapted = _adapt_agent_result(agent_result)
assert adapted.jsonl_context_exhausted is False

def test_turn_failed_error_code_only_classify_returns_context_exhausted(self) -> None:
ndjson = _turn_failed_ndjson(
"Agent terminated unexpectedly", error_code="context_length_exceeded"
)
agent_result = CodexResultParser().parse_stdout(ndjson)
adapted = _adapt_agent_result(agent_result)
result = _sr(returncode=1)
assert classify_infra_exit(adapted, result) == InfraExitCategory.CONTEXT_EXHAUSTED

def test_turn_failed_error_code_and_message_classify_returns_context_exhausted(self) -> None:
ndjson = _turn_failed_ndjson(
"The model's context length has been exceeded",
error_code="context_length_exceeded",
)
agent_result = CodexResultParser().parse_stdout(ndjson)
adapted = _adapt_agent_result(agent_result)
result = _sr(returncode=1)
assert classify_infra_exit(adapted, result) == InfraExitCategory.CONTEXT_EXHAUSTED

def test_turn_failed_message_only_classify_returns_context_exhausted(self) -> None:
ndjson = _turn_failed_ndjson("context_length_exceeded", error_code="")
agent_result = CodexResultParser().parse_stdout(ndjson)
adapted = _adapt_agent_result(agent_result)
result = _sr(returncode=1)
assert classify_infra_exit(adapted, result) == InfraExitCategory.CONTEXT_EXHAUSTED

def test_turn_failed_non_exhaustion_error_code_not_context_exhausted(self) -> None:
ndjson = _turn_failed_ndjson("Internal server error", error_code="server_error")
agent_result = CodexResultParser().parse_stdout(ndjson)
adapted = _adapt_agent_result(agent_result)
result = _sr(returncode=1)
assert classify_infra_exit(adapted, result) == InfraExitCategory.API_ERROR

def test_stderr_non_exhaustion_not_context_exhausted(self) -> None:
session = ClaudeSessionResult(
subtype="success",
is_error=False,
result="",
session_id="s1",
)
result = _sr(returncode=1, stderr="some transient error")
assert classify_infra_exit(session, result) == InfraExitCategory.COMPLETED


class TestRateLimitClassification:
def test_rate_limited_text_classified_as_rate_limited(self) -> None:
Expand Down
Loading