# Why Four Packages? Where Does Each Layer End?

> The repo ships four independently publishable npm packages: pi-ai, pi-agent-core, pi-tui, and pi-coding-agent. Each boundary is a deliberate seam. This page asks: which concerns forced each split, what can cross each boundary, and what would break if two packages were merged? Reading package.json exports, tsconfig.build.json, and the import graph answers these questions concretely.

- 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

- `packages/ai/package.json`
- `packages/agent/package.json`
- `packages/tui/package.json`
- `packages/coding-agent/package.json`
- `packages/coding-agent/src/core/sdk.ts`

---

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

- [packages/ai/package.json](packages/ai/package.json)
- [packages/agent/package.json](packages/agent/package.json)
- [packages/tui/package.json](packages/tui/package.json)
- [packages/coding-agent/package.json](packages/coding-agent/package.json)
- [packages/coding-agent/src/core/sdk.ts](packages/coding-agent/src/core/sdk.ts)
- [packages/ai/src/index.ts](packages/ai/src/index.ts)
- [packages/ai/src/types.ts](packages/ai/src/types.ts)
- [packages/ai/src/stream.ts](packages/ai/src/stream.ts)
- [packages/agent/src/index.ts](packages/agent/src/index.ts)
- [packages/agent/src/agent.ts](packages/agent/src/agent.ts)
- [packages/agent/src/types.ts](packages/agent/src/types.ts)
- [packages/agent/src/node.ts](packages/agent/src/node.ts)
- [packages/agent/src/harness/types.ts](packages/agent/src/harness/types.ts)
- [packages/tui/src/tui.ts](packages/tui/src/tui.ts)
- [packages/coding-agent/src/modes/interactive/interactive-mode.ts](packages/coding-agent/src/modes/interactive/interactive-mode.ts)
</details>

# Why Four Packages? Where Does Each Layer End?

This repository ships four independently publishable npm packages: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-tui`, and `@earendil-works/pi-coding-agent`. Each split is a boundary between concerns that cannot be safely merged without forcing unwanted coupling on downstream consumers. This page traces the dependency graph from the bottom up, names the concern each package owns exclusively, and asks what would break if any two packages were folded together.

The question is not "what does each package do?" but rather "what does each package _not_ see?"—because the constraint each package avoids owning is precisely what forces the split.

---

## The Dependency Lattice

Before examining any individual layer, map what depends on what:

```text
@earendil-works/pi-coding-agent
  └── @earendil-works/pi-agent-core
  └── @earendil-works/pi-ai
  └── @earendil-works/pi-tui

@earendil-works/pi-agent-core
  └── @earendil-works/pi-ai

@earendil-works/pi-tui
  (no pi-* deps)

@earendil-works/pi-ai
  (no pi-* deps)
