vh 1228c37e6f feat(web): in-browser debug companion — issue #16 (v0.15.0)
Browser-based debug companion to the Ratatoskr TUI, reusing the
existing wire-layer modules unchanged. Same five surfaces (transcript,
thinking, tools, debug, persona) over the same Worldtree Conversation
API SSE wire, viewable from any device on the operator's LAN.

Per docs/contracts/issues/16.contract.md (full v2.1 module contract
with 11 FN blocks + 9 invariants + Heid panel review pass merged).

Architecture:
- New module `ratatoskr.web` with `server.py` (Starlette app, ~250 LOC),
  `entrypoint.py` (lazy-import gate, ~100 LOC), `static/index.html`
  (single-page vanilla JS UI, ~360 LOC)
- Optional-deps group `[web]` = starlette + uvicorn[standard]; dev
  pulls these in transitively
- New console script `ratatoskr-web`
- Streaming via browser-native `EventSource` GET; prompt-submit is a
  separate POST (load-bearing Hulda finding from R13 panel — EventSource
  is GET-only)
- Small in-memory turn registry maps (session_id, turn_id) → upstream
  request handle for cancel + browser-disconnect cleanup

Endpoint surface (9 routes):
- `GET /` → static index.html
- `GET /static/*` → static assets
- `GET /version` → {"ratatoskr": "<version>"}
- `GET /api/agents` → upstream /agents + local Tier 3 merge
- `POST /api/sessions` → upstream POST /sessions
- `GET /api/agents/{id}/persona_state` → upstream persona-state
- `POST /api/turns/{sid}` → allocate turn_id, register in turn registry
- `GET /api/turns/{sid}/stream?turn_id=N` → proxy upstream SSE to browser
- `POST /api/turns/{sid}/cancel?turn_id=N` → upstream cancel

Trust model: internal LAN debug surface. Binds 0.0.0.0:8765 default;
no auth, no CORS guard (operator direction). What stays disciplined
regardless of network trust:
- Transcript HTML-escapes assistant content (INV-004 — model output
  is untrusted text; adversarial HTML must not execute in browser)
- Upstream API key never reaches browser DOM (INV-003 — proxy-only)

Lifecycle:
- Browser disconnect mid-stream → upstream cancel (INV-005;
  asyncio.CancelledError caught in stream handler)
- Server Ctrl-C → lifespan shutdown drains turn registry within 5s
  budget (INV-006; structured-log line on timeout)

Tests (37 new, 356 total; previous 319 baseline preserved):
- tests/test_web_server.py (23 cases): endpoint contract via Starlette
  TestClient + respx mocks; covers each endpoint, browser-disconnect →
  upstream cancel, lifespan shutdown draining the registry
- tests/test_web_presentation_contract.py (11 cases): proxy
  serialization matches tests/fixtures/presentation_contract.json
  for one of each Event type — drift detection between server-side
  serializer and the JS presenter without forcing a shared abstraction
- tests/test_web_packaging.py (4 cases): static asset packaging via
  importlib.resources; AST-checked lazy-import discipline (no top-
  level starlette/uvicorn import in entrypoint.py); missing-API-key
  exit-11 path; missing-extras exit-12 path

Provenance:
- Scope v1 → Heid panel review (Gróa + Hulda, R13) → 8 load-bearing
  corrections (POST→GET split, Starlette > FastAPI, lazy-import
  discipline, browser-disconnect → upstream cancel, presentation-
  contract fixture, error event contract, static-asset packaging,
  escaped plain-text Markdown deferred) merged into scope v2
- Operator direction: internal-LAN debug surface; auth + CORS
  deliberately omitted

Not yet (deferred to v0.16.x+):
- Cross-reload session resume via Last-Event-ID
- Tier 3 lifecycle UI (define/patch/delete in browser)
- Markdown rendering with vendored safe-subset renderer
- TLS + real auth (only if a non-LAN use case ever surfaces)
2026-05-27 19:03:50 -07:00

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

  1. 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.
  2. docs/SPEC-PIN.md — what Worldtree spec version Ratatoskr is built against, where the vendored snapshot lives, and how to bump the pin.
  3. 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. See docs/design-brief.md §2.
  4. CLAUDE.md — Claude Code conventions for this repo (mostly inherited from the Corviduo template).
  5. 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.py lives 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:

  1. Spec-version pin in pyproject.toml (worldtree-spec-rev). Bump explicitly; bumps are a tracked action.
  2. Recorded-SSE snapshot tests at tests/snapshots/. Captured against a live Worldtree; replayed in CI. Re-record after every pin bump.
  3. 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.

  • 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)
S
Description
No description provided
Readme 20 MiB
Languages
Python 82%
HTML 17.6%
Shell 0.4%