Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74d41eb559 |
@@ -158,18 +158,23 @@ others, they get their own row here — the default is NOT a general "any 404
|
||||
| SDK `AgentNotAvailable` / `TurnLaunchUnavailable` / `SessionRetired` (stream-open) | ratatoskr `AgentNotAvailable` / `TurnLaunchUnavailable` / (retired → `SessionApiFailed`) — same names, passthrough |
|
||||
| SDK `ConnectionDropped` (mid-stream) | `SseConnectionDropped` |
|
||||
| SDK `ResumeError` subclasses (in resilient stream) | resilient `stream_turn` absorbs; terminal → `SseConnectFailed` |
|
||||
| SDK `Cancel*` (cancel_turn) | folded into `CancelResult`; late-cancel race (B-CAN-3) returns `cancelled=False`, never raises |
|
||||
| SDK `MalformedSseId` / `MalformedSseData` / `TurnIdFlip` (stream `ProtocolError`) | ratatoskr same-named types — same-name rewrap of the discriminated stream protocol errors |
|
||||
| SDK `Cancel*` (cancel_turn) — the SDK RAISES the typed races | 404 `turn_not_found` → `CancelTurnNotFound`; 409 `turn_finished` → `CancelAlreadyCompleted`; other `CancelError` → `CancelFailed`. A 200 (incl. `cancelled=False`, the B-CAN-3 late-cancel no-op) returns a `CancelResult` — never raises. The caller surface stays exception-based (DEC-2; matches the pre-cutover CLI/web handlers). |
|
||||
| `ApiError(404)` on `sessions.create` | `AgentNotFound` |
|
||||
| `ApiError(404)` on `sessions.write_history` | `AuthoredHistoryUnavailable` (hide-existence) |
|
||||
| `ApiError(422 cursor_invalid)` on `sessions.list` | `InvalidCursor` |
|
||||
| `ApiError(502 bifrost_handshake_failed)` on bound `sessions.create` | `BifrostHandshakeFailed` |
|
||||
| `ApiError(422 cursor_invalid)` on `sessions.list` | `InvalidCursor` (dual-key: status 422 AND error_code; the flat cursor body surfaces the code) |
|
||||
| `ApiError(502)` on bound `sessions.create` | `BifrostHandshakeFailed` — NOT gated on error_code (unlike list's 422): INV-002, the synchronous handshake is the SOLE bound-502 cause; and the SDK's envelope parser prefers the nested `detail` (which carries `bifrost_error`, not `error_code`), so no distinguishing top-level `error_code` surfaces. The route+status IS the discriminator. |
|
||||
| **`ApiError` (any other status/route) — the default** | `SessionApiFailed(status, error_code, body)` |
|
||||
|
||||
The default row is load-bearing: any `ApiError` not matched above surfaces as the
|
||||
generic `SessionApiFailed` carrying the raw `status`/`error_code`/`body` — the
|
||||
adapter does NOT invent per-route semantics the contract doesn't list, and does NOT
|
||||
leave an `ApiError` un-mapped. Each slice adds/confirms its route's rows here before
|
||||
the old path is deleted.
|
||||
leave an `ApiError` un-mapped. **This default holds on EVERY route, including the
|
||||
stream and cancel** (each carries a defensive `except ApiError → SessionApiFailed`
|
||||
after its discriminated branches — the SDK maps those routes to discriminated types
|
||||
today, but the default guarantees INV-CUT-2 structurally, not by SDK-internal
|
||||
coupling). Each slice adds/confirms its route's rows here before the old path is
|
||||
deleted.
|
||||
|
||||
## Slice plan (incremental, DEC-4)
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ratatoskr"
|
||||
version = "0.21.8"
|
||||
version = "0.21.9"
|
||||
description = "Worldtree Conversation API debug console (web + headless CLI) — multi-pane observability"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -197,6 +197,12 @@ async def create_session(
|
||||
except ApiError as exc:
|
||||
if exc.status == 404:
|
||||
raise AgentNotFound(agent_id=agent_id) from exc
|
||||
# NOT gated on error_code (unlike list's 422+cursor_invalid): INV-002 — a 502
|
||||
# on a BOUND create IS the synchronous Bifrost handshake failing, the sole
|
||||
# bound-502 cause; and the SDK does not surface a distinguishing top-level
|
||||
# error_code here (its envelope parser prefers the nested `detail`, which
|
||||
# carries `bifrost_error`, not `error_code`). The nested bifrost_error is
|
||||
# extracted for the exception; the route+status is the discriminator.
|
||||
if bifrost is not None and exc.status == 502:
|
||||
raise BifrostHandshakeFailed(
|
||||
bifrost_error=_bifrost_error_from_body(exc.body),
|
||||
@@ -299,6 +305,10 @@ async def stream_turn(
|
||||
raise MalformedSseData(raw=exc.raw) from exc
|
||||
except wtsdk.TurnIdFlip as exc:
|
||||
raise TurnIdFlip(established=exc.established, got=exc.got) from exc
|
||||
except ApiError as exc:
|
||||
# INV-CUT-2 default: an undiscriminated ApiError surfacing from the stream →
|
||||
# SessionApiFailed (the discriminated stream errors are handled above).
|
||||
raise translate_error(exc) from exc
|
||||
|
||||
|
||||
async def cancel_turn(
|
||||
@@ -323,3 +333,6 @@ async def cancel_turn(
|
||||
raise CancelFailed(
|
||||
status=0, body=(getattr(exc, "message", "") or str(exc)).encode()
|
||||
) from exc
|
||||
except ApiError as exc:
|
||||
# INV-CUT-2 default: an undiscriminated ApiError on this route → SessionApiFailed.
|
||||
raise translate_error(exc) from exc
|
||||
|
||||
@@ -299,6 +299,12 @@ class TestReadPassthroughs:
|
||||
await get_session_messages(_wt(fake), "s")
|
||||
assert ei.value.status == 401
|
||||
|
||||
async def test_tools_error_maps_to_session_api_failed(self) -> None:
|
||||
fake = _FakeSessions(error=ApiError("auth_revoked", "no", status=401))
|
||||
with pytest.raises(SessionApiFailed) as ei:
|
||||
await get_session_tools(_wt(fake), "s")
|
||||
assert ei.value.status == 401
|
||||
|
||||
|
||||
async def _drain(aiter: Any) -> list[Any]:
|
||||
out: list[Any] = []
|
||||
@@ -376,6 +382,14 @@ class TestStreamTurn:
|
||||
await _drain(stream_turn(_wt(fake), "s", "hi"))
|
||||
assert (ei.value.established, ei.value.got) == (5, 7)
|
||||
|
||||
async def test_undiscriminated_api_error_maps_to_session_api_failed(self) -> None:
|
||||
# INV-CUT-2 default: an undiscriminated ApiError surfacing from the stream
|
||||
# (not a discriminated stream error) → SessionApiFailed.
|
||||
fake = _FakeSessions(stream_error=ApiError("weird", "boom", status=500))
|
||||
with pytest.raises(SessionApiFailed) as ei:
|
||||
await _drain(stream_turn(_wt(fake), "s", "hi"))
|
||||
assert ei.value.status == 500
|
||||
|
||||
|
||||
class TestCancelTurn:
|
||||
async def test_happy_returns_cancel_result(self) -> None:
|
||||
@@ -410,3 +424,11 @@ class TestCancelTurn:
|
||||
fake = _FakeSessions(error=wtsdk.CancelFailed(42, error_code="boom", message="failed"))
|
||||
with pytest.raises(CancelFailed):
|
||||
await cancel_turn(_wt(fake), "s", 42)
|
||||
|
||||
async def test_undiscriminated_api_error_maps_to_session_api_failed(self) -> None:
|
||||
# INV-CUT-2 default: an undiscriminated ApiError on the cancel route (not a
|
||||
# typed Cancel* race) → SessionApiFailed, never leaked as a bare ApiError.
|
||||
fake = _FakeSessions(error=ApiError("weird", "boom", status=500))
|
||||
with pytest.raises(SessionApiFailed) as ei:
|
||||
await cancel_turn(_wt(fake), "s", 42)
|
||||
assert ei.value.status == 500
|
||||
|
||||
Reference in New Issue
Block a user