Back to Pi Mono

mini

packages/coding-agent/src/experimental/mini/README.md

0.85.15.7 KB
Original Source

mini

A minimal coding agent on the durable AgentHarness, split across three processes that talk JSON over a socket and a pipe. It exists to exercise the harness from a real client and to find out what an RPC-shaped presentation actually needs from it.

bash
node packages/coding-agent/src/experimental/mini/main.ts [--continue]

# while the harness is changing under us, run from source instead of built dist:
./node_modules/.bin/tsx --tsconfig tsconfig.json packages/coding-agent/src/experimental/mini/main.ts

--continue attaches to the newest session for the current directory. Starting mini twice attaches two presentations to the same session; both see the same transcript, live.

Topology

text
tui        tui        tui          presentations: render, no agent state
  \         |         /
   \        |        /             unix socket, ~/.pi/agent/experimental/mini.sock
    \       |       /
        server                     routes calls, fans out events, spawns workers
       /        \
      /          \                 child process stdio pipes
  worker       worker              one per session: harness, storage, model runtime

A presentation holds one connection and reaches every service through it. The server answers sessions itself and forwards anything else to the worker that connection is attached to, so the TUI never learns which host answers what. The same rule runs backwards: a worker calling sessions.list is answered by the server.

The first mini to start spawns the server detached. The server kills a worker when its last presentation disconnects, and retires itself ten seconds after the last presentation leaves, so the next start always runs current code. If that worker held an open durable operation, its replacement automatically resumes the operation from the last recorded recovery state.

Layout

pathrole
main.tsthe mini command
shared/transport.tsConnection: newline-delimited JSON over any duplex pair
shared/rpc.tsframes, named services, routing, liveness, cancellation
shared/protocol.tsservice tokens, contracts, wire types
server/run.tsroutes, worker supervision, event fan-out
worker/run.tsopens the session, builds the harness, provides services
worker/lane-service.tsLane: watch subscriptions and lane commands
worker/models-service.tsModels: catalog, accounts, interactive login
tui/run.tspresentation host: ensure server, attach, run the view
tui/session.tsattach, subscribe, hold the replicated snapshot
tui/view.tsalt-screen rendering, reusing interactive-mode components

RPC

Two verbs. Call asks a question and gets one answer; emit publishes to whoever listens. Both directions on every connection.

ts
const peer = createPeer(connection, { forward });   // forward handles names this peer lacks
peer.provide(Lane, laneService);                    // register and announce
const lane = peer.use(Lane);                        // Remote<LaneServiceApi>
await lane.prompt("hi");                            // -> { kind: "call", id, method: "lane.prompt" }
peer.on(Lane, (event) => fold(event));              // typed by the token

The wire is six frame kinds: call, result, error, cancel, event, announce, ping. Replies correlate by id and resolve one promise; events carry a service name and no id, so the two never interfere — a five-minute lane.prompt sits pending while its stream of events flows past.

Services are plain objects registered under a token, and a token carries the name plus the call and event types, so provide checks the implementation and use checks the caller. Names are announced on registration, so routing is a lookup and an unroutable call fails with both inventories named.

Failure handling: a closed connection rejects every pending call and aborts every in-flight handler; peers ping every five seconds and declare a silent line dead after fifteen, which catches a wedged process that never closes; callers may pass a signal (sends cancel) or a timeoutMs. Timeouts are opt-in per call site, because an agent turn has no bounded duration.

State replication

The worker holds every live object. A presentation holds a LaneSnapshot and nothing else.

Each presentation gets its own lane.watch() in the worker, in the harness's two phases: watch() captures a snapshot while the harness buffers that subscription's events, and start() drains them. Because the harness pairs snapshot and stream with no gap and no duplicate, the client needs no buffering or ordering logic of its own — it applies events with the harness's own reduceLaneSnapshot. When the fold answers { rebase: true }, which a completed navigation does, the presentation takes a fresh subscription and drops the old one. First attach and rebase are the same function.

Rendering is a pure function of that snapshot. Transcript entries are keyed by id and appended, so a streamed token costs one markdown rebuild rather than a repaint; the view repaints wholesale only when the entry-id prefix diverges, which is what compaction and navigation do.

What it is not

No slash-command system, extensions, skills, hooks, themes beyond the shared one, session picker, or tree navigation. Interactive login and model selection exist because they proved the awkward directions of the protocol: server-to-client requests, and identity-based configuration.

Two known shortcuts: the server broadcasts a worker's events to every attached presentation, so with N presentations each event crosses N² times and each client discards the copies that are not its own; and cancelling an interactive login only interrupts at a prompt, since the worker's login has no abort path yet.