Skip to content
View as .md

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. A Context is plain serialisable data, so a conversation can move between providers mid-run.
  • pi-coding-agent — the pi CLI binary. Arbe execs it as a subprocess and decodes its JSONL session files (current version: 3). Entries form an id/parentId tree, 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.ts
local CLI (arbe chat) exec pi --print --mode json --session-dir <dir> [--continue] + JSONL decoder
local interactive (arbe chat) pi --session-dir <dir> (TUI), then sync transcript on exit
detached sandbox job daytona: arbe-pi-runner + mirror → thread; sprite: relay tails wire.jsonl

Env-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.