pi
Arbe’s LLM driver is @earendil-works’s
pi-mono stack. At runtime we use pi-ai and the pi CLI binary (pi-coding-agent). Upstream
READMEs are the source of truth for each API — read one with gh api repos/earendil-works/pi-mono/contents/packages/<pkg>/README.md --jq .content | base64 -d
(<pkg>: ai, agent, coding-agent).
- pi-ai — one LLM API across providers (Anthropic, OpenAI, Google, …), and arbe’s provider
boundary: import
@earendil-works/pi-ai, never a provider SDK like@anthropic-ai/sdk. AContextis plain serialisable data, so a conversation can move between providers mid-run. - pi-coding-agent — the
piCLI binary. Arbe execs it as a subprocess and decodes its JSONL session files (currentversion: 3). Entries form anid/parentIdtree, and compaction replaces old entries with one summary — so a decoder that skips unknown entry types silently drops conversation state. - pi-agent-core — a stateful tool-calling loop over pi-ai, for tool execution without rolling your own. Not wired in yet.
How arbe runs pi:
in-process bot reply pi-ai.complete() inside packages/core/dispatch/dispatch.tslocal CLI (arbe chat) exec pi --print --mode json --session-dir <dir> [--continue] + JSONL decoderlocal interactive (arbe chat) pi --session-dir <dir> (TUI), then sync transcript on exitdetached sandbox job daytona: arbe-pi-runner + mirror → thread; sprite: relay tails wire.jsonlEnv-bound dispatch reaches the sandbox through the in-process bot’s run_command tool. The
detached row is the producer a delegated coding agent will run on; see
daytona runtime (default) and sandbox-sprite (legacy).
Sessions and decoding
Arbe owns the per-thread session directory, scoped by arbe thread id:
<repo>/.arbe/pi-sessions/<threadId>/ locally, /home/sprite/.arbe/pi-sessions/<threadId>/
on sprites. --continue is set when a *.jsonl already exists.
pi’s stdout JSONL decodes through decodePiEvent in packages/core/pi/events.ts into
pi.chunk, pi.assistant, pi.tool_result, and pi.compaction envelopes. Detached
sandbox runs decode the same events in-sandbox via the pi mirror extension, which posts
them to the thread while arbe-pi-runner owns pi’s exit code and the terminal.
After an interactive TUI exits, packages/core/pi/transcript.ts syncs from the one
*.jsonl in that directory. Zero files means nothing to sync; more than one is ambiguous
and fails loudly. derivePiUsage aggregates per-message usage across a thread transcript.
pi is a process boundary
@arbe/core and @arbe/cli must not import pi-coding-agent at runtime. Its module init
reads its own package.json, which fails under bun build --compile, and its heavy deps
(a TUI, WASM, zip/file-type/glob, and more) would balloon every @arbe/core import. So
treat pi as a process boundary and decode its wire or file output at arbe-owned seams.
Auth
OPENROUTER_API_KEY is the base key for in-process bot replies, local pi (arbe chat),
and sandbox pi turns. Direct provider keys such as ANTHROPIC_API_KEY work only when the
chosen model ref uses that provider. Both are server-side only: the browser talks to route
handlers, never pi-ai. Bots use model refs (agent.model, default
openrouter/anthropic/claude-haiku-4.5); local pi reads the same defaultModelRef, not
openrouter/auto. OAuth providers (Anthropic Pro, OpenAI Codex, GitHub Copilot) use
@earendil-works/pi-ai/oauth with caller-managed credentials, and are out of scope for v1.
Code: packages/core/pi/events.ts, packages/core/pi/transcript.ts,
packages/core/pi/defaults.ts, packages/core/dispatch/. Proof:
bun run apps/www/scripts/prove-pi-ai.ts. See system/dispatch,
system/auth, system/secrets.