5bc39a092eea577f1027ff7a73e005e45c097322
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
deab7627eb |
feat(#20): characters + me/capabilities/models onto the wt adapter (slice-5)
Slice-5 of the worldtree-sdk cutover: migrate the remaining consumer READS + transient-character CRUD off the hand-rolled httpx wrappers onto the `ratatoskr.wt` adapter over the SDK, and delete the retired path. Adapter (`wt.py`): add `get_me` / `get_capabilities` / `list_character_models` / `create_character` / `get_character_state` / `delete_character` over `client.me` / `client.capabilities` / `client.models` / `client.characters.*`. All six are open-world reads/acks returned verbatim; none carries a discriminated SDK error, so each maps any `ApiError` → the `SessionApiFailed` default (INV-CUT-2) — exact parity with the retired path. No new Error-map rows. Decisions (contract § slice-5 notes): `create_character` omits `state` when None (SDK-idiomatic inline literal, server-equivalent to the retired explicit null); `delete_character` returns the SDK's open ACK verbatim (`-> Mapping|None`, not normalized to None). CLI rewire (`cli.py`): `--whoami` (me + capabilities) and `--characters` (models → create → state → delete) build a `wt.build_client` over the injected probe transport and catch `wt.SessionApiFailed` + `ConnectFailed`. Open-world degrade-not-crash carried (cumulative cutover foot-gun): `_characters_probe` reads `items` null-safe and extracts `character_id` defensively (clean abort, no hard-index KeyError); `_format_whoami` widened to `Mapping`. Deleted the six hand-rolled `sessions.py` wrappers (net -5 mypy no-any-return); `endpoint_for_plane` + `get_session_bifrost` (slice-6) + the exception classes stay. Retired the corresponding `test_sessions.py` classes; added the slice-5 adapter tests + a CLI malformed-create-abort test. LIVE SMOKE (:8081, b128) — INV-CUT-5 / DEC-4 cleared: `--whoami` rendered real identity + capabilities; `--characters` drove the full lifecycle end-to-end (char-rp catalog → created char_8c00006e… → PAD read-back → deleted). Suite 483 green; ruff clean; mypy at the 2 pre-existing baseline errors. Patch bump 0.21.15 → 0.21.16 (the cutover MINOR is deferred to slice-7, DEC-6). |
||
|
|
477d98f52e |
fix(#20): heid-bug-hunt fixups — open-world presenter degrade-not-crash (slice-4)
Panel (Gróa+Hulda+Regin, 5/5/5, no false positives) confirmed two 3/3 crash
sites where open-world dict reads violate the declared "degrade, never crash the
presenter" invariant — the wt adapter tests + the live smoke used full server
dicts, so partial/drifted wire responses were never exercised:
- FIX (tier3.py _run_define/_run_patch): the CLI hard-indexed the open-world
define/patch dicts (`info["agent_id"]` / `["role"]` / `["agent_name"]`), so a
partial 2xx → KeyError escaping main()'s exit matrix as a raw traceback (exit 1);
and `make_description(info.get("system_prompt", ""))` fed None to .splitlines()
on a present-but-null field → AttributeError. Now reads via `_str_field` (absent/
null/non-str → default), degrades role to '?', indexes only a well-formed identity,
and maps a no-usable-agent_id 2xx to [api_failed] exit 20 (controlled, not a crash).
- FIX (web/server.py _agents_endpoint): the upstream dedup hard-indexed each item
(`{a["agent_id"] for a in upstream}` + `_as_dict`), so a malformed item (`[{}]`,
`["str"]`, `{"name":…}`, non-str agent_id) or a non-list envelope → 500 before the
local fallback merged. Now filters to well-formed mappings first; a non-list
upstream degrades to the local-only list.
- FIX (wt.py _error_field_from_body): type-check the parsed `field` is a str (the
exception surface is `field: str | None`, the CLI prints it) — restores the retired
hand-rolled `_extract_error_field` isinstance guard.
Held (triaged, no change): the 429→Tier3QuotaExceeded / bare-404→Tier3AgentNotFound
maps are ungated-by-error_code BY CONTRACT DESIGN (§ Error map route+status rows; the
SDK's ApiError floor drops Retry-After, so retry_after=0 is canonical) — the arms
flagged them spec-free; Heid's source-check confirmed intended. Dual-keying define's
429 for full row consistency is an available tightening (contract amendment), surfaced
not applied. The persona-endpoint SessionApiFailed gap the arms also caught was
already closed in the prior code-review fixup (
|
||
|
|
aed942972f |
fix(#20): heid-code-review fixups — persona-endpoint SessionApiFailed parity (slice-4)
Panel (Gróa+Hulda+Regin) returned zero adapter / error-map / model→role drift; three actionable items triaged as genuine adds: - FIX: `_persona_state_endpoint` now catches `wt.SessionApiFailed` and returns the `session_api_failed` envelope with the upstream status, for parity with `_agents_endpoint` / session-create / admin (2/3 arms flagged it; it was the lone sibling letting an unmatched upstream ApiError escape as a raw 500). Confirmed NOT a slice-4 regression — the pre-cutover persona endpoint had the same latent gap — but closed here since the endpoint's error surface is already being hardened (it gained the ConnectFailed catch this slice). - TESTS: dual-key NEGATIVE rows — a wrong error_code at the same status defaults to SessionApiFailed for `define_agent` (403, 422) and `patch_agent` (422); plus the flat-`field` body-parse shape for `_error_field_from_body` (only the nested detail.field form was exercised). Closes the assertion-symmetry gap with the persona route's existing negative test. - AMEND: contract slice-4 notes document the intentional client-side `":" in agent_id` PRE on patch/delete (a Tier-3 id is always <user>:<name>, ADR-0019). Suite 470 green (+5). |
||
|
|
c62b4eecb3 |
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. |
||
|
|
fc256bbaa4 |
fix(#20): heid-bug-hunt fixups — probe ConnectFailed + adapter finite-PAD (slice-3)
The slice-3 heid-bug-hunt panel (3/3) caught a real regression the cutover introduced, plus a chokepoint-invariant gap: - ConnectFailed escaped both rewired CLI probes. When --set-persona-pad and --seed-first-message moved off raw httpx onto the wt adapter, transport failures changed class: the SDK normalizes any pre-response transport error to worldtree_sdk.ConnectFailed (request.py), a WorldtreeError (not ApiError), so it passed the adapter unmapped AND the probes' httpx-only except tuples → an uncaught traceback instead of the graceful [network_error] exit 21. _amain (slice-2) already handled it; the probes lagged. Fix: add ConnectFailed to both probe except tuples (mirrors _amain). Live-verified at a refused host → [network_error] exit 21. - Finite-PAD enforced only at the CLI, not the adapter chokepoint. wt.set_persona_state delegated finiteness to the caller (documented), so a direct/non-CLI caller passing nan/inf got a raw SDK ConfigurationError. Fix: assert finiteness in the adapter precondition (consistent with its other precondition asserts) so the invariant holds at the chokepoint in ratatoskr's own terms; the CLI pre-check stays for the friendly usage error. Triaged-and-declined (all correct per the panel + Heid's source-check): the deleted sessions.py exports (intended no-shim cutover, zero un-migrated importers), the session["session_id"] index (accept-known-risk, matches --new), and Regin's "web indefinite block" (refuted — the seed is asyncio.wait_for-bounded). The concurrent heid-code-review panel returned zero drift, no code change. TDD: 3 RED tests (both probes' ConnectFailed → exit 21; adapter nan/inf/-inf → AssertionError, never reaches the SDK) → GREEN. Suite 469; ruff clean; mypy no new errors. |
||
|
|
ca9a339050 |
feat(#20): persona + authored-history + first-message onto the wt adapter (slice-3)
Slice-3 of the worldtree-sdk cutover: migrate the session persona-state write, the #347 authored-history write, and get_session_messages onto ratatoskr.wt, and route the first-message preset seed through the adapter. Retire the last hand-rolled sessions.py paths the --seed-first-message probe kept alive (create_session + SessionInfo, set_persona_state, write_authored_history, get_session_messages, _bifrost_error_from). - wt.set_persona_state (SDK PadState) — the CLI passes three finite PAD axes; the SDK owns the {"pad": {...}} wire (#317). No route-specific error row → the SessionApiFailed default. - wt.write_authored_history (SDK write_history) — v1 author=assistant; 404 → AuthoredHistoryUnavailable (hide-existence; the route is the discriminator, never the body); every other ApiError → the default. Drops the unused author/effects/claimed_original_at params (no caller uses them). - first_message.seed_preset_first_message now takes a WorldtreeClient and routes through wt.write_authored_history; the best-effort invariants (INV-001..004, never-raise/never-block/one-write/zero-worldtree-source-import) are unchanged. Tests drive a fake WorldtreeClient — the wire is the SDK's to prove. - CLI --set-persona-pad / --seed-first-message + the _amain and web create-path first-message seeds rewired onto the adapter. --set-persona-pad pre-validates PAD finiteness (clean usage_error, never a crash on the SDK ConfigurationError). LIVE-SMOKE on personal :8081 (b128, INV-CUT-5): --seed-first-message → 201 (seq=0, phase=seeded) → read-back verbatim; --set-persona-pad → 204; the --new create-path preset seed observed routing through the adapter. All slice-3 route families proven end-to-end through the ratatoskr surface. docs/coverage-map.md + first_message.contract.md re-anchored onto the adapter; the slice-2 create/stream/cancel rows re-anchored too (they still named the deleted sse_client/sessions symbols). Suite 466 green; mypy no new errors (baseline 22 → 20 in the touched modules); ruff clean. INV-CUT-1..5 held. Bifrost provider planes untouched. |
||
|
|
aba17304bd |
fix(#20): heid-bug-hunt fixups — cutover edge-path robustness (slice-2)
Triaged the heid-bug-hunt panel (Gróa 8 / Hulda 6 / Regin 6; Heid source-checked + refuted 2 Regin FPs). The lens pulled real weight — confirmed bugs the conformance review structurally could not see. Confirmed bugs fixed: - SessionRetired (410) stream-open maps to wt.SessionApiFailed, but neither cli _run_turn nor web gen() caught it → crash / dropped SSE stream. Both presenters now catch it (cli → exit 20; web → labeled `event: error`). (Gróa#2) + cli regression test. - cli forwarded consumer_key unconditionally; an UNBOUND create with the env key set would auth as the Bifrost consumer, not the default bearer. Guarded in the adapter (consumer_key only when bifrost is set). (Gróa#4 + Regin#4) + test. - cli _turn_id_from_sse_id crashed on a None/non-str sse_id (web guarded, cli didn't) → now tolerant. (Gróa#1 + Hulda#2) + test. - _cancel_and_log broadened to `except Exception` — after the code-review's ApiError default, a cancel could raise SessionApiFailed it didn't catch, breaking INV-009 (never-raise). (Gróa#3, Heid-endorsed over Regin's refuted mechanism). Open-world degrade-not-crash (contract posture): render hardened — float duration_ms (_format_duration_safe), non-mapping usage/snapshot guards, unknown event type degrades instead of asserting (Gróa#5/#6 + Hulda#3); web _event_to_browser_payload guards a non-mapping `raw` (Hulda#4); web _wt_client bearer extraction is now case-insensitive + whitespace-robust (Hulda#5 + Regin#5). + render-degrade test. Rejected (verified): Regin#1 (httpx IS caught), Regin#2 (wtsdk IS worldtree_sdk), Regin#3 (sse_client.AgentNotAvailable IS caught by SseConnectFailed) — all FPs; Hulda#1 (deleted funcs "break callers") — grep-verified zero callers pre-deletion. Accepted-known-risk: lenient sse_id parse, CancelFailed status=0, async-gen aclose (pre-existing pattern, not a cutover regression). Suite 497 green; wt/cli/web ruff + wt mypy clean. Patch. |
||
|
|
74d41eb559 |
fix(#20): heid-code-review fixups — INV-CUT-2 completeness on cancel/stream (slice-2)
Triaged the heid-code-review panel (Gróa + Hulda substantive, Regin zero=weak).
Adopted (genuine adds):
- cancel_turn + stream_turn gain a defensive `except ApiError -> SessionApiFailed`
default after their discriminated branches. INV-CUT-2 ("every ApiError is mapped;
default SessionApiFailed") now holds STRUCTURALLY on those routes, not by coupling
to the SDK's internal guarantee that it maps them to discriminated types. + tests.
- get_session_tools error-path test (symmetric with messages).
- Contract § Error map amended: added the stream ProtocolError rows
(Malformed*/TurnIdFlip -> ratatoskr same-named), clarified the cancel row (the SDK
RAISES the typed races -> ratatoskr exceptions, only a 200/cancelled=False is a
CancelResult; caller surface stays exception-based per DEC-2), and noted the
ApiError default holds on stream+cancel too.
Rejected (category-5, wrong-grounding) — 2/3 arms flagged create's bound-502 as
"should gate on error_code like list's 422+cursor_invalid". Verified against the SDK
parser (not in the arms' file set): the bound-502 body is
{"error_code":"bifrost_handshake_failed","detail":{"bifrost_error":...}}, and the
SDK's envelope parser PREFERS the nested detail (which lacks error_code), so
ApiError.error_code resolves to "unknown" — gating would REGRESS handshake detection
(the cli/web integration tests caught it). INV-002 also makes the handshake the sole
bound-502 cause. Kept the any-bound-502 mapping; documented WHY in code + contract.
Accepted-as-is: create_session -> Mapping annotation (intentional open-world
passthrough, already documented in the route-map note; category 3).
Suite 493 green; wt.py mypy + ruff clean. Patch.
|
||
|
|
b907a7b8a5 |
feat(#20): stream + cancel adapter routes complete the wt surface (slice-2, part 2a)
Completes the adapter's session/turn surface, still additive and non-breaking (no surface rewired, no hand-rolled path deleted — the cli/web rewire + deletions + live smoke are part 2b). - stream_turn: drives the SDK's resilient stream (auto-resume absorbs the old reconnect_turn) and yields SDK TurnEvents, re-wrapping the stream's TERMINAL SDK errors into ratatoskr's caller-semantic exceptions per DEC-2 (SessionRetired → SessionApiFailed; AgentNotAvailable / TurnLaunchUnavailable / MalformedSse* / TurnIdFlip → ratatoskr's same-named types; ConnectionDropped → SseConnectionDropped; ConnectFailed / terminal ResumeError → SseConnectFailed). The presenter keeps catching ratatoskr types (part 2b aligns the except clauses). - cancel_turn: returns the SDK CancelResult (a 200 cancelled=False is the benign late-cancel race, B-CAN-3), mapping the typed cancel races onto ratatoskr's CancelTurnNotFound / CancelAlreadyCompleted / CancelFailed. - SseConnectionDropped.last_seen_sse_id widened to SseId | str | None: the SDK's resume cursor is a raw composite-id str (the cutover's target form); the hand-rolled path's SseId stays accepted until it is deleted. The one live reader (stream_turn_resilient) generalizes cleanly — a str cursor is already the id. Suite 570 green (555 + 15); wt.py + sse_client.py mypy + ruff clean. Patch. |
||
|
|
bb158ae47d |
feat(#20): sessions read/create adapter routes — ratatoskr.wt (slice-2, part 1)
First slice-2 increment: the presenter-independent sessions routes, additive and non-breaking (no surface rewired, no hand-rolled path deleted yet — the cli/web rewire + deletions + live smoke land in part 2). - create_session / list_sessions / get_session_messages / get_session_tools over WorldtreeClient.sessions.*, each building the request from ratatoskr's domain params and mapping the SDK's ApiError floor by ROUTE (INV-CUT-2): create 404 → AgentNotFound, bound 502 → BifrostHandshakeFailed, list 422 cursor_invalid → InvalidCursor, else the SessionApiFailed default. - Open-world reads returned VERBATIM (parity-pass posture): the routes return the SDK's open dicts, not ratatoskr's typed SessionInfo/SessionPage — those typed result shapes retire when the presenters are rewired to read mappings (adopt the dep's canonical open-world way, reference-impl doctrine). - Transitional: wt imports the caller-semantic exceptions + BifrostBinding from the retiring sessions module (one-way, no cycle); they relocate into the adapter as their call-sites are rewired. - Cancel + the resilient turn STREAM are deferred to part 2, where they wire into the async presenter loop and are validated by the live smoke. Suite 555 green (541 + 14); mypy strict + ruff clean. Patch (internal, additive). |
||
|
|
12cd8642fa |
feat(#20): worldtree-sdk adapter foundation — ratatoskr.wt (slice-1)
Slice-1 of the SDK cutover (docs/contracts/worldtree_sdk_cutover.contract.md): the adapter chokepoint onto worldtree-sdk 1.0.0, unit-tested but not yet wired to any surface (that is slice-2). - build_client(base_url, *, api_key, admin_key=None, transport) constructs the single WorldtreeClient over a ratatoskr-owned injected httpx.AsyncClient. INV-CUT-1: the SDK is given the transport (_owns_client=False) and never closes it — proven by a test asserting aclose() leaves ratatoskr's transport open. - translate_error implements the § Error map DEFAULT: SDK ApiError → the adapter's SessionApiFailed (carrying the SDK's parsed status/error_code/body); every discriminated WorldtreeError subclass passes through by identity. Route-specific rows land at their call-sites in later slices (the route is the discriminator). - SessionApiFailed gains error_code vs the retiring sessions.py copy (extends it per the contract error-map row); the two coexist transiently and reconcile in slice-2 (DEC-4 incremental cutover — nothing wires the adapter this slice, so they never meet at runtime). Deletes no hand-rolled path, so DEC-4's live-smoke bar does not apply yet. Suite 541 green (534 + 7 new); mypy + ruff clean. Patch (internal foundation; the cutover's minor bump is DEC-6 at slice-7 ship). |