Package and dependency choices for the Python game engine. FastAPI + uvicorn, pydantic v2, pytest, OTEL.
Last updated: 2026-06-08 (11 live genre packs; game/ ~70+ modules)
| Concern | Choice | Notes |
|---|---|---|
| Python | 3.12+ | Required by pydantic v2 typing, Annotated discriminators |
| Build backend | hatchling |
declared in pyproject.toml; no compile step |
| Environment manager | uv |
Fast lockfile resolution; uv run for entry points |
| Test runner | pytest + pytest-asyncio |
asyncio_mode = "auto" for async fixtures |
| Lint / format | ruff |
ruff check and ruff format — one tool |
| Type checker | pyright |
Strict on protocol + game modules |
Declared in sidequest-server/pyproject.toml:
| Concern | Package | Minimum | Notes |
|---|---|---|---|
| HTTP / WS framework | fastapi |
0.110 | Router, dependency injection, pydantic integration |
| ASGI server | uvicorn[standard] |
0.29 | uvicorn sidequest.server.app:app --reload in dev |
| Models / validation | pydantic |
2.6 | Discriminated unions, protocol payload validation |
| YAML | pyyaml |
6.0 | Genre pack loading |
| WebSockets | websockets |
12.0 | Underlies FastAPI WS handler; direct usage for tests |
| Narrator transport | anthropic (Python SDK) |
0.34 | Default narrator backend per ADR-101; tool-use + prompt caching |
| Observability | opentelemetry-api + opentelemetry-sdk |
1.24 | Span catalog ported from the Rust tree verbatim; native tool-registry spans per ADR-103 |
| HTTP client (tests) | httpx |
0.27 | FastAPI's recommended test transport |
| Persistence driver | psycopg + psycopg_pool |
3.1 | psycopg3 (sync) against PostgreSQL, pooled per ADR-115 |
| Migrations | alembic |
1.13 | Owns DDL via raw SQL op.execute; no ORM models |
Persistence is PostgreSQL via psycopg (psycopg3, sync) + psycopg_pool.ConnectionPool per ADR-115 (single shared database, sessions keyed by session_slug/session_id; supersedes the per-session sqlite3 files). SIDEQUEST_DATABASE_URL is a hard runtime requirement — no silent default (fail-loud). DDL is owned by Alembic (0001_initial_unified_schema, 0002_asset_ledger), which applies raw SQL via op.execute — there are no ORM models. Game state stays largely document-shaped (JSON columns), now stored in Postgres rather than stdlib sqlite3. The standard library still covers subprocess (asyncio.create_subprocess_exec), filesystem, regex, time, random, and UUID.
sidequest-server/
├── pyproject.toml # [project], deps, entry points, ruff config
├── sidequest/
│ ├── protocol/ # GameMessage discriminated union, 44 message types, sanitization
│ ├── genre/ # YAML genre pack loader, pydantic models, 11 live packs
│ ├── game/ # ~70+ modules — state, combat, NPCs, lore, audio direction
│ ├── agents/ # Anthropic SDK narrator (default) + claude -p/Ollama opt-in, auxiliary agents
│ ├── server/ # FastAPI app, session management, dispatch, watcher
│ ├── daemon_client/ # Unix socket client for Python media daemon
│ ├── telemetry/ # OTEL span helpers + watcher event infrastructure
│ └── cli/ # CLI entry points (pyproject.toml [project.scripts])
└── tests/ # pytest test suite
Dependency graph:
sidequest.server
├── sidequest.agents
│ └── sidequest.protocol
├── sidequest.game
│ ├── sidequest.protocol
│ └── sidequest.genre
├── sidequest.daemon_client
└── sidequest.protocol
The composition mirrors the prior Rust crate layout 1:1 (per ADR-082) — any feature, span, or test can be compared across historical trees by path.
The narrator LLM path uses the Anthropic Python SDK by default per ADR-101 (supersedes ADR-001): prompt caching on stable system zones, native tool-use for structured output (ADR-102), and per-call model routing (Haiku 4.5 classification/scratch, Sonnet 4.6 narration, Opus 4.8 declared-important moments). ANTHROPIC_API_KEY is a hard runtime requirement on narrator paths (fail-loud, no silent fallback). Backend is selected by SIDEQUEST_LLM_BACKEND (default anthropic_sdk) in agents/llm_factory.py; claude -p (claude_client.py) and Ollama (ollama_client.py) remain opt-in non-default backends, and claude -p still serves some non-narrator jobs (e.g. the daemon's subject extraction and the dungeon "curate" stage).
Active agent types:
- Narrator — unified prose generation (exploration, dialogue, combat, chase). Unified per ADR-067; stateless turns per ADR-098 (each turn is a bounded, self-contained call — no
--resume, no persistent session; supersedes ADR-066's persistent-Opus model). On the SDK backend, structured output is native tool-use (ADR-102), not prose extraction. - IntentRouter — state-override classification (in_combat → Combat, in_chase → Chase, default → Exploration). Single Haiku call, no narrator entanglement.
- AsideResolver (ADR-107) — out-of-band OOC questions, separate Haiku call site, does not consume a turn or fire the multiplayer barrier.
- WorldBuilder / Troper — auxiliary specialists for offline corpus and world materialization. On the live turn path their work is now folded into the unified narrator via tool-use; the named modules survive for offline/CLI use.
Historical multi-agent dispatch (CreatureSmith, Ensemble, Dialectician — ADR-010) is superseded by ADR-067; their references remain in the AgentKind enum but do not dispatch.
The opt-in claude backend wraps a CLI subprocess (claude_client.py), kept for daemon-side subject extraction and the dungeon "curate" stage (ADR-106), plus as a narrator fallback when explicitly selected:
proc = await asyncio.create_subprocess_exec(
"claude", "-p", prompt,
"--system", system_prompt,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)The narrator outputs prose only — no JSON blocks. On the default anthropic_sdk backend, mechanical state changes (mood, intent, items, quests, SFX, resource changes, personality events, scene renders) are produced as native SDK tool calls (ADR-102) — the narrator structurally cannot describe a mechanical effect without invoking the matching tool. The legacy ADR-039 fenced-JSON sidecar survives only on the opt-in, default-off SIDEQUEST_NARRATOR_STREAMING=1 claude -p path. Tool results are validated and merged in assemble_turn with tool values taking precedence. NPC and encounter data is pre-generated server-side and injected into <game_state> as world facts (ADR-059: Monster Manual).
Player input is sanitized at the protocol layer before reaching any agent prompt (ADR-047).
Two save-side telemetry sinks landed in May 2026 to give the GM-panel "lie-detector" mechanical ground truth (CLAUDE.md: OTEL Observability Principle):
turn_telemetry(Phase 1) — one row per turn carrying narrator back-pressure metrics (model, latency, token counts, tool-call counts, cost). Written inside the sameNARRATIONPostgres transaction (one pooled connection per turn, per ADR-115) so per-turn metrics never desync from the narration event they describe.mechanical_census(Phase 2) — pure canonical-state projections (edge / xp / inv / trope baselines, plus per-round mechanical-diff lanes: baseline / static / moved / absent). Per-seated-PC isolation; session-level trope. Hot-path cost-guarded — one turn writes N=1 census rows.mechanical_strip/fold_mechanical_stripship as Phase-3 forward seams (annotated, not wired into a consumer yet).
The save-forensics viewer reads both tables through PgForensicReader.
Under Postgres (ADR-115) reads are MVCC snapshots that never block writers,
so the old save-clobber discipline (the ?mode=ro workaround for
SqliteStore.open() writing on construction) is gone.
The ML inference stack is a separate Python service. sidequest.daemon_client communicates over Unix socket with newline-delimited JSON-RPC (ADR-035).
| Subsystem | Stack | Notes |
|---|---|---|
| Image generation | Flux.1 / Z-Image Turbo, MLX | Apple Silicon target (ADR-070). Z-Image Turbo is the active renderer; prompting guide at sidequest-content/PROMPTING_Z_IMAGE.md |
| Music library | Pre-rendered ACE-Step tracks | Build-time generation, runtime library playback |
| Audio mixing | pygame mixer | Music + SFX (no voice/TTS channel after 2026-04 removal) |
| Scene interpretation | Pattern matching | Narrative text → stage cues |
| Subject extraction | Claude CLI | Prose → visual descriptions |
See ADR-035 for the Unix socket IPC architecture and ADR-046 for GPU memory budget coordination.
| Concern | Choice | Notes |
|---|---|---|
| Framework | React 19 | Concurrent features for streaming narration |
| Bundler | Vite | HMR for dev, optimized build |
| Language | TypeScript | Strict mode |
| Styling | Tailwind CSS + shadcn/ui | Genre theming via CSS variables (ADR-079) |
| Testing | Vitest | Unit + component tests |
| Audio | Web Audio API | Two-channel playback (music + SFX) per current ADR-045 state |
| State management | useStateMirror (custom) |
Delta replay from server (ADR-026) |
| 3D dice | Three.js + Rapier | Overlay with genre-themed skins (ADR-075) |
Not used and not planned:
Reversal (ADR-115, 2026-05-26): PostgreSQL and Alembic were previously listed here as deliberate omissions ("SQLite is sufficient"). The SQLite-per-session model was migrated to a single PostgreSQL database and Alembic now owns migrations — see Core Dependencies above. Both are adopted, not omitted.
- SQLAlchemy / ORM — game state is document-shaped; repositories use raw
psycopg(psycopg3) against Postgres with typed row factories, and Alembic applies raw SQL. Still no ORM models. - asyncpg — the adopted Postgres driver is
psycopg(psycopg3, sync) +psycopg_pool, not asyncpg; async handlers offload viaanyio.to_thread(ADR-115) - protobuf / msgpack — JSON over WebSocket is the protocol; no binary serialization needed
- gRPC — Unix-socket JSON-RPC is the daemon contract (ADR-035)
- Redis / external cache — in-process caches are adequate at target scale
- Celery / task queue — asyncio tasks and background subprocesses cover all async work
- mypy — pyright chosen for speed and strict-mode ergonomics
The backend was originally Python (archived as sq-2), briefly Rust (sidequest-api, ~2026-03-30 to 2026-04-19, 12-crate cargo workspace on tokio + axum + serde + rusqlite), and is now Python again per ADR-082. The Rust tree no longer exists on disk. Design carried forward: crate boundaries → package boundaries, serde structs → pydantic models, tracing spans → OTEL spans (verbatim catalog), cargo test → pytest. The typed-protocol discipline survives intact; the language does not.