feat(#20): agents/tier3 family onto the wt adapter + model→role fold (slice-4)
Cut ratatoskr's consumer agent-lifecycle routes over to worldtree-sdk (issue #20 slice-4). Five routes now flow through `ratatoskr.wt` over the SDK's `client.agents.*`, returning open-world dicts and mapping the SDK's undiscriminated `ApiError` floor 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) - `define_agent` → `agents.define` (429→Tier3QuotaExceeded(retry_after=0), 403→Tier3UserIdUnsupported, 422 layer_deferred→…) - `patch_agent` → `agents.patch` (404→Tier3AgentNotFound, 422 field_not_mutable) - `delete_agent` → `agents.delete` (404→Tier3AgentNotFound; NOT hide-existence) Rewired call-sites: the `python -m ratatoskr.tier3` CLI (define/patch/delete) and the web `_agents_endpoint` / `_persona_state_endpoint`, both catching the SDK's `ConnectFailed` transport-failure normalization. Deleted the hand-rolled paths: `sessions.list_agents` / `get_persona_state` / `AgentInfo`, and `tier3.define/patch/delete_agent` / `Tier3AgentInfo` / parse+extract helpers. model→role fold (scope B): the define/patch response echoes `role` (spec 1.2 / b128), read off the open-world dict; `LocalAgentEntry.model`→`.role`, local-index schema v1→2 (old index discarded, no-backwards-compat). The Tier-3 caller-semantic exceptions move to `sessions.py`: running the CLI as `__main__` while `wt` imports `ratatoskr.tier3` bound two copies of each exception class, so a raised `Tier3AgentNotFound` escaped the CLI's `except` as an uncaught traceback. Homing them in `sessions` (never `__main__`) makes the class identity single. The live smoke — not the unit tests, which call `main()` in-process — caught this. Error-map rows + slice-4 notes added to the cutover contract; coverage-map re-anchored. LIVE-SMOKE on personal :8081 (b128): define(thoughtful-character) → patch → list(6 agents) → persona_state(→PersonaNotConfigured mapped) → delete → index empty; non-existent-id patch via `-m` → [agent_not_found] exit 20. Suite 465 green.
This commit is contained in:
@@ -164,7 +164,15 @@ others, they get their own row here — the default is NOT a general "any 404
|
||||
| `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` (any other status/route) — the default** | `SessionApiFailed(status, error_code, body)` |
|
||||
| `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
|
||||
@@ -206,6 +214,32 @@ re-anchor its coverage-map rows.
|
||||
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`→`role` cutover folds in here (scope B).** Worldtree spec 1.2 (`v1.0.0b128`,
|
||||
live on :8080/:8081) made the `/agents/define` response echo `role`, closing the
|
||||
old W-4 `model` echo. The adapter returns the SDK's OPEN-WORLD `DefinedAgent` /
|
||||
`PatchedAgent` dicts verbatim (parity posture); callers read `info["role"]`. The
|
||||
frozen `Tier3AgentInfo` dataclass (which read `body["model"]` and would KeyError
|
||||
post-b128) is DELETED — no dataclass normalization layer survives.
|
||||
- **`ratatoskr.local_agents` schema bump.** `LocalAgentEntry.model` → `.role` (the
|
||||
field stores what the wire now calls a role); `_SCHEMA_VERSION` 1→2 so any
|
||||
pre-cutover on-disk index is discarded cleanly (no-backwards-compat, DEC-3).
|
||||
- **`AgentNotAvailable` name collision.** `sessions.AgentNotAvailable` (persona-state
|
||||
404 `agent_not_available`) and `sse_client.AgentNotAvailable` (eager-turn 409) are
|
||||
distinct types that share a name; `wt` already imports the sse_client one for the
|
||||
stream, so it imports the persona one ALIASED (`PersonaAgentNotAvailable`) and
|
||||
raises it from `get_persona_state`. The web endpoint keeps importing the persona
|
||||
`AgentNotAvailable` from `sessions` (same class), so its `except` is unchanged.
|
||||
- **`ConnectFailed` at every rewired caller (slice-3 foot-gun).** The SDK normalizes
|
||||
ANY transport failure to `ConnectFailed(status=0)` (`request.py`), not a raw httpx
|
||||
error. The rewired tier3 CLI and both web endpoints (`_agents_endpoint`,
|
||||
`_persona_state_endpoint`) catch `wtsdk.ConnectFailed` → their existing
|
||||
network-error surface (CLI exit 21 / web 502). The web `test_network_error_returns_502`
|
||||
(respx `httpx.ConnectError` side-effect) is the RED that proves this.
|
||||
- **`agents.get(agent_id)`** (SDK `GET /agents/{id}`) is NOT wrapped — ratatoskr has no
|
||||
`get_agent` consumer; only list/persona_state/define/patch/delete are in coverage.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Bifrost PROVIDER planes (memory/affect) — hand-rolled, ADR-0009, untouched.
|
||||
|
||||
Reference in New Issue
Block a user