`[2026-07-19]` **worldtree-sdk cutover SLICE-4 COMPLETE + committed (`c62b4ee`+`aed9429`+`477d98f`, v0.21.13–.15, 475 green).** Agents/Tier-3 family onto the `ratatoskr.wt` adapter + the `model`→`role` fold (scope B), full House Code Discipline end-to-end (contract error-map → TDD → rewire → delete → live smoke → coverage re-anchor → both heid gates → fixups). **What moved onto the SDK** (`client.agents.*`, open-world dicts, errors mapped by route+(status,error_code) per INV-CUT-2): `list_agents` (`agents.list`), `get_persona_state` (`agents.persona_state`; 404 persona_not_configured / 404 agent_not_available / 403 auth_scope_denied dual-key), `define_agent` (`agents.define`; 429→Tier3QuotaExceeded(retry_after=0) status-only, 403 tier3_user_id_unsupported, 422 layer_deferred), `patch_agent` (`agents.patch`; bare-404→ Tier3AgentNotFound, 422 field_not_mutable), `delete_agent` (`agents.delete`; bare-404). Rewired the `python -m ratatoskr.tier3` CLI + the web `_agents_endpoint` / `_persona_state_endpoint`. DELETED the hand-rolled `sessions.list_agents` / `get_persona_state` / `AgentInfo` and `tier3.define/patch/delete_agent` / `Tier3AgentInfo` / parse+extract helpers. **model→role fold:** define/patch response echoes `role` (spec 1.2 / b128), read off the open-world dict; `LocalAgentEntry.model`→`.role`, local-index `_SCHEMA_VERSION` 1→2 (old index discarded, no-compat). `project_tier3_agents_model_to_role_pending` is RESOLVED by this — the deferred scope-B work landed here (doing it standalone would have been throwaway). **KEY SLICE-4 FOOT-GUNS (for slices 5-7 + any tier3 work):** - **`python -m` double-module exception identity.** Running tier3 as `__main__` while `wt` imports `ratatoskr.tier3` bound TWO copies of each `Tier3*` class → a raised `Tier3AgentNotFound` escaped the CLI's `except` as an uncaught traceback (exit 1, not the mapped exit 20). Unit tests call `main()` in-process (module is `ratatoskr.tier3`, not `__main__`) so they NEVER hit the split — the LIVE SMOKE caught it. Fix: the `Tier3*` exceptions live in `sessions.py` (never run as `__main__`) → single class identity, and it removed the `wt→tier3` import edge. Rule: caller-semantic exceptions the adapter raises + a `-m` CLI catches must NOT live in the `-m` module. - **Open-world presenters must degrade, never crash.** The heid-bug-hunt panel (5/5/5, two 3/3 crash sites) caught the CLI (`_run_define`/`_run_patch`) and web (`_agents_endpoint`) HARD-INDEXING the open-world dicts (`info["agent_id"]`, `{a["agent_id"] for a in upstream}`) → KeyError/TypeError on a partial/drifted 2xx response (incl. `system_prompt: null` → `None.splitlines()` AttributeError). The wt tests + live smoke used FULL server dicts, so it never surfaced. Fix: `_str_field` (absent/null/non-str → default) in the CLI; well-formed-mapping filter in the web endpoint; a no-agent_id 2xx → controlled exit 20. The invariant "open-world reads degrade, never crash the presenter" must hold at EVERY presenter, not just the adapter. **Both heid gates cleared:** - **code-review (panel, zero adapter/error-map/model→role drift):** 3 fixups (v0.21.14, `aed9429`) — persona endpoint now catches `wt.SessionApiFailed` (parity with 3 sibling endpoints; a latent PRE-cutover gap, NOT a slice-4 regression); dual-key NEGATIVE tests for define/patch + flat-`field` test; contract documents the `":" in agent_id` PRE. - **bug-hunt (panel 5/5/5, no false positives):** 3 fixups (v0.21.15, `477d98f`) — the two open-world presenter crash sites above + `_error_field_from_body` type-checks `field` is str. HELD (contract-intended, Heid-confirmed): the 429→quota / bare-404→not-found status-only maps (the arms flagged them spec-free; the § Error map specifies them; the SDK's ApiError floor drops Retry-After so retry_after=0 is canonical). Dual-keying define's 429 for full row consistency is an available tightening (contract amendment), surfaced not applied. **LIVE-SMOKE on personal :8081 (b128):** valid agent roles are `thoughtful-character` / `character` / `assistant` (NOT the `/models/available-for-characters` `char-rp` — that's a character-model, a different vocab; define 422s on it). define→patch→list(6 agents: forseti/lofn/mask/mimir/vili/…)→persona_state(→PersonaNotConfigured mapped)→delete→index empty; non-existent-id patch via `-m` → `[agent_not_found]` exit 20 (double-module fix proven). Throwaway `ratatoskr:slice4-smoke` deleted, server left clean. **NEXT = slice-5** (characters + me/capabilities/models — the remaining consumer reads); then slice-6 (admin: `stream_admin_events` + `get_session_bifrost`, admin_auth), slice-7 (teardown: delete residual hand-rolled, drop `httpx-sse`, retire contracts #2/#15, MINOR bump per DEC-6 w/ operator approval). Consumer layer ONLY; Bifrost provider planes untouched.