rowboat/AGENTS.md
Ramnique Singh 32e02d588c feat(x): agent snapshot inheritance, session inspection, runtime docs
Third application of the reference mechanism: session turns whose
system prompt + tools are byte-identical to the context predecessor's
materialized snapshot persist { agentId, model, inheritedFrom } instead
of ~70KB per turn (measured: turn 2 of a session is now ~1.1KB total).
Inheritance is decided at createTurn by equality against the
materialized predecessor; the model stays concrete, and on
materialization the inherited record's own agentId/model win — a rule
the new test matrix caught as a real bug (the chain base's model was
overriding a mid-session model switch). Reducer invariants: inherited
snapshots must reference the context predecessor; tool identity arrives
via invocation events.

Test matrix per review: prompt-diff -> full snapshot, tools-diff ->
full snapshot, model-switch -> inherits with concrete model, multi-hop
chains materialize, standalone turns never inherit, unreadable
predecessor falls back to full, cyclic inheritance is corruption,
sessions denormalize the model from inherited snapshots.

The inspector now handles sessions too (auto-detected): overview with
per-turn status/size/input preview, --turns to cascade full turn
inspection. Documented in a new repo-root AGENTS.md (storage layout,
reference model, inspector usage, invariants) with a CLAUDE.md pointer.

Breaking for dev turn files: wipe ~/.rowboat/storage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:20:44 +05:30

74 lines
3.5 KiB
Markdown

# AGENTS.md — Working on the Rowboat runtime
Context for AI coding agents (and humans) working on this repo. General
codebase orientation lives in `CLAUDE.md`; this file covers the **new
turn/session runtime** in `apps/x` — its storage, its debugging tools, and
the invariants you must not break.
## The runtime in one paragraph
Chats are **sessions**; each user message starts a **turn**. Both are
append-only JSONL event logs under `~/.rowboat/storage/{turns,sessions}/YYYY/MM/DD/`.
All state is derived by pure reducers (`reduceTurn`, `reduceSession` in
`@x/shared/src/turns.ts` / `sessions.ts`) shared byte-for-byte between the
main process and the renderer. Design specs:
`apps/x/packages/core/docs/turn-runtime-design.md` and `session-design.md`
read the relevant spec before changing runtime behavior.
## Storage is reference-based — files store each fact once
Three applications of the same mechanism:
1. **Context**: a session turn's `context` is `{ previousTurnId }`; the
conversation prefix is materialized by walking the chain
(`TurnRepoContextResolver`).
2. **Model requests**: `model_call_requested.request.messages` is a list of
string refs into the turn's own events — `"context"`, `"input"`,
`"assistant:<index>"`, `"toolResult:<toolCallId>"` — recording only what
is NEW since the previous call.
3. **Agent snapshots**: when a turn's system prompt + tools are
byte-identical to its predecessor's, `turn_created.agent.resolved` is
`{ agentId, model, inheritedFrom }` instead of re-persisting ~70KB. The
model stays concrete (mid-session model switches still inherit).
The exact provider payload is rebuilt by `composeModelRequest`
(`packages/core/src/turns/compose-model-request.ts`) — **the same code path
the loop transmits through**, so the file plus the composer reproduce the
wire bytes exactly (there is a property test asserting composed == sent).
## Inspecting turns and sessions
```bash
cd apps/x/packages/core
# A whole session: title, per-turn status/size/input preview
npm run inspect -- <sessionId | path/to/session.jsonl>
# One turn: per model call, the EXACT provider payload — resolved system
# prompt, tool list, wire-form messages (user-message context woven in,
# tool-result envelopes), and the response/failure
npm run inspect -- <turnId | path/to/turn.jsonl> [modelCallIndex] [--full]
# Cascade full turn inspection across a session
npm run inspect -- <sessionId> --turns
```
`--full` prints untruncated system prompts and message contents. Turn vs
session ids are auto-detected. This is the intended way to see "what did the
model actually receive" — the raw JSONL deliberately stores structural facts
and references, never the derived wire form.
## Invariants to respect
- Turn/session files are **append-only**; reducers reject impossible
histories loudly (`TurnCorruptionError`). Never hand-edit files.
- Durable events are persisted **before** side effects (model calls, tool
invocations). Deltas (`text_delta`, …) are stream-only, never persisted.
- The reducers in `@x/shared` must stay pure (no I/O, no node imports) —
the renderer imports them directly.
- Every behavior change needs tests: reducers in `packages/shared`,
runtime/sessions in `packages/core`, renderer stores/views in
`apps/renderer` (all vitest; run `npm test` per package).
- Schema changes: the schema is pre-release (`schemaVersion: 1` throughout);
breaking changes are acceptable but require wiping `~/.rowboat/storage`
and a note in the commit message.