The last slice of the consumer-layer cutover. Teardown only — zero runtime-logic change; the 494-green suite is the regression gate. - Drop `httpx-sse` from pyproject + lockfile: slice-6 deleted its last user, nothing imports `httpx_sse`, the SDK owns SSE parsing now. - Module boundary (operator decision): KEEP `sessions.py` + `sse_client.py` as pure caller-semantic type/exception homes (no rename, no fold — A3 was blocked by the `AgentNotAvailable` name collision + `wt.py` would mis-home `endpoint_for_plane`). Docstrings updated to stop claiming "client"; the `AdminEvent`/`SseId`/exception homes stay put (resolves the slice-6 deferred-home item). - Retire wire contracts #2 (sessions) + #15 (tier3): DEC-1 phase-2 — normative authority already transferred to the cutover contract; the code they specified is gone, so the files are deleted. #1 (SSE event vocab) and `first_message` stay (ratatoskr-owned, not retired). - Final coverage-map re-anchor: tools/list_sessions re-homed to `wt.py`; the Last-Event-ID SSE-resume sub-gap CLOSED (folded into `stream_turn` auto-resume); Surface-2 SSE parsing re-anchored to the SDK. - Stale doc-rot fix: the cli.py transport comment no longer calls `seed_preset_first_message` "not-yet-migrated" (it rides `wt`). - v0.22.0 (minor, DEC-6, operator-approved): publishes the full 6-slice cutover milestone.
29 KiB
contract_version, module, purpose, touches, scope, depends_on, used_by, language, complexity, estimated_loc, confidence, assumptions
| contract_version | module | purpose | touches | scope | depends_on | used_by | language | complexity | estimated_loc | confidence | assumptions | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2.1 | ratatoskr.wt | 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. |
|
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). |
|
|
python | high | 400 | 0.8 |
|
worldtree-sdk cutover — consumer-layer adapter contract
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_messageis 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
ApiErrorfloor → 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.0from the internal Gitea registry once published (verify the uv install first); use thepython-v1.0.0git 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 ONEWorldtreeClient, built with ratatoskr's ownhttpx.AsyncClientinjected (transport=), ratatoskr'sauth/admin_authproviders, 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
AsyncClientis ratatoskr-owned;WorldtreeClient.aclose()/async withexit must NOT close it (SDK_owns_client=Falseon 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 fromApiError(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.pystays; the boundary becomes "no Worldtree SOURCE imports", withworldtree_sdkthe allowed client dependency.
Invariants
- INV-CUT-1 [hard] — Ratatoskr owns the injected
httpx.AsyncClientlifecycle; the SDK never closes it (_owns_client=Falseon 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
ApiErrorby the ROUTE that raised it (the route is the discriminator, never the body). EveryApiErroris mapped; the default isSessionApiFailed. 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.createdoes (consumer-side). - INV-CUT-4 [hard] — The no-Worldtree-SOURCE-import boundary holds
(
worldtree_sdkallowed);tests/test_no_worldtree_imports.pyenforces 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.
Route map — ratatoskr wrapper → SDK method (grounded @ python-v1.0.0)
| Ratatoskr wrapper (retiring) | SDK method | SDK return |
|---|---|---|
create_session |
sessions.create(...) |
SessionInfo |
list_sessions |
sessions.list(limit, cursor) |
page |
stream_turn |
sessions.stream_turn(...) (resilient) / stream_turn_raw (single) |
AsyncIterator[Event] |
reconnect_turn |
folded into sessions.stream_turn auto-resume |
— |
cancel_turn |
sessions.cancel_turn(...) |
CancelResult |
get_session_messages |
sessions.messages(id) |
open-world |
get_session_tools |
sessions.tools(id) |
SessionTools |
set_persona_state |
sessions.set_persona_state(id, PadState) |
None |
write_authored_history |
sessions.write_history(...) |
authored-turn |
get_persona_state |
agents.persona_state(agent_id) |
AgentPersonaState |
list_agents |
agents.list() |
AgentList |
define_agent |
agents.define(...) |
DefinedAgent |
patch_agent |
agents.patch(id, changes) |
PatchedAgent |
delete_agent |
agents.delete(id) |
None |
get_me |
me.get() |
MeInfo |
get_capabilities |
capabilities.get() |
Capabilities |
list_character_models |
models.available_for_characters() |
ModelsAvailable |
create_character |
characters.create(...) |
CreatedCharacter |
get_character_state |
characters.state(id) |
CharacterState |
delete_character |
characters.delete(id) |
CharacterDeleteResult |
get_session_bifrost |
admin.sessions.bifrost(id) |
BifrostInspection |
stream_admin_events |
admin.stream_events(...) |
AsyncIterator[AdminEvent] |
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".
| SDK raises (from) | Adapter maps to |
|---|---|
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 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 (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(429) on agents.define (slice-4) |
Tier3QuotaExceeded(retry_after=0) — the SDK's ApiError floor carries no response headers, so the Retry-After header the hand-rolled path read is unavailable; spec §2675 pins Phase-2.0 quota to Retry-After: 0, so the adapter defaults to 0. A non-zero forward-compat value is unrecoverable until the SDK surfaces headers (INFORM wtsdk-dev; reference-impl posture). |
ApiError(403 tier3_user_id_unsupported) on agents.define (slice-4) |
Tier3UserIdUnsupported (dual-key: status 403 AND error_code) |
ApiError(422 layer_deferred) on agents.define (slice-4) |
Tier3LayerDeferred(field) — field parsed from the body (detail.field / flat field); the SDK carries error_code but not field, so the adapter body-parses it (same posture as bound-502's bifrost_error) |
ApiError(404) on agents.patch / agents.delete (slice-4) |
Tier3AgentNotFound (route-discriminated; agents CRUD is NOT a hide-existence route — a 404 there IS "no such agent") |
ApiError(422 field_not_mutable) on agents.patch (slice-4) |
Tier3FieldNotMutable(field) (dual-key status+error_code; field body-parsed) |
ApiError(404 persona_not_configured) on agents.persona_state (slice-4) |
PersonaNotConfigured (dual-key) |
ApiError(404 agent_not_available) on agents.persona_state (slice-4) |
AgentNotAvailable (the persona-surface sessions.AgentNotAvailable, distinct from the eager-turn sse_client.AgentNotAvailable; dual-key) |
ApiError(403 auth_scope_denied) on agents.persona_state (slice-4) |
AuthScopeDenied(scope="persona.read") (dual-key) |
ApiError (any other status/route, incl. agents.list and any unmatched agent-route code) — 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. 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)
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.wtmodule + injectedWorldtreeClient+ auth providers + the error-adapter skeleton (the § Error map's defaultApiError→SessionApiFailedrule + 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.wtimportable, 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 oldreconnect_turn, which is deleted this slice with its call-sites);stream_turn_rawis 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).
- Agents (Tier-3) — list/get/define/patch/delete/persona_state. (Folds in the
pending
model→rolecutover — see project-tier3-agents-model-to-role-pending.) - Characters + me/capabilities/models — the remaining consumer reads.
- Admin — admin.sessions.bifrost + admin.stream_events (admin_auth).
- Teardown — delete residual hand-rolled modules; drop
httpx-sse(SDK owns SSE parsing); retire contracts #2/#15; final coverage-map re-anchor; minor bump (DEC-6, operator approval).
Slice-4 notes (Agents/Tier-3 + model→role fold, decided at TDD)
model→rolecutover folds in here (scope B). Worldtree spec 1.2 (v1.0.0b128, live on :8080/:8081) made the/agents/defineresponse echorole, closing the old W-4modelecho. The adapter returns the SDK's OPEN-WORLDDefinedAgent/PatchedAgentdicts verbatim (parity posture); callers readinfo["role"]. The frozenTier3AgentInfodataclass (which readbody["model"]and would KeyError post-b128) is DELETED — no dataclass normalization layer survives.ratatoskr.local_agentsschema bump.LocalAgentEntry.model→.role(the field stores what the wire now calls a role);_SCHEMA_VERSION1→2 so any pre-cutover on-disk index is discarded cleanly (no-backwards-compat, DEC-3).AgentNotAvailablename collision.sessions.AgentNotAvailable(persona-state 404agent_not_available) andsse_client.AgentNotAvailable(eager-turn 409) are distinct types that share a name;wtalready imports the sse_client one for the stream, so it imports the persona one ALIASED (PersonaAgentNotAvailable) and raises it fromget_persona_state. The web endpoint keeps importing the personaAgentNotAvailablefromsessions(same class), so itsexceptis unchanged.ConnectFailedat every rewired caller (slice-3 foot-gun). The SDK normalizes ANY transport failure toConnectFailed(status=0)(request.py), not a raw httpx error. The rewired tier3 CLI and both web endpoints (_agents_endpoint,_persona_state_endpoint) catchwtsdk.ConnectFailed→ their existing network-error surface (CLI exit 21 / web 502). The webtest_network_error_returns_502(respxhttpx.ConnectErrorside-effect) is the RED that proves this.agents.get(agent_id)(SDKGET /agents/{id}) is NOT wrapped — ratatoskr has noget_agentconsumer; only list/persona_state/define/patch/delete are in coverage.- Client-side Tier-3-id PRE on
patch_agent/delete_agent. Both assert":" in agent_idpre-HTTP (a Tier-3 id is always<user_id>:<agent_name>, ADR-0019), so a non-colon id fails fast with anAssertionErrorrather than reaching the SDK's route-discriminated 404 →Tier3AgentNotFound. Intentional fail-fast on a wrong-shaped id (carried over from the retired hand-rolled wrappers); documented here per the heid-code-review slice-4 precision flag (the § Error map 404 rows assume a well-formed Tier-3 id reaches the route).
Slice-5 notes (Characters + me/capabilities/models, decided at TDD)
- No new § Error map rows. All six routes (
me.get,capabilities.get,models.available_for_characters,characters.create/.state/.delete) are open-world reads/acks (B-OPEN-2) whose SDK ops carry NO discriminated error (nomap_error), so everyApiErrormaps to the defaultSessionApiFailed— exact parity with the retiring hand-rolled path, which likewise raised only its genericSessionApiFailedon any non-2xx (never discriminating a status/code on these routes). The route-map table above already lists all six. create_characterbody — omitstatewhen None. The adapter sends{"character": …}plus"state"only when the caller supplies a non-None state (the SDK forwards the body dict as-is via httpxjson=). This drops the hand-rolled path's redundant explicit"state": null— server-equivalent (Worldtree'sCreateCharacterRequest.statedefaults None whether omitted or explicit-null), SDK-idiomatic (matches the SDK'sCreateCharacterInputNotRequiredshape), and invisible at the sole call-site (--charactersnever passes a state). Adopt- canonical over byte-for-byte wire parity.delete_characterreturns the SDK's open ack verbatim (-> Mapping | None). The SDK route returns an open-world ack body (not 204 —CharacterDeleteResult), so the adapter passes it through rather than normalizing to the hand-rolledNone(parity posture: no None-normalization of an open-world read). On a 204 no-content the SDK yieldsNone, so the return type isMapping | None; the sole call-site (--characters) ignores the value, so the change is unobservable.- Open-world presenter degrade-not-crash (cumulative foot-gun).
_format_whoamiis already hardened (slice-4 heid bug-hunt). The rewired_characters_probeextracts the created id defensively (created.get("character_id")+ type-guard → clean abort, never a hard-index KeyError) since the create ACK is now an open-world SDK read. - Container-type hardening (heid code-review + bug-hunt slice-5). The degrade-not-
crash floor is guarded at THREE levels for the CLI presenters, not just one: (a) the
list-typed fields
scopes/allowed_roles/ modelitemsdegrade a non-list scalar (123) or a bare string to empty via_display_seq/ anisinstance(_, list)guard — the olderor []idiom only caught null/absent and wouldfor x in 123TypeError; (b) each element is type-guarded (isinstance(m, dict)); (c) the top-level open-world readscreated/models/stateareisinstance(_, Mapping)-guarded before any.get(a non-mapping passthrough would otherwiseAttributeError). All three feed--whoami/--charactersonly. - Accepted (not fixed): the
--charactersprobe leaks its transient character on a mid-lifecycle failure. create → get-state → delete runs linearly with nofinally, so a state/delete failure after a successful create orphans the probe character until its TTL. This is PRE-EXISTING (the retired hand-rolled probe had the identical structure — the cutover did not worsen it), TTL-bounded, and--charactersis a one-shot diagnostic smoke; atry/finallycleanup would also swallow a happy-path delete-failure (delete is both the teardown AND a tested lifecycle step). Accepted as known-risk per the heid bug-hunt (Gróa + Heid concur accept is defensible). - CLI-only rewire.
me/capabilities/characters/modelshave NO web-server caller — only the--whoamiand--charactersCLI one-shot probes. The web surface is untouched this slice.
Slice-6 notes (Admin: bifrost inspection + admin-events stream, decided at TDD)
- Admin auth moves from a per-call header override to the client's
admin_auth. The SDK'sadmin.*methods authenticate with the client'sadmin_authprovider (set viabuild_client(admin_key=...)), NOT a per-requestAuthorizationheader. So the two web admin endpoints build their wt client WITHadmin_key(_wt_client(client, admin_key=...), extended this slice); the hand-rolled per-calladmin_key=+ header-override is retired. The web already guardsif not admin_key: 400before the call, so the SDK's pre-HTTPConfigurationError(missing admin_auth, W-5) is unreachable from the web surface. CLI has no admin caller — both routes are web-only (the coverage-map'stui.pyrows were stale; corrected toweb/server.py). get_session_bifrost— no new § Error map row.client.admin.sessions.bifrostreturns the open-worldBifrostInspectiondict verbatim; anyApiError(notably 403auth_scope_denied, 404session_not_bifrost_bound) → theSessionApiFaileddefault — exact parity with the retired path (which mapped every non-200 →SessionApiFailed).stream_admin_eventsre-wraps the SDK'sAdminEvent→ ratatoskr'sAdminEvent(chosen over yield-through). The SDK'sAdminEventdiverges from ratatoskr's:admin_id: int|float(nanfor an id-less envelope) vs ratatoskr'sid: int(0 default), and the SDK'stype/dataare None-able where ratatoskr's are a dotted-str / a{}-default dict. The web filter + SSE formatter readev.id/ev.type/ev.data. The adapter re-wraps at the boundary —id = admin_id if int else 0(nan→0),type = type or ""(None→"" so.startswithnever crashes),data = data or {}— degrading the SDK's open-world None/nan ONCE at the adapter, keeping the web endpoint +_admin_event_matches_web+ the ratatoskrAdminEventdomain type UNCHANGED (preserves the web surface per § Out of scope). Rejected alternative: yield SDKAdminEvents through and rewire the web filter foradmin_id/None/nan (the slice-2 turn-stream precedent) — heavier web churn + scatters the None/nan hardening through the filter; re-wrap localizes it. The ratatoskrAdminEventdataclass stays insse_client.pythis slice (imported bywt+ the web); its home moves in slice-7 teardown ifsse_client.pyis retired.- Admin-stream error mapping (reuses the § Error map stream rows). The SDK admin
stream raises
ApiError("admin_stream_failed", status=…)on a NON-200 open (NOTConnectFailed— a gotcha the web integration test caught that the unit fake could not) →SseConnectFailed; andConnectionDroppedon a connect-time transport failure (cursor None) OR a mid-stream drop / the long-lived stream's resumable EOF (cursor set) →SseConnectionDropped. The SDK admin stream is best-effort (skips malformed frames — noMalformed*), as was the retired hand-rolled path; the web endpoint's existingexcept (…, MalformedSseId, MalformedSseData)stays a harmless defensive superset (pre-existing, not introduced here).
Slice-7 notes (Teardown — the LAST slice, decided at teardown)
- Module boundary: KEEP
sessions.py+sse_client.pyas pure type/exception homes (operator decision A1, 2026-07-19). Post-cutover both modules hold NO client — only ratatoskr's caller-semantic exception surface + a couple of dataclasses (BifrostBinding;SseId,AdminEvent) + theendpoint_for_planeprovider helper. Options weighed: (A1) keep as-is + fix docstrings; (A2) rename to honest names (session_errors/stream_errors), re-point ~7 importers; (A3) consolidate into oneerrors.py/ fold intowt.py. A1 chosen — teardown is deletion + dep-drop, not a rename refactor; A3 is blocked by theAgentNotAvailablename collision (two distinct classes: persona-404 insessionsvs eager-turn-409 insse_client) which would force renaming a contract-level caller-semantic type + its § Error-map rows + catch sites, and folding intowt.pymis-homesendpoint_for_plane(provider-side). Naming-honesty (principle-2) addressed by the one-line docstring note, not a rename. Resolves the slice-6 open item (line ~326): the ratatoskrAdminEvent/SseId+ exceptions stay insse_client.py; the session/tier3 exceptions +BifrostBindingstay insessions.py. httpx-ssedropped frompyproject.toml+ lockfile. Slice-6 deleted its last user (sse_client.stream_admin_events); a tree grep confirmed nothing importshttpx_sse.uv syncphysically pruned it; suite green (494) with the module absent.- Wire contracts #2 (sessions) + #15 (tier3) retired (files DELETED, DEC-1
phase-2). Their normative authority transferred to this contract at authoring;
the code they specified is gone, so the files are removed now. #1 (SSE event
vocabulary) is NOT retired — it stays current (amended
4bd9abd2026-07-18) as ratatoskr's SSE-event-rendering reference;first_messageis NOT retired (DEC-1, ratatoskr-owned usage contract). Accepted side-effect:issues/5.contract.md's historical "amended #2/#3/#4 in-place" line now points at a deleted #2 — left as-is (frozen issue-record of a past action; not expanding DEC-1's #2/#15 scope). - Final coverage-map re-anchor.
GET /sessions/{id}/tools→wt.py get_session_tools(SDKsessions.tools) →web/server.py(the oldsessions.py→tui.pyrow was stale; TUI deleted).GET /sessionslist_sessionsre-homed towt.py, still caller-less (picker was a TUI frontier, now moot). TheLast-Event-IDSSE-resume sub-gap is CLOSED —reconnect_turndeleted, resume folded intowt.py stream_turnauto-resume. Surface-2 SSE parsing re-anchored to the SDK (_envelope_for_typegone). - Ships as v0.22.0 (minor, DEC-6, operator-approved 2026-07-19). Publishes the full 6-slice consumer-layer cutover milestone.
Out of scope
- Bifrost PROVIDER planes (memory/affect) — hand-rolled, ADR-0009, untouched.
- 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.