Issue #15. Worldtree Phase 2.0 ships Tier 3 (consumer-defined) agents at `<user_id>:<agent_name>`; ratatoskr now exposes their lifecycle via a dedicated module + CLI tool. The picker handles the colon-containing agent_id generically (per issue #8 out-of- scope clause); session creation works unchanged. What was missing was a way to DEFINE / PATCH / DELETE these agents from ratatoskr itself — operators previously had to curl the API directly. ## Public surface (ratatoskr.tier3) Tier3AgentInfo (frozen dataclass) define_agent (client, *, agent_name, system_prompt, model) → Info patch_agent (client, agent_id, *, system_prompt?, model?) → Info delete_agent (client, agent_id) → None Tier3QuotaExceeded — 429 agent_quota_exceeded (50-agent cap) Tier3UserIdUnsupported — 403 tier3_user_id_unsupported Tier3FieldNotMutable — 422 field_not_mutable (PATCH) Tier3LayerDeferred — 422 layer_deferred (define, defense-only) Tier3AgentNotFound — 404 SessionApiFailed (reused) — all other non-2xx Caller-owned httpx.AsyncClient posture (same as ratatoskr.sessions). Module is standalone — does NOT import sessions/sse_client/tui/cli beyond reusing the USER_AGENT constant from cli. ## CLI (python -m ratatoskr.tier3 <subcommand>) define --name <slug> --system-prompt <str> --model <id> patch <agent_id> [--system-prompt <str>] [--model <id>] delete <agent_id> Auth resolution mirrors ratatoskr.cli verbatim — --api-key flag > $WORLDTREE_API_KEY > exit 11. Server URL via --server > $WORLDTREE_API_URL > http://localhost:8000. Exit codes follow the cli.py matrix: 0 / 10 (usage) / 11 (auth) / 20 (api-failure) / 21 (network). ## Real-world finding from live smoke Tier-3 agents do NOT appear in `GET /agents` — the public list filters them out. The picker won't surface tier-3 agents; operators bypass it via `ratatoskr --send "..." --new --agent ratatoskr:<n>` directly. This contradicts the contract's acceptance assumption ("the new tier-3 agent should appear in the list") — caught at smoke time. The picker integration was hopeful; the real shape is "you know your tier-3 agent_id because you defined it." Adding a ratatoskr-side `tier3 list` subcommand would need a Worldtree endpoint that doesn't exist today; surfacing to worldtree-dev as a followup. ## Live lifecycle smoke (personal Worldtree v0.16.2) $ python -m ratatoskr.tier3 define --name smoke-tier3 \ --system-prompt "..." --model qwen3.6-35-a3b → defined ratatoskr:smoke-tier3 (qwen3.6-35-a3b) $ ratatoskr --send "hello via tier-3" --new --agent ratatoskr:smoke-tier3 → [done] turn_id=286 model=qwen3.6-35-a3b duration=14.2s usage 44 in → 390 out (434 total, 0 cached) $ python -m ratatoskr.tier3 delete ratatoskr:smoke-tier3 → deleted ratatoskr:smoke-tier3 $ python -m ratatoskr.tier3 delete ratatoskr:smoke-tier3 → [agent_not_found] ratatoskr:smoke-tier3 (exit 20) The colon-containing agent_id flowed transparently through ratatoskr.sessions.create_session, the SSE stream's text + worker_phase + done events all rendered correctly, and the ratatoskr.sessions module needed zero changes. ## Contract docs/contracts/issues/15.contract.md — new module spec; drift-check clean. Acceptance criterion about "appears in GET /agents" should be amended in a follow-up to reflect the empirical finding. ## Tests +26 tests (264 total GREEN, was 238). Covers all error paths via respx mocking — quota, user_id, layer_deferred, field_not_mutable, 404, 5xx — plus CLI happy + error paths. ruff clean. Minor bump (v0.6.5 → v0.7.0) per SemVer etiquette: new public module + CLI surface; new caller-visible behavior.
Ratatoskr
A Worldtree Conversation API debug TUI. Runs up and down Worldtree's API surface — sessions, turns, persona, tools, admin events, Bifrost state — carrying messages between layers. Like the squirrel.
The product is the observability surface; chat is the input mechanism. Devs run Ratatoskr against a local Worldtree to watch a turn flow through every layer of the system, side-by-side, in one terminal.
Status
v0 scaffold. Design locked; implementation hasn't started. The dev team owns the implementation pass.
Read in this order
docs/design-brief.md— the locked design. Read this first. Every architectural decision is recorded with its rationale, the alternatives considered, and (where relevant) the operator's lock-in moment.docs/SPEC-PIN.md— what Worldtree spec version Ratatoskr is built against, where the vendored snapshot lives, and how to bump the pin.docs/conversation-api-spec.md— the vendored Worldtree spec snapshot. Read this to understand the API surface Ratatoskr consumes. Do not import anything from a Worldtree checkout — the boundary is the spec, not the code. Seedocs/design-brief.md§2.CLAUDE.md— Claude Code conventions for this repo (mostly inherited from the Corviduo template).persistent-memory.md— durable intent across context resets. Update as decisions and state evolve.
Quickstart
# 1. Environment
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
# 2. Verify the spec pin
cat docs/SPEC-PIN.md # documented Worldtree SHA + bump procedure
# 3. Tests (none yet; scaffold only)
uv run pytest
# 4. Run against a local Worldtree (once implementation lands)
# Worldtree must be running: python -m core.conversation_api
ratatoskr --agent mimir
What this repo is NOT
- NOT a polished consumer for Worldtree's end-users — that's the web app.
- NOT a Worldtree admin tool — admin CLI is separate (
sessions_cli.pylives in Worldtree). - NOT a featuretracking shadow of the web app — when the Conversation API surface grows, Ratatoskr does not necessarily grow with it.
- NOT a remote-Worldtree debug client — file-tail surfaces (persona log, server log) assume local-dev posture.
The full negative-clause list lives in docs/design-brief.md §6.
Boundary rule
Ratatoskr depends on three things only:
httpx+httpx-sse(network layer)textual(TUI framework)- Worldtree's published Conversation API spec at the pinned SHA
Hard rule: no imports from a Worldtree checkout. No core.* imports,
no from worldtree.*, no shared models, no submodule of Worldtree. The
spec is the entire surface. Boundary smoke test at tests/test_no_worldtree_imports.py
enforces this in CI.
Version-skew strategy
The dev team is decoupled from Worldtree's dev team. To detect drift when Worldtree changes the API:
- Spec-version pin in
pyproject.toml(worldtree-spec-rev). Bump explicitly; bumps are a tracked action. - Recorded-SSE snapshot tests at
tests/snapshots/. Captured against a live Worldtree; replayed in CI. Re-record after every pin bump. - Conformance smoke test — boots Worldtree via Docker compose in CI, runs a one-turn happy path. Catches integration-level drift.
See docs/SPEC-PIN.md for the bump procedure.
Consumer-side discoveries
If you find a spec gap, ambiguity, or missing-but-needed endpoint while working on Ratatoskr: route the discovery back to worldtree-dev via althing rather than via PR on Worldtree directly. The separate-team boundary is intentional and helps catch spec gaps that an in-tree consumer would paper over.
Related repos
- Worldtree — the API Ratatoskr consumes
- brokkr-smithy — authored this design brief
- Skaldsong — another Worldtree API consumer (Python; render-and-aggregate pattern)
- mead-hall — another Worldtree API consumer (TypeScript; server-side broker)