diff --git a/pyproject.toml b/pyproject.toml index b5a29d1..a265393 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.18.3" +version = "0.18.4" description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard" readme = "README.md" requires-python = ">=3.12" diff --git a/src/ratatoskr/sse_client.py b/src/ratatoskr/sse_client.py index 345d0c1..da9b963 100644 --- a/src/ratatoskr/sse_client.py +++ b/src/ratatoskr/sse_client.py @@ -241,7 +241,11 @@ class TurnLaunchUnavailable(SseConnectFailed): self.message = message -_EAGER_TURN_FAILURE_CODE = {409: "agent_not_available", 503: "turn_launch_unavailable"} +# Canonical error_codes (Worldtree #331 / v1.0.0b2): 409 -> agent_not_available, +# 503 -> not_ready (retryable; re-pinned from internal_error). Used only as a +# fallback default when the body omits error_code — the real code is surfaced +# verbatim from the {detail:{error_code,message}} envelope. +_EAGER_TURN_FAILURE_CODE = {409: "agent_not_available", 503: "not_ready"} def _eager_failure_fields(body: bytes, status: int) -> tuple[str, str]: diff --git a/tests/test_sse_client.py b/tests/test_sse_client.py index d7ea595..6c3ffac 100644 --- a/tests/test_sse_client.py +++ b/tests/test_sse_client.py @@ -479,6 +479,19 @@ class TestStreamTurn: _ = [e async for e in stream_turn(client, "s1", "hi")] assert exc.value.error_code == "agent_not_available" + @respx.mock + async def test_eager_503_non_json_body_defaults_not_ready(self) -> None: + """b2: eager 503 with a non-JSON body -> TurnLaunchUnavailable with the + canonical default error_code `not_ready`.""" + respx.post("https://w.example/sessions/s1/messages").mock( + return_value=httpx.Response(503, content=b"nope") + ) + async with httpx.AsyncClient(base_url="https://w.example") as client: + with pytest.raises(TurnLaunchUnavailable) as exc: + _ = [e async for e in stream_turn(client, "s1", "hi")] + assert exc.value.error_code == "not_ready" + assert exc.value.retryable is True + @respx.mock async def test_no_text_aggregation(self) -> None: """no_text_aggregation: consumer yields each text event separately; no concat.""" diff --git a/uv.lock b/uv.lock index abb0edf..f4ca9d8 100644 --- a/uv.lock +++ b/uv.lock @@ -1052,7 +1052,7 @@ wheels = [ [[package]] name = "ratatoskr" -version = "0.18.3" +version = "0.18.4" source = { editable = "." } dependencies = [ { name = "httpx" },