# The First Question: Why Is a Simple Script Not Enough?

> What problem does pi actually solve, and why does the answer demand four separate packages instead of one? This page traces the repo from its name down to its monorepo shape, asking at every step which assumption would break if the system were simpler. The answer reveals the architectural bet: that provider neutrality, session persistence, and a live extension system are inseparable from any serious coding agent.

- Repository: earendil-works/pi
- GitHub: https://github.com/earendil-works/pi
- Human wiki: https://grok-wiki.com/public/wiki/earendil-works-pi-8b87608fc234
- Complete Markdown: https://grok-wiki.com/public/wiki/earendil-works-pi-8b87608fc234/llms-full.txt

## Source Files

- `README.md`
- `package.json`
- `packages/coding-agent/src/main.ts`
- `packages/coding-agent/src/core/index.ts`
- `AGENTS.md`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [README.md](README.md)
- [package.json](package.json)
- [packages/coding-agent/package.json](packages/coding-agent/package.json)
- [packages/agent/package.json](packages/agent/package.json)
- [packages/ai/package.json](packages/ai/package.json)
- [packages/tui/package.json](packages/tui/package.json)
- [packages/coding-agent/src/main.ts](packages/coding-agent/src/main.ts)
- [packages/coding-agent/src/core/index.ts](packages/coding-agent/src/core/index.ts)
- [packages/coding-agent/src/core/extensions/types.ts](packages/coding-agent/src/core/extensions/types.ts)
- [packages/coding-agent/src/core/agent-session-services.ts](packages/coding-agent/src/core/agent-session-services.ts)
- [packages/coding-agent/src/core/sdk.ts](packages/coding-agent/src/core/sdk.ts)
- [packages/coding-agent/src/core/session-manager.ts](packages/coding-agent/src/core/session-manager.ts)
- [packages/ai/src/providers/register-builtins.ts](packages/ai/src/providers/register-builtins.ts)
- [AGENTS.md](AGENTS.md)
</details>

# The First Question: Why Is a Simple Script Not Enough?

At first glance, pi looks like a coding assistant you could prototype in an afternoon: pipe a prompt to an LLM, get shell commands back, execute them. So why does this repository contain four separately published npm packages and a monorepo build pipeline rather than a single file or a straightforward CLI wrapper?

The answer is not complexity for its own sake. Each package boundary in this repo encodes a distinct architectural necessity — a place where "make it simpler" would cause something real to break. This page traces those boundaries from the repo's own name down to the event-driven extension system, asking at every step which assumption would collapse if the system were flatter.

---

## What is the simplest version of this tool?

Start with the dumbest possible coding agent: read a prompt, call one LLM API, pipe the output to a shell, print the result. This single-function version is easy to write. It also has immediate, concrete failure modes:

- It is permanently coupled to one model provider. Swapping from OpenAI to Anthropic requires rewriting the network layer.
- It has no memory. Each invocation starts from scratch. Long tasks — refactoring a module, writing and iterating on tests — cannot be split across time.
- It cannot be extended without forking the source. Any feature not baked in at build time is absent entirely.
- It has no interactive feedback loop. The user cannot see streaming output, cannot approve or redirect tool calls in real time, cannot cancel a runaway bash command.

Pi's four-package shape is the answer to each of these four failure modes, one package per concern.

Sources: [README.md:23-56]()

---

## How the monorepo boundary map answers those failure modes

```text
┌─────────────────────────────────────────────────────────────┐
│  What breaks in the simple script     │  Which package fixes it │
├───────────────────────────────────────┼─────────────────────────┤
│  Single-provider coupling             │  @earendil-works/pi-ai  │
│  No persistent memory / session state │  @earendil-works/pi-agent-core │
│  No live extension surface            │  (extension system in   │
│                                       │   pi-coding-agent core) │
│  No interactive terminal rendering    │  @earendil-works/pi-tui │
│  (orchestration + CLI glue)           │  @earendil-works/pi-coding-agent │
└─────────────────────────────────────────────────────────────┘
```

