SDK-adapter cutover — replace ratatoskr's hand-rolled httpx consumer wrappers (sessions/sse_client/tier3) with a thin adapter over worldtree-sdk (Python) 1.0.0, deleting the old wrappers; CLI/TUI/web surfaces and caller-semantic exceptions preserved.
src/ratatoskr/wt.py
src/ratatoskr/cli.py
src/ratatoskr/tui.py
src/ratatoskr/web/server.py
Cut ratatoskr's CONSUMER client layer over from its hand-rolled httpx wrappers (sessions.py / sse_client.py / tier3.py / first_message.py) to consume worldtree-sdk (Python) 1.0.0. Replace the wrappers with a thin ratatoskr-owned adapter over the SDK; delete the old wrappers; keep the CLI / TUI / Starlette web surfaces and ratatoskr's caller-semantic exception surface. The Bifrost PROVIDER planes (memory/affect — where Worldtree dispatches INTO ratatoskr, ADR-0009) are consumer-orthogonal and untouched. Cutover decided by operator ruling 2026-07-18; design locked via vor-cross with worldtree-codex (thread 01KXWCWV3VKNR0HP74D2RCY0DC).
worldtree-sdk (Python) == 1.0.0 # git tag python-v1.0.0; pin to the internal Gitea registry once published (DEC-5)
ratatoskr.cli
ratatoskr.tui
ratatoskr.web
python
high
400
0.8
worldtree-sdk 1.0.0 is the GA Python consumer SDK ratatoskr's parity pass shaped; its resource surface (WorldtreeClient.sessions/agents/characters/me/capabilities/models/admin) covers all 21 of ratatoskr's covered routes. Verified against ~/development/worldtree-sdk @ python-v1.0.0.
Caller-injected transport is a first-class tested path (PY-INV-012): WorldtreeClient(base_url, auth=..., transport=my_httpx_async_client) uses ratatoskr's AsyncClient and NEVER closes it. Ratatoskr keeps base_url / bearer / User-Agent / timeout / web-app lifecycle ownership.
The SDK REST floor is an undiscriminated ApiError(error_code, message, status, body) (B-ERR-1/B-OPEN-3); hide-existence 404s are NOT distinguished by the SDK (B-ERR-4). Ratatoskr's caller-semantic mapping lives one layer up (DEC-2).
Single SDK-adapter contract per operator DEC-1. This is a migration contract: its
spine is a route-family slice plan, not a dense FN-block spec — each slice's
adapter FN detail is filled in at that slice's TDD (incremental migration; the
per-route SDK signatures are already mapped below).
Locked design (operator-accepted 2026-07-18, post vor-cross)
DEC-1 — retire, don't repurpose (two-phase). Normative authority for the
consumer wire transfers to THIS contract immediately (as of this file, #2 sessions
and #15 tier3 are superseded — no longer the spec of record); the contract FILES
are DELETED at slice-7 teardown, only after the code they specified is gone
(avoids both orphaning live code and leaving obsolete norms active mid-migration).
first_message is NOT retired — it stays a ratatoskr-owned usage contract at the
spec level (its preset / best-effort-timeout / swallow policy is local, not
SDK-owned). Repurposing the wire contracts would risk a shadow-normative copy of
SDK-owned semantics — the exact thing the cutover eliminates.
DEC-2 — thin semantic error adapter. CLI/web/TUI keep ratatoskr's typed
exceptions; the adapter maps the SDK's ApiError floor → ratatoskr's
caller-semantic types (the SDK stays undiscriminated; hide-existence maps here).
DEC-3 — no shims (house rule). Delete the old wrapper modules and update all
call-sites to the new adapter. No compatibility facade preserving old
module/function APIs (pre-1.0 no-backwards-compat rule).
DEC-4 — incremental + live-smoke, no soak. Migrate one route-family per
slice, TDD each slice, and run a targeted LIVE smoke of that family against the
personal endpoint before deleting the replaced hand-rolled path. (The SDK's
fixture corpus proves the WIRE; the live smoke proves ratatoskr's
presenter/adapter/env-wiring survived the dependency inversion.)
Smoke acceptance bar (the minimum that lets an old path be deleted): drive
each route in the family's primary flow END-TO-END through the ratatoskr surface
(CLI/web) — not the wrapper directly — against the personal endpoint under the
real consumer key, and OBSERVE the expected success (a created session, a streamed
turn that renders, a persona write that reads back), not merely a non-crash. A
smoke that only asserts "no exception" does NOT clear the bar. Smoke vs. soak
boundary: a smoke is a single pass of the family's flows at deletion time; a
(forbidden) soak is a sustained dual-path parallel-run — do the former, not the
latter.
DEC-5 — wait for the registry. Pin worldtree-sdk == 1.0.0 from the internal
Gitea registry once published (verify the uv install first); use the
python-v1.0.0 git tag only if the registry lags. Contract-drafting is NOT
gated (SDK source is inspectable); IMPLEMENTATION pin is.
DEC-6 — minor bump at ship. The cutover ships as a minor (architecture-cutover
milestone; pre-1.0 breaking-internal) — requires explicit operator approval at
ship time, not now.
Adapter design
ratatoskr.wt — a thin adapter module owning ONE WorldtreeClient, built
with ratatoskr's own httpx.AsyncClient injected (transport=), ratatoskr's
auth/admin_auth providers, and the ratatoskr User-Agent. It exposes
ratatoskr-semantic call surfaces the presenters already expect, and translates
SDK errors → ratatoskr exceptions (DEC-2).
Transport ownership (INV-CUT-1 [hard]): the injected AsyncClient is
ratatoskr-owned; WorldtreeClient.aclose() / async with exit must NOT close
it (SDK _owns_client=False on injection). Ratatoskr owns the client lifecycle
exactly as today.
Error boundary (INV-CUT-2 [hard]): ratatoskr's caller-semantic exceptions
are raised by the adapter, mapping from the SDK where the SDK already
discriminates (AgentNotAvailable, TurnLaunchUnavailable, SessionRetired,
Cancel*, Resume*, Malformed*) and from ApiError(status=..., error_code)
on a KNOWN route otherwise — including the hide-existence 404 →
AuthoredHistoryUnavailable (labeling a call we made, NOT capability-probing).
Provider boundary (INV-CUT-3 [hard]): the Bifrost provider planes stay
hand-rolled and are never routed through the SDK. The Bifrost-BOUND
sessions.create (consumer-side, bifrost= + consumer-key auth) DOES go through
the SDK and stays consumer-layer.
Import boundary (INV-CUT-4 [hard]):tests/test_no_worldtree_imports.py
stays; the boundary becomes "no Worldtree SOURCE imports", with worldtree_sdk
the allowed client dependency.
Invariants
INV-CUT-1 [hard] — Ratatoskr owns the injected httpx.AsyncClient lifecycle;
the SDK never closes it (_owns_client=False on transport injection).
INV-CUT-2 [hard] — The adapter raises ratatoskr's caller-semantic exceptions
per the explicit § Error map table below — mapping from the SDK's discriminated
errors (passthrough) and from ApiError by the ROUTE that raised it (the route is
the discriminator, never the body). Every ApiError is mapped; the default is
SessionApiFailed. No route invents semantics the table doesn't list.
INV-CUT-3 [hard] — The Bifrost PROVIDER planes never route through the SDK;
the Bifrost-BOUND sessions.create does (consumer-side).
INV-CUT-4 [hard] — The no-Worldtree-SOURCE-import boundary holds
(worldtree_sdk allowed); tests/test_no_worldtree_imports.py enforces it.
INV-CUT-5 [hard] — Each slice runs a LIVE smoke of its route family before the
replaced hand-rolled path is deleted (DEC-4); no old path is removed unproven.
Note: SDK returns are typed shapes.*; per the parity pass the freeform reads
(SessionTools, BifrostInspection, capabilities) are deliberately open-world in
the Python spine. The adapter PASSES THESE THROUGH unmodified (does not normalize);
presenters read them as mappings, tolerant of wire drift. If an open-world read is
malformed the presenter degrades (never crashes) — same posture as the existing
_format_whoami hardening.
Error map (INV-CUT-2 — the explicit table)
The ROUTE is the discriminator, not the error body (labeling a call we made, NOT
capability-probing — B-ERR-4). A 404 is mapped by which method raised it; the adapter
NEVER sniffs the body to decide whether something exists. Authored-history is
currently the ONLY hide-existence route (per Worldtree B-ERR-4); if Worldtree adds
others, they get their own row here — the default is NOT a general "any 404 → hidden".
folded into CancelResult; late-cancel race (B-CAN-3) returns cancelled=False, never raises
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 (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.
Slice plan (incremental, DEC-4)
Each slice: wire the family through ratatoskr.wt → update call-sites → TDD →
LIVE smoke against the personal endpoint → delete the replaced hand-rolled path →
re-anchor its coverage-map rows.
Foundation — ratatoskr.wt module + injected WorldtreeClient + auth
providers + the error-adapter skeleton (the § Error map's default ApiError →
SessionApiFailed rule + the discriminated-error passthrough). No USER-VISIBLE
behavior change (imports/construction change; the skeleton is unit-tested but
unexercised by any surface until slice 2). Done = ratatoskr.wt importable, the
client constructs with an injected transport, and the error-map default is unit-
covered.
Sessions/turn — create/list/messages/tools/cancel + the turn STREAM + the
Bifrost-bound create. The turn presenter wires the resilient sessions.stream_turn
(auto-resume — this ABSORBS the old reconnect_turn, which is deleted this slice
with its call-sites); stream_turn_raw is available for a single-attempt caller
but is NOT the presenter default. (Highest-traffic; the SSE presenter is the
riskiest wiring.)
Persona + authored-history — set_persona_state (PadState) + write_history
(hide-existence mapping) + first_message presets (stay ratatoskr-owned OVER the
SDK method).
CLI / TUI / Starlette web surfaces — their EXTERNAL BEHAVIOR is preserved (their
call-sites DO change — they're updated to the new adapter per touches:; what's
invariant is the observable behavior, not the code).
The four parked post-v1 SDK ergonomics items (replayed:bool, raw-dict persona
escape hatch, list_all iterator, split session types) — coordinated post-v1 lane
with wtsdk-dev, not this cutover.
Coverage-map re-anchor
docs/coverage-map.md shifts from "hand-rolled route exists + live-proven" to
"SDK method wired and exercised by a ratatoskr surface"; excluded-by-design rows
stay explicit. Re-anchored per slice as each family lands.