```

Two packages sit at the base and share no cross-dependency: `pi-ai` and `pi-tui`. They are orthogonal concerns — LLM I/O vs. terminal rendering. `pi-agent-core` uses `pi-ai` but is unaware of `pi-tui`. Only `pi-coding-agent` pulls all three together.

Sources: [packages/agent/package.json:32](), [packages/coding-agent/package.json:42-44]()

---

## Layer 1 — `pi-ai`: The Provider Abstraction

### What concern owns this layer?

`pi-ai` answers a single question: *given a `Model` descriptor and a `Context`, how do you get an `AssistantMessageEventStream` back?* Nothing about agents, nothing about terminals.

The package exports a registry of concrete provider adapters (Anthropic, OpenAI, Google, Bedrock, Mistral, Azure, etc.), a generated model catalogue, OAuth helpers, and the two primary streaming entry-points: `stream()` and `streamSimple()`. Every provider resolves through `getApiProvider(model.api)`, keeping the call-site provider-neutral.

```ts
// packages/ai/src/stream.ts:43-50
export function streamSimple<TApi extends Api>(
  model: Model<TApi>,
  context: Context,
  options?: SimpleStreamOptions,
): AssistantMessageEventStream {
  const provider = resolveApiProvider(model.api);
  return provider.streamSimple(model, context, options);
}
```

The `exports` field in `package.json` exposes each provider as a separate sub-path (`./anthropic`, `./google`, `./bedrock-provider`, etc.), so consumers can tree-shake provider bundles they don't need.

Sources: [packages/ai/src/stream.ts:43-50](), [packages/ai/package.json:8-52]()

### What would break if `pi-ai` were merged into `pi-agent-core`?

Any consumer wanting only a lightweight LLM client — a one-off completion script, an image-generation wrapper, a test harness — would be forced to take the agent loop, session storage, compaction logic, and skill-loading machinery. The reverse is also true: merging would make it impossible to swap or publish a fresh provider adapter independently.

---

## Layer 2 — `pi-tui`: The Terminal Rendering Layer

### What concern owns this layer?

`pi-tui` owns everything the terminal sees: differential rendering, keyboard input, ANSI escape sequences, Kitty protocol image display, Unicode/East-Asian width accounting, the editor component, autocomplete, keybindings, and a minimal Markdown renderer. Its `package.json` lists **zero** `pi-*` dependencies; it imports nothing from the LLM stack.

```ts
// packages/tui/src/tui.ts:39-56
export interface Component {
  render(width: number): string[];
  handleInput?(data: string): void;
  wantsKeyRelease?: boolean;
  // ...
}
```

The `Component` interface is the entire contract: a component renders to an array of strings and optionally handles input. The TUI drives differential updates from those arrays without knowing whether the content came from an LLM, a static string, or a file diff.

Sources: [packages/tui/src/tui.ts:39-56](), [packages/tui/package.json:38-41]()

### What would break if `pi-tui` were merged into `pi-coding-agent`?

`pi-tui` is independently publishable and usable as a general terminal-UI library. Merging it into the coding agent would make it impossible to use the TUI in any project that doesn't also want the full agent stack. Because `pi-agent-core` deliberately avoids importing `pi-tui`, there would also be a circular-concern problem: the agent harness would suddenly carry terminal rendering machinery that it never exercises in non-interactive or RPC modes.

---

## Layer 3 — `pi-agent-core`: The Agent Loop and Harness

### What concern owns this layer?

`pi-agent-core` owns the LLM agent loop and the infrastructure required to run it reliably: message state, tool dispatch, context compaction, session persistence (JSONL and in-memory repos), skill loading, system-prompt assembly, and transport negotiation. It depends on `pi-ai` (for `Model`, `streamSimple`, message types, `ThinkingLevel`, `Transport`) but knows nothing about terminals or the coding-specific tool set.

```ts
// packages/agent/src/types.ts:24-26
export type StreamFn = (
  ...args: Parameters<typeof streamSimple>
) => ReturnType<typeof streamSimple> | Promise<ReturnType<typeof streamSimple>>;
```

`StreamFn` is the only connection the agent loop has to actual provider I/O. The caller (the coding agent's `sdk.ts`) wires in the real `streamSimple` call along with auth resolution and attribution headers, but the loop itself is provider-agnostic.

The `./node` sub-path export adds `NodeExecutionEnv` for callers that need Node.js-specific environment integration without bundling it unconditionally.

Sources: [packages/agent/src/types.ts:24-26](), [packages/agent/src/node.ts:1-2](), [packages/agent/package.json:13-17]()

### What would break if `pi-agent-core` were merged into `pi-coding-agent`?

`pi-agent-core`'s harness — compaction, session repos, skill system, prompt templates — is generic enough to power agents beyond the coding use-case. Embedding it inside the coding agent would mean any embedding application would have to import `cross-spawn`, `diff`, `glob`, `highlight.js`, WASM image processing, and the full TUI just to run an agent loop. The library surface would also become impossible to test in isolation, because the harness tests (`test:harness`) currently run against `pi-agent-core` alone without the heavy coding-agent dependencies.

---

## Layer 4 — `pi-coding-agent`: The Composition Layer

### What concern owns this layer?

`pi-coding-agent` is the only package that sees all three lower layers simultaneously. It owns:

- **Coding tools** (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) — file system operations specific to a coding workflow
- **Session configuration** — auth storage, model registry, settings manager, resource loader, extension runner
- **Rendering** — interactive TUI mode (implements `Component` from `pi-tui`), print mode, and RPC mode
- **CLI entry point** (`dist/cli.js` via `bin.pi`)
- **`createAgentSession()`** — the top-level factory that wires all three lower layers into a running session

The `sdk.ts` file is the architectural join-point. It imports `Agent` from `pi-agent-core`, `streamSimple` and `Model` from `pi-ai`, and passes neither directly to `pi-tui`. The interactive mode (`interactive-mode.ts`) then imports from all three simultaneously to wire together rendering, agent events, and LLM output.

```ts
// packages/coding-agent/src/core/sdk.ts:2-3
import { Agent, type AgentMessage, type ThinkingLevel } from "@earendil-works/pi-agent-core";
import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai";
```

```ts
// packages/coding-agent/src/modes/interactive/interactive-mode.ts:10-49
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import { getProviders, type Model, type OAuthProviderId, ... } from "@earendil-works/pi-ai";
import type { AutocompleteItem, EditorComponent, ... } from "@earendil-works/pi-tui";
import { TUI, Container, Markdown, Text, ... } from "@earendil-works/pi-tui";
```

Sources: [packages/coding-agent/src/core/sdk.ts:2-3](), [packages/coding-agent/src/modes/interactive/interactive-mode.ts:10-49]()

---

## Boundary Summary

```text
┌─────────────────────────────────────────────────────┐
│  pi-coding-agent                                    │
│  • CLI entry point (bin: pi)                        │
│  • Coding tools (read/bash/edit/write/grep/…)       │
│  • Interactive / print / RPC modes                  │
│  • createAgentSession() factory                     │
│  deps: pi-ai + pi-agent-core + pi-tui               │
└───────────┬───────────┬──────────────┬──────────────┘
            │           │              │
   ┌────────▼───┐  ┌────▼──────────┐  │
   │ pi-ai      │  │ pi-agent-core │  │
   │ • Models   │  │ • Agent loop  │  │
   │ • Providers│◄─┤ • Compaction  │  │
   │ • OAuth    │  │ • Sessions    │  │
   │ • stream() │  │ • Skills      │  │
   └────────────┘  └───────────────┘  │
                                       │
                       ┌───────────────▼──┐
                       │ pi-tui            │
                       │ • Terminal render │
                       │ • Diff render     │
                       │ • Editor / keys   │
                       │ (no pi-* deps)    │
                       └──────────────────┘
```

| Package | Owned concern | Forbidden dependency |
|---|---|---|
| `pi-ai` | Provider abstraction, streaming, model catalogue | Nothing from the agent or TUI |
| `pi-tui` | Terminal rendering, keyboard, Markdown display | Nothing from the LLM or agent layer |
| `pi-agent-core` | Agent loop, state, tools interface, session, compaction | `pi-tui` (rendering is caller's concern) |
| `pi-coding-agent` | Coding tools, modes, CLI, session wiring | — (is the composition root) |

---

## What the Split Enables

Each boundary is a publish-time contract. A consumer embedding only an LLM call takes `pi-ai` alone. A project building a terminal dashboard takes `pi-tui` alone. A project building a custom agent (not a coding agent) takes `pi-agent-core` + `pi-ai` and supplies its own tool set and renderer. Only the `pi` CLI binary needs all four.

The `build:binary` script in `pi-coding-agent` makes this concrete: it builds the three dependency packages in order before compiling the coding agent, precisely because none of the lower packages knows about the one above it.

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