The coding agent `package.json` shows the dependency direction explicitly: the CLI depends on all three libraries (`@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, and `@earendil-works/pi-tui`), while each library has no upward dependency on the CLI.

Sources: [packages/coding-agent/package.json:42-48]()

---

## Why provider neutrality demands its own package

### The simplest version would hardcode one SDK

A script that imports `@anthropic-ai/sdk` directly and calls `messages.create()` works fine until users want GPT-4o, Gemini, Mistral, or a corporate proxy. At that point the script needs conditional branches, provider-specific token formats, and divergent error handling — all tangled with the agent logic.

### What pi-ai actually does

`@earendil-works/pi-ai` packages eight provider implementations behind a uniform `streamSimple` interface, registered via lazy-loaded modules to avoid paying the import cost of unused SDKs:

```typescript
// packages/ai/src/providers/register-builtins.ts
interface LazyProviderModule<TApi, TOptions, TSimpleOptions> {
  stream: (model: Model<TApi>, context: Context, options?: TOptions) => AsyncIterable<AssistantMessageEvent>;
  streamSimple: (model: Model<TApi>, context: Context, options?: TSimpleOptions) => AsyncIterable<AssistantMessageEvent>;
}
```

The `package.json` for `pi-ai` lists the provider SDKs as direct dependencies (`@anthropic-ai/sdk`, `openai`, `@google/genai`, `@mistralai/mistralai`, `@aws-sdk/client-bedrock-runtime`), all pinned to exact versions. This is the only package that should ever know those SDK shapes exist.

Sources: [packages/ai/src/providers/register-builtins.ts:23-33](), [packages/ai/package.json:69-79]()

### Why separation matters for extension authors

Extensions can register entirely new providers at runtime without touching the core:

```typescript
// From ExtensionAPI in types.ts
pi.registerProvider("my-proxy", {
  baseUrl: "https://proxy.example.com",
  apiKey: "PROXY_API_KEY",
  api: "anthropic-messages",
  models: [{ id: "claude-sonnet-4-20250514", ... }]
});
```

If the provider abstraction were embedded in the CLI, extensions could not safely add models. The `pi-ai` package boundary is what makes BYOK/BYOC (bring-your-own-key / bring-your-own-credentials) possible at runtime, not just at build time.

Sources: [packages/coding-agent/src/core/extensions/types.ts:1254-1292]()

---

## Why session persistence demands its own package

### What "no memory" actually costs

Without session persistence, every resumed conversation starts from token zero. The user's previous error messages, the agent's previous tool outputs, partial file edits — all gone. For toy tasks this is acceptable. For multi-step coding tasks that span hours (refactoring, debugging, writing a test suite), it is a show-stopper.

### What pi-agent-core actually stores

`@earendil-works/pi-agent-core` is described as a "general-purpose agent with transport abstraction, state management, and attachment support." The session file format it defines (`SessionManager`) stores messages as an append-only JSONL tree, versioned (`CURRENT_SESSION_VERSION = 3`), with typed entries for messages, model changes, compaction summaries, branch summaries, and arbitrary extension-defined custom entries:

```typescript
// packages/coding-agent/src/core/session-manager.ts (excerpt)
export interface SessionHeader {
  type: "session";
  version?: number;
  id: string;
  timestamp: string;
  cwd: string;
  parentSession?: string;
}
```

The `cwd` field in the session header is architectural: sessions are bound to the working directory they were created in. When you resume a session from a different project, `main.ts` detects the mismatch via `getMissingSessionCwdIssue()` and prompts you to fork rather than silently corrupt the context.

Session branching (fork, tree navigation, compaction) is managed entirely within this layer, invisible to the LLM layer and the UI layer.

Sources: [packages/coding-agent/src/core/session-manager.ts:1-37](), [packages/coding-agent/src/main.ts:507-519]()

### Why it's a separate published package

`pi-agent-core` is `"description": "General-purpose agent with transport abstraction, state management, and attachment support"`. It is published independently precisely so other tools (e.g., [earendil-works/pi-chat](https://github.com/earendil-works/pi-chat)) can reuse the agent runtime without inheriting the interactive TUI or the coding-specific tools.

Sources: [packages/agent/package.json:2-3](), [README.md:57]()

---

## Why a live extension system is inseparable from a serious agent

### The question the simple script cannot answer

What happens when users want: a Jira tool, a custom compaction strategy, a vim-keybindings editor, a corporate OAuth provider, or a status bar widget showing open PRs? In a simple script, each of these requires a fork or a config flag. There is no principled answer.

### How the extension API is structured

The extension system in `pi-coding-agent` is a first-class event bus, not a plugin loader bolted on afterward. Extensions receive a typed `ExtensionAPI` object at load time and can:

- Subscribe to the full agent lifecycle (`session_start`, `before_agent_start`, `turn_start`, `turn_end`, `tool_call`, `tool_result`, `agent_end`, and more)
- Register LLM-callable tools with TypeBox parameter schemas, custom renderers, and per-tool execution modes
- Register slash commands, keyboard shortcuts, and CLI flags
- Register or override LLM providers at runtime via `registerProvider`
- Mutate tool inputs in-flight by modifying `event.input` in place within a `tool_call` handler

```typescript
// packages/coding-agent/src/core/extensions/types.ts:1084-1135 (ExtensionAPI interface)
export interface ExtensionAPI {
  on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
  on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
  registerTool<TParams extends TSchema>(tool: ToolDefinition<TParams>): void;
  registerProvider(name: string, config: ProviderConfig): void;
  // ...
}
```

This is not a narrow plugin point — it is the full observability surface of a running agent turn. Extensions can cancel operations (`block: true` in `ToolCallEventResult`), replace system prompts, inject messages before turns, and drive UI components.

Sources: [packages/coding-agent/src/core/extensions/types.ts:1084-1140](), [packages/coding-agent/src/core/extensions/types.ts:820-826]()

### Why "reload without restart" matters

Extensions can be reloaded at runtime via `ctx.reload()` from a command handler. The `ExtensionRuntimeState.invalidate()` method marks stale extension instances so their pending handlers throw rather than silently operating on old state. This means the extension system must track lifecycle epochs, not just loaded modules — a complexity that belongs in a dedicated subsystem, not scattered through the CLI entry point.

Sources: [packages/coding-agent/src/core/extensions/types.ts:1451-1466]()

---

## Why the TUI is its own package

### Differential rendering is not trivial

A simple `console.log` output model breaks the moment you want streaming token output, inline diffs, expandable tool call results, and a persistent input editor in the same terminal window simultaneously. These require a retained component tree and differential rendering — knowing which terminal cells changed and repainting only those.

`@earendil-works/pi-tui` is described as a "Terminal User Interface library with differential rendering for efficient text-based applications." It is the rendering substrate for the interactive mode and is imported directly into `main.ts` for the session-picker and the missing-session-cwd prompt, before the full agent session is even created.

```typescript
// packages/coding-agent/src/main.ts:395-416
const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());
ui.setClearOnShrink(settingsManager.getClearOnShrink());
const selector = new ExtensionSelectorComponent(
  formatMissingSessionCwdPrompt(issue),
  ["Continue", "Cancel"],
  (option) => finish(option === "Continue" ? issue.fallbackCwd : undefined),
  ...
);
```

Extensions use `ExtensionUIContext` to call TUI primitives directly — overlays, footers, headers, custom editor components — without depending on the coding agent's internal rendering logic. The TUI package boundary is what allows extension authors to build rich UI without coupling to the CLI internals.

Sources: [packages/tui/package.json:2-11](), [packages/coding-agent/src/main.ts:395-416](), [packages/coding-agent/src/core/extensions/types.ts:124-275]()

---

## The four-mode run model: why flat CLI cannot host all of them

`main.ts` resolves one of four `AppMode` values before any agent work begins: `interactive`, `print`, `json`, or `rpc`. Each mode requires a different output contract:

| Mode | How it runs | What it needs |
|---|---|---|
| `interactive` | Full TUI, streaming, user input | `pi-tui` + full extension surface |
| `print` | Non-interactive, stdout text | No TUI, piped stdin handled |
| `json` | Non-interactive, structured JSON | Same as print, different serialization |
| `rpc` | JSON-RPC over stdin/stdout | Stdin consumed for protocol, not user input |

The `ExtensionUIContext` interface has a mode-specific implementation per `AppMode`. This means the extension system does not know which mode it is running in — it calls `ctx.ui.select()` and gets the right behavior. A simple script cannot offer this abstraction: it either hardcodes terminal output or hardcodes JSON output.

Sources: [packages/coding-agent/src/main.ts:97-113](), [packages/coding-agent/src/core/extensions/types.ts:124-275]()

---

## The architectural bet, stated plainly

The repo's shape encodes a single wager: that provider neutrality (pi-ai), session persistence (pi-agent-core), live extensibility (extension system in coding-agent core), and an interactive rendering layer (pi-tui) are not independent features that can be added incrementally to a simple script. Each one requires the others to be correct. An extension that registers a new LLM provider must be able to persist that provider choice into the session file. A session resumed from disk must render its history correctly in the TUI. A tool call blocked by an extension must still emit the right JSON in RPC mode.

The packages are separated not to enforce organizational hygiene but to make each concern independently testable, independently publishable, and independently reusable — as proven by `pi-chat` reusing `pi-agent-core` without the coding-specific tools.

Sources: [README.md:22-57](), [packages/agent/package.json:2-7]()

---

## Summary

The gap between "a script that calls an LLM" and "a serious coding agent" is exactly the gap between four missing properties: provider neutrality, durable session state, a live extension surface, and a rendering layer that survives streaming. The `earendil-works/pi` monorepo does not split into four packages as an abstraction exercise — it splits because each property requires its own isolation boundary to be testable, reusable, and safe to evolve without breaking the others. `pi-ai` owns the provider contract; `pi-agent-core` owns the session and agent loop; the extension system in `pi-coding-agent/core` owns the live plugin surface; and `pi-tui` owns the terminal rendering model. Remove any one of them and the architectural bet collapses back into a well-dressed script. Sources: [packages/coding-agent/src/core/agent-session-services.ts:67-75]()
