# pi

Arbe's LLM driver is [@earendil-works](https://github.com/earendil-works/pi-mono)'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](./sandbox-daytona.md) (default) and [sandbox-sprite](./sandbox-sprite.md) (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](./dispatch.md),
[system/auth](./auth.md), [system/secrets](./secrets.md).
