Decision: defer code-mode unification; no deletions. The legacy runs runtime (runs/, AgentRuntime/streamAgent, runs:* IPC, App.tsx legacy tab state) remains solely for code-mode sessions. AGENTS.md gets the authoritative carve-out section: what stays and why, the no-new-callers rule (headless work uses agents/headless.ts), the temporary fallbacks that die with unification, and the scoped future project (rowboat-mode code prompts as composition turns; direct-mode ACP streams as a delegated turn kind). Turn-runtime design doc status updated to implemented-and-live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 KiB
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:
- Context: a session turn's
contextis{ previousTurnId }; the conversation prefix is materialized by walking the chain (TurnRepoContextResolver). - Model requests:
model_call_requested.request.messagesis 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. - Agent snapshots: when a turn's system prompt + tools are
byte-identical to its predecessor's,
turn_created.agent.resolvedis{ 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
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.
The legacy runs runtime is code-mode-only
The old runs runtime (packages/core/src/runs/, AgentRuntime/streamAgent
in packages/core/src/agents/runtime.ts, the runs:* IPC channels, and the
renderer's legacy chat-tab state machine in App.tsx) remains in the tree
solely for code-mode sessions — a deliberate, documented carve-out:
- A code session IS a run (
sessionId === runId). Direct-mode prompts drive an external ACP agent and hand-assemblecode-run-events into the run log (code-mode/sessions/service.ts); rowboat-mode code tabs go throughruns:createMessage→ the oldAgentRuntimeloop. - Everything else — main chat, background tasks, live notes, knowledge
pipelines, scheduled agents — runs on turns/sessions. Do NOT add new
callers of
createRun/createMessage/streamAgent; headless work usespackages/core/src/agents/headless.ts(startHeadlessAgent/runHeadlessAgent). - Temporary bridges that die when code-mode is unified: the renderer
transcript loader's
runs:fetchfallback (lib/agent-transcript.ts), theruns:downloadLogfallback in the chat sidebar, and thenotify-usergate'sfetchRunfallback (application/lib/builtin-tools.ts). - The remaining migration (designed but deferred): port code sessions onto
sessions/turns — rowboat-mode prompts become normal turns with
codeMode/codeCwdcomposition (already supported by the agent resolver's composition overrides), direct-mode prompts become a delegated turn kind carrying opaque code events. That project deletesruns/entirely.
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/sharedmust stay pure (no I/O, no node imports) — the renderer imports them directly. - Every behavior change needs tests: reducers in
packages/shared, runtime/sessions inpackages/core, renderer stores/views inapps/renderer(all vitest; runnpm testper package). - Schema changes: the schema is pre-release (
schemaVersion: 1throughout); breaking changes are acceptable but require wiping~/.rowboat/storageand a note in the commit message.