# Pi Coding Agent Documentation

> Technical docs for @earendil-works/pi-coding-agent: the pi CLI, interactive/print/RPC run modes, session runtime, skills and extensions, settings, and the embeddable SDK.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1)
- [GitHub repository](https://github.com/earendil-works/pi)

## Repository Metadata

- Repository: earendil-works/pi
- Branch: main
- Generated: 2026-08-09T04:03:44.924Z
- Updated: 2026-08-09T04:11:50.352Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 24

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/01-overview.md) - What pi is, the four run modes, default tools, and the first docs routes for CLI users and embedders.
- 02. [Installation](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/02-installation.md) - npm and installer install paths, bin entry, package exports, and verification for @earendil-works/pi-coding-agent.
- 03. [Quickstart](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/03-quickstart.md) - Install, authenticate with API key or /login, start interactive pi, and confirm the default read/write/edit/bash tool surface.
- 04. [Run modes](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/04-run-modes.md) - Interactive, print/JSON, RPC, and SDK modes: how each is invoked, entry points, and when to choose them.
- 05. [Agent sessions](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/05-agent-sessions.md) - Session lifecycle, prompt queue, concurrent behavior, stats, and runtime services that own a conversation turn.
- 06. [Branching and session trees](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/06-branching-and-session-trees.md) - Session branching, tree navigation, cancel-during-compact interactions, and name/event constraints for branches.
- 07. [Context compaction](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/07-context-compaction.md) - Auto and manual compaction triggers, reasons, retries on stream drop, and interactions with in-flight prompts and RPC.
- 08. [Tools and allowlists](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/08-tools-and-allowlists.md) - Default read/write/edit/bash tools, extension tools, allowlist and exclude-tools filters, and blocked-tool termination behavior.
- 09. [Authentication](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/09-authentication.md) - API keys, OAuth login, credential storage, auth checks, ambient auth, and refresh hang failure modes.
- 10. [Providers and models](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/10-providers-and-models.md) - Built-in and dynamic providers, scoped model order and refresh, models.json hot reload, and provider-retry message behavior.
- 11. [Settings](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/11-settings.md) - Settings load and in-memory reload, stale resource refresh, and merge rules for provider retry and related session options.
- 12. [Context files](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/12-context-files.md) - How project context files are discovered and injected into sessions, including SDK wiring for context file inputs.
- 13. [Skills](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/13-skills.md) - SKILL.md frontmatter rules, naming constraints, collision precedence, disable-model-invocation, and valid versus rejected skill fixtures.
- 14. [Extensions](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/14-extensions.md) - TypeScript extension registration, inline naming, active tools on next turn, OAuth prompt input, and shutdown cleanup contracts.
- 15. [Prompt templates](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/15-prompt-templates.md) - Custom system prompts and prompt templates as reusable session configuration, with SDK entry points and related prompt tests.
- 16. [Themes and Pi packages](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/16-themes-and-pi-packages.md) - Theme export requirements, package manifest validation, and how shareable Pi packages bundle extensions, skills, templates, and themes.
- 17. [SDK](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/17-sdk.md) - Embed pi with the package main export: minimal agent construction, full control hooks, custom models, tools, and settings.
- 18. [Session runtime](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/18-session-runtime.md) - AgentSessionRuntime services, lifecycle events (settled, start notify, event bus), and embedding sessions without the interactive TUI.
- 19. [RPC mode](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/19-rpc-mode.md) - Process integration via rpc-entry: command IDs, prompt-during-compaction constraints, JSON stream behavior, and unknown-command handling.
- 20. [CLI reference](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/20-cli-reference.md) - pi binary flags and argument parsing, auth subcommands, experimental auth entry points, and package bin wiring.
- 21. [Package exports](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/21-package-exports.md) - Public npm surface for @earendil-works/pi-coding-agent: main, rpc-entry, client exports, bin name, and piConfig.configDir.
- 22. [SDK examples](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/22-sdk-examples.md) - Copy-paste SDK recipes: minimal agent, custom model, skills, tools, extensions, sessions, settings, and full-control setups.
- 23. [Extension examples](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/23-extension-examples.md) - Reference extension packages: subagent agents, plan-mode, doom-overlay, dynamic resources skill, and auto-commit-on-exit.
- 24. [Troubleshooting](https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/24-troubleshooting.md) - Retry and network failures, credential refresh hangs, SIGTERM cleanup, bash output truncation, and session event settlement issues.

## Source File Index

- `packages/coding-agent/CHANGELOG.md`
- `packages/coding-agent/examples/extensions/auto-commit-on-exit.ts`
- `packages/coding-agent/examples/extensions/doom-overlay/README.md`
- `packages/coding-agent/examples/extensions/plan-mode/README.md`
- `packages/coding-agent/examples/extensions/README.md`
- `packages/coding-agent/examples/extensions/subagent/agents.ts`
- `packages/coding-agent/examples/extensions/subagent/README.md`
- `packages/coding-agent/examples/README.md`
- `packages/coding-agent/examples/sdk/01-minimal.ts`
- `packages/coding-agent/examples/sdk/02-custom-model.ts`
- `packages/coding-agent/examples/sdk/03-custom-prompt.ts`
- `packages/coding-agent/examples/sdk/04-skills.ts`
- `packages/coding-agent/examples/sdk/05-tools.ts`
- `packages/coding-agent/examples/sdk/06-extensions.ts`
- `packages/coding-agent/examples/sdk/07-context-files.ts`
- `packages/coding-agent/examples/sdk/08-prompt-templates.ts`
- `packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts`
- `packages/coding-agent/examples/sdk/10-settings.ts`
- `packages/coding-agent/examples/sdk/11-sessions.ts`
- `packages/coding-agent/examples/sdk/12-full-control.ts`
- `packages/coding-agent/examples/sdk/13-session-runtime.ts`
- `packages/coding-agent/examples/sdk/README.md`
- `packages/coding-agent/package.json`
- `packages/coding-agent/README.md`
- `packages/coding-agent/src/cli/args.ts`
- `packages/coding-agent/src/cli/auth-check.ts`
- `packages/coding-agent/src/cli/auth-command.ts`
- `packages/coding-agent/src/cli/experimental/auth.ts`
- `packages/coding-agent/src/core/agent-session-runtime.ts`
- `packages/coding-agent/src/core/agent-session-services.ts`
- `packages/coding-agent/src/core/agent-session.ts`
- `packages/coding-agent/src/core/auth-guidance.ts`
- `packages/coding-agent/src/core/auth-storage.ts`
- `packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts`
- `packages/coding-agent/test/agent-session-branching.test.ts`
- `packages/coding-agent/test/agent-session-compaction.test.ts`
- `packages/coding-agent/test/agent-session-concurrent.test.ts`
- `packages/coding-agent/test/agent-session-dynamic-provider.test.ts`
- `packages/coding-agent/test/agent-session-dynamic-tools.test.ts`
- `packages/coding-agent/test/agent-session-runtime-events.test.ts`
- `packages/coding-agent/test/agent-session-tree-navigation.test.ts`
- `packages/coding-agent/test/args.test.ts`
- `packages/coding-agent/test/auth-storage.test.ts`
- `packages/coding-agent/test/fixtures/skills-collision/first/calendar/SKILL.md`
- `packages/coding-agent/test/fixtures/skills/disable-model-invocation/SKILL.md`
- `packages/coding-agent/test/fixtures/skills/missing-description/SKILL.md`
- `packages/coding-agent/test/fixtures/skills/valid-skill/SKILL.md`
- `packages/coding-agent/test/suite/agent-session-compaction.test.ts`
- `packages/coding-agent/test/suite/agent-session-model-extension.test.ts`
- `packages/coding-agent/test/suite/agent-session-prompt.test.ts`
- `packages/coding-agent/test/suite/agent-session-queue.test.ts`
- `packages/coding-agent/test/suite/agent-session-runtime.test.ts`
- `packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts`
- `packages/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts`
- `packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts`
- `packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts`
- `packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts`
- `packages/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts`
- `packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts`
- `packages/coding-agent/test/suite/regressions/3616-settings-inmemory-reload.test.ts`
- `packages/coding-agent/test/suite/regressions/3686-session-name-event.test.ts`
- `packages/coding-agent/test/suite/regressions/3688-tree-cancel-compacting.test.ts`
- `packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts`
- `packages/coding-agent/test/suite/regressions/5109-exclude-tools.test.ts`
- `packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts`
- `packages/coding-agent/test/suite/regressions/5303-bash-output-truncation.test.ts`
- `packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts`
- `packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts`
- `packages/coding-agent/test/suite/regressions/5868-rpc-unknown-command-id.test.ts`
- `packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts`
- `packages/coding-agent/test/suite/regressions/5998-blocked-tool-terminate.test.ts`
- `packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts`
- `packages/coding-agent/test/suite/regressions/6162-extension-active-tools-next-turn.test.ts`
- `packages/coding-agent/test/suite/regressions/6260-inline-extension-naming.test.ts`
- `packages/coding-agent/test/suite/regressions/6363-agent-settled-event.test.ts`
- `packages/coding-agent/test/suite/regressions/6647-compaction-retries-transient-stream-drop.test.ts`
- `packages/coding-agent/test/suite/regressions/6949-unavailable-scoped-model.test.ts`
- `packages/coding-agent/test/suite/regressions/6999-models-json-hot-reload.test.ts`
- `packages/coding-agent/test/suite/regressions/7027-credential-refresh-hang.test.ts`
- `packages/coding-agent/test/suite/regressions/7150-rpc-prompt-during-compaction.test.ts`
- `packages/coding-agent/test/suite/regressions/7153-scoped-models-refresh.test.ts`
- `packages/coding-agent/test/suite/regressions/7187-malformed-package-manifest.test.ts`
- `packages/coding-agent/test/suite/regressions/7253-manual-compact-during-response.test.ts`
- `packages/coding-agent/test/suite/regressions/7290-json-stream-linear.test.ts`
- `packages/coding-agent/test/suite/regressions/7572-provider-retry-settings-merge.test.ts`

---

## 01. Overview

> What pi is, the four run modes, default tools, and the first docs routes for CLI users and embedders.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/01-overview.md
- Generated: 2026-08-09T03:38:42.553Z

### Source Files

- `packages/coding-agent/README.md`
- `packages/coding-agent/package.json`
- `packages/coding-agent/examples/README.md`
- `packages/coding-agent/src/core/agent-session.ts`
- `packages/coding-agent/src/cli/args.ts`

---
title: "Overview"
description: "What pi is, the four run modes, default tools, and the first docs routes for CLI users and embedders."
---

`@earendil-works/pi-coding-agent` (package version `0.84.1`) is the coding-agent package for **pi**: a minimal terminal coding harness with the `pi` binary (`dist/cli.js`), default tools `read` / `write` / `edit` / `bash`, session management, and four run surfaces—interactive TUI, print/JSON, RPC process integration, and an embeddable SDK.

## What pi is

Pi is designed so you adapt the agent to your workflow instead of forking internals. Core product facts from the package surface:

| Item | Value |
|------|--------|
| npm package | `@earendil-works/pi-coding-agent` |
| description | Coding agent CLI with `read`, `bash`, `edit`, `write` tools and session management |
| bin | `pi` → `dist/cli.js` |
| main / types | `./dist/index.js`, `./dist/index.d.ts` |
| module type | ESM (`"type": "module"`) |
| config dir key | `piConfig.configDir` = `.pi` |
| Node engine | `>=22.19.0` |
| license | MIT |
| repository | `github.com/earendil-works/pi` (`packages/coding-agent`) |

**Defaults, not a full product suite:** pi ships with strong defaults and intentionally omits built-in features such as sub-agents and plan mode. Those capabilities come from skills, prompt templates, TypeScript extensions, themes, or third-party Pi packages (npm or git)—or by asking pi to build what you need.

**Extension surfaces (product terms):**

- TypeScript **extensions**
- **Skills**
- **Prompt templates**
- **Themes**
- Shareable **Pi packages** that bundle the above

## Four run modes

README product modes and the CLI/package entry points that implement them:

| Product mode | Role | Entry / flags (evidence) |
|--------------|------|---------------------------|
| **Interactive** | Full terminal UI (header, messages, editor, footer) | Default `pi` TUI; optional `--tui-mode regular\|fullscreen` |
| **Print / JSON** | Non-interactive output for scripts and automation | `--print` / `-p` (optional message); CLI `--mode text\|json` |
| **RPC** | Process integration over a machine protocol | `--mode rpc`; package export `./rpc-entry` → `dist/rpc-entry.js` |
| **SDK** | Embed in your own apps | Package main export `./dist/index.js`; examples use `createAgentSession()` |

CLI mode values accepted by `--mode`:

```ts
export type Mode = "text" | "json" | "rpc";
```

Shared runtime: `AgentSession` is the core lifecycle abstraction used across interactive, print, and RPC. Modes add their own I/O on top of the same session engine (agent state, event subscription + session persistence, model/thinking control, compaction, bash, session switch/branch).

```mermaid
flowchart TB
  subgraph entry ["Entry surfaces"]
    CLI["pi bin<br/>dist/cli.js"]
    RPC["@earendil-works/pi-coding-agent/rpc-entry<br/>dist/rpc-entry.js"]
    SDK["package main<br/>dist/index.js<br/>createAgentSession()"]
  end

  subgraph modes ["Run modes"]
    I["Interactive TUI"]
    P["Print / JSON<br/>--print, --mode text|json"]
    R["RPC<br/>--mode rpc"]
    S["SDK embed"]
  end

  subgraph core ["Shared session core"]
    AS["AgentSession<br/>src/core/agent-session.ts"]
    Tools["Built-in tools<br/>read · bash · edit · write"]
  end

  CLI --> I
  CLI --> P
  CLI --> R
  RPC --> R
  SDK --> S
  I --> AS
  P --> AS
  R --> AS
  S --> AS
  AS --> Tools
```

<Note>
Print/JSON and RPC are selected on the CLI with `--mode` and/or `--print`. SDK embedding uses the package main export rather than the interactive TUI. Details for each mode live on the dedicated run-mode, RPC, and SDK pages.
</Note>

## Default tools

By default the model receives four built-in tools:

| Tool | Role (product default) |
|------|-------------------------|
| `read` | Read project files |
| `write` | Create / write files |
| `edit` | Edit existing files |
| `bash` | Run shell commands |

`AgentSessionConfig` documents the same default active set:

```ts
/** Initial active built-in tool names. Default: [read, bash, edit, write] */
initialActiveToolNames?: string[];
```

Tool exposure can be constrained at session construction:

| Config field | Effect |
|--------------|--------|
| `allowedToolNames` | Allowlist: only these tool names are exposed |
| `excludedToolNames` | Denylist: these tool names are not exposed |
| `baseToolsOverride` | Replace base tools (custom runtimes); synthesized into minimal `ToolDefinition`s |
| `customTools` | SDK custom tools registered outside extensions |

CLI flags that filter tools (parsed in `src/cli/args.ts`):

| Flag | Short | Effect |
|------|-------|--------|
| `--tools <names>` | `-t` | Comma-separated tool name list |
| `--exclude-tools <names>` | `-xt` | Comma-separated denylist |
| `--no-tools` | `-nt` | Disable tools |
| `--no-builtin-tools` | `-nbt` | Disable built-in tools |

Capabilities beyond the four defaults come from skills, prompt templates, extensions, or Pi packages—not from expanding the built-in set by default.

## Package exports (embedders)

Public npm surface for `@earendil-works/pi-coding-agent`:

| Export | Path | Use |
|--------|------|-----|
| `.` (main) | `dist/index.js` (+ types) | SDK / programmatic embedding |
| `./rpc-entry` | `dist/rpc-entry.js` | RPC process entry |
| `./client` | `dist/client/index.js` (+ types) | Client API surface |
| bin `pi` | `dist/cli.js` | CLI |

Published package files include `dist`, `docs`, `examples`, `containerization.md`, `CHANGELOG.md`, and `npm-shrinkwrap.json`.

Examples under `packages/coding-agent/examples/`:

- **`sdk/`** — programmatic usage via `createAgentSession()` (models, prompts, tools, extensions, session management)
- **`extensions/`** — lifecycle handlers, custom tools, commands/keybindings, UI, git hooks, system prompt / compaction, external integrations, custom providers

## First run (CLI)

Install paths:

```bash
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
```

`--ignore-scripts` disables dependency lifecycle scripts; pi does not require install scripts for normal npm installs.

```bash
curl -fsSL https://pi.dev/install.sh | sh
```

Authenticate, then start interactive pi:

```bash
# API key (example provider env)
export ANTHROPIC_API_KEY=sk-ant-...
pi
```

```bash
# Or subscription /login in the TUI
pi
/login   # then select provider
```

Providers are BYOK and multi-provider: subscriptions (for example Anthropic Claude Pro/Max, OpenAI ChatGPT Plus/Pro Codex, GitHub Copilot) and many API-key providers (Anthropic, OpenAI, Azure OpenAI, Google Gemini/Vertex, Amazon Bedrock, xAI, OpenRouter, and others listed in the package README). Custom providers can be added via `~/.pi/agent/models.json` when they speak a supported API (OpenAI, Anthropic, Google); custom APIs or OAuth use extensions. Local llama.cpp router is configured with `/login llama.cpp` and `/llama`.

## Interactive UI (quick map)

Default interactive layout:

1. **Startup header** — shortcuts (`/hotkeys`), loaded `AGENTS.md`, prompt templates, skills, extensions  
2. **Messages** — user/assistant turns, tool calls/results, notifications, errors, extension UI  
3. **Editor** — input; border color reflects thinking level  
4. **Footer** — cwd, session name, token/cache usage, cost, context usage, current model  

Representative slash commands (type `/` in the editor):

| Command | Description |
|---------|-------------|
| `/login`, `/logout` | Provider credentials |
| `/model` | Switch models |
| `/settings` | Thinking level, theme, message delivery, transport |
| `/resume`, `/new` | Session lifecycle |
| `/tree` | Jump in the session tree |
| `/fork`, `/clone` | Branch-related session ops |
| `/compact [prompt]` | Manual context compaction |
| `/export [file]` | Export session to HTML or JSONL |

Skills appear as `/skill:name`; prompt templates expand as `/templatename`. Extensions can register additional commands and replace or decorate the editor.

## Shared session core

`AgentSession` (`src/core/agent-session.ts`) owns:

- Agent state access  
- Event subscription with automatic session persistence  
- Model and thinking-level management  
- Compaction (manual and auto; reasons include `manual`, `threshold`, `overflow`)  
- Bash execution  
- Session switching and branching  

Session events include (non-exhaustive): `agent_end` (with `willRetry`), `agent_settled`, `queue_update`, `compaction_start` / `compaction_end`, `entry_appended`, `session_info_changed`, `thinking_level_changed`, auto-retry and summarization-retry events, and `bash_execution_update`.

Resource loading for a session covers extensions, skills, prompts, themes, context files, and the system prompt via `resourceLoader` on `AgentSessionConfig`.

## Useful CLI flags (overview)

Parsed in `src/cli/args.ts` (not a full flag reference):

| Area | Flags |
|------|--------|
| Mode / print | `--mode text\|json\|rpc`, `--print` / `-p` |
| Model / auth | `--provider`, `--model`, `--api-key`, `--models`, `--list-models`, `--thinking` |
| Session | `--continue` / `-c`, `--resume` / `-r`, `--session`, `--session-id`, `--session-dir`, `--fork`, `--name` / `-n`, `--no-session` |
| Tools | `--tools` / `-t`, `--exclude-tools` / `-xt`, `--no-tools` / `-nt`, `--no-builtin-tools` / `-nbt` |
| Resources | `--extension` / `-e`, `--no-extensions` / `-ne`, `--skill`, `--no-skills` / `-ns`, `--prompt-template`, `--no-prompt-templates` / `-np`, `--theme`, `--no-themes`, `--no-context-files` / `-nc` |
| Other | `--system-prompt`, `--append-system-prompt`, `--export`, `--tui-mode`, `--offline`, `--verbose`, `--approve` / `-a`, `--no-approve` / `-na`, `@file` args |

Valid thinking levels: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`.

## Who should open which docs next

| Audience | Start here |
|----------|------------|
| New CLI users | Install → authenticate → interactive session with default tools |
| Script / CI users | Print/JSON and CLI flags |
| Process integrators | RPC entry and `--mode rpc` |
| App embedders | Package main export, `createAgentSession()`, SDK examples |
| Extenders | Skills, extensions, prompt templates, themes, Pi packages |

## Next

<CardGroup cols={2}>
  <Card title="Installation" href="/installation">
    npm and installer paths, bin entry, package exports, and verification for `@earendil-works/pi-coding-agent`.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    Install, authenticate with API key or `/login`, start interactive `pi`, and confirm the default tool surface.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK: invocation, entry points, and when to choose each.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed pi via the package main export: agent construction, hooks, custom models, tools, and settings.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default read/write/edit/bash tools, extension tools, allowlist and exclude-tools filters.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    `pi` binary flags, argument parsing, auth-related entry points, and package bin wiring.
  </Card>
  <Card title="Package exports" href="/package-exports">
    Public npm surface: main, rpc-entry, client, bin name, and `piConfig.configDir`.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes for minimal agents, custom models, skills, tools, extensions, and sessions.
  </Card>
</CardGroup>

---

## 02. Installation

> npm and installer install paths, bin entry, package exports, and verification for @earendil-works/pi-coding-agent.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/02-installation.md
- Generated: 2026-08-09T03:38:26.591Z

### Source Files

- `packages/coding-agent/package.json`
- `packages/coding-agent/README.md`
- `packages/coding-agent/CHANGELOG.md`
- `packages/coding-agent/src/cli/args.ts`

---
title: "Installation"
description: "npm and installer install paths, bin entry, package exports, and verification for @earendil-works/pi-coding-agent."
---

`@earendil-works/pi-coding-agent` publishes the `pi` CLI (`bin.pi` → `dist/cli.js`) and embeddable ESM exports for SDK, RPC, and client use. Current package version in tree is **0.84.1**. Runtime requirement: **Node.js `>=22.19.0`**.

## Prerequisites

| Requirement | Value |
|-------------|--------|
| Package name | `@earendil-works/pi-coding-agent` |
| Node engine | `>=22.19.0` |
| Module type | ESM (`"type": "module"`) |
| Config directory name | `piConfig.configDir` = `.pi` |
| License | MIT |

Provider credentials are **not** required to install the package. Authentication (API keys or `/login`) is required to run agent turns against a model.

## Install paths

### npm global CLI

```bash
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
```

`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs.

### Installer script

```bash
curl -fsSL https://pi.dev/install.sh | sh
```

### Project / library dependency

For embedding (SDK / RPC / client), add the package as a dependency and import from the published export map (see [Package surface](#package-surface)):

```bash
npm install @earendil-works/pi-coding-agent
```

Use a package manager and lockfile of your choice; the published identity and export paths are the same.

<Note>
CLI subcommand `pi install <source>` installs **extension sources** into settings. It does not install the `@earendil-works/pi-coding-agent` npm package.
</Note>

## Package surface

### Identity and bin

| Field | Value |
|-------|--------|
| `name` | `@earendil-works/pi-coding-agent` |
| `version` | `0.84.1` |
| `description` | Coding agent CLI with read, bash, edit, write tools and session management |
| `bin.pi` | `dist/cli.js` |
| `main` | `./dist/index.js` |
| `types` | `./dist/index.d.ts` |
| `piConfig.configDir` | `.pi` |

Build marks `dist/cli.js` and `dist/rpc-entry.js` executable (`chmod +x` in the package `build` script).

### Export map

| Export | Import path | Resolves to |
|--------|-------------|-------------|
| Main / SDK | `@earendil-works/pi-coding-agent` | `./dist/index.js` (+ `./dist/index.d.ts`) |
| RPC entry | `@earendil-works/pi-coding-agent/rpc-entry` | `./dist/rpc-entry.js` |
| Client | `@earendil-works/pi-coding-agent/client` | `./dist/client/index.js` (+ types) |

### Published package files

npm publishes: `dist`, `docs`, `examples`, `containerization.md`, `CHANGELOG.md`, `npm-shrinkwrap.json`.

### Related monorepo dependencies

Runtime dependencies (same major line `^0.84.1`):

- `@earendil-works/pi-agent-core`
- `@earendil-works/pi-ai`
- `@earendil-works/pi-client`
- `@earendil-works/pi-protocol`
- `@earendil-works/pi-tui`

Optional: `@mariozechner/clipboard@0.3.9`.

## Verification

<Steps>
  <Step title="Confirm the binary">
    After a global or installer install, the `pi` command should resolve on `PATH` (wired from package `bin.pi` → `dist/cli.js`).
  </Step>
  <Step title="Check version and help">
    ```bash
    pi --version
    # or
    pi -v

    pi --help
    # or
    pi -h
    ```
    Help identifies the app as an AI coding assistant with **read**, **bash**, **edit**, and **write** tools.
  </Step>
  <Step title="Optional auth readiness">
    From 0.84.1: `pi auth check` verifies provider/model credentials and can optionally emit the resolved credential.
  </Step>
  <Step title="First interactive run">
    API key example (Anthropic shown in README; other providers use their own env vars):

    ```bash
    export ANTHROPIC_API_KEY=sk-ant-...
    pi
    ```

    Or start `pi` and authenticate with a subscription:

    ```bash
    pi
    /login   # then select provider
    ```
  </Step>
</Steps>

Default model tools after a successful start: `read`, `write`, `edit`, `bash`.

## Post-install layout (defaults)

| Path / surface | Role |
|----------------|------|
| `~/.pi/agent/sessions/` | Session JSONL storage (auto-save; organized by working context) |
| `~/.pi/agent/models.json` | Custom providers/models (supported API shapes) |
| `~/.pi/agent/keybindings.json` | Custom keybindings |
| `piConfig.configDir` = `.pi` | Package-declared config directory name |

## Update after install

CLI help lists:

```text
pi update [source|self|pi]   Update pi, extensions, or model catalogs
```

Also:

| Command | Purpose |
|---------|---------|
| `pi list` | List installed extensions from settings |
| `pi config [-l]` | TUI to enable/disable package resources (Tab switches scope) |
| `pi auth <command>` | Print credentials or check provider readiness |
| `pi <command> --help` | Help for install/remove/uninstall/update/list/config/auth |

Provider model catalogs can refresh automatically; force with `pi update --models`.

## Standalone binary build (package scripts)

The package defines `build:binary`, which compiles a Bun standalone binary to `dist/pi` using:

```bash
bun build --compile --no-compile-autoload-bunfig ...
```

Changelog 0.84.1: standalone Bun binaries no longer crash on startup when the cwd contains a `bunfig.toml` with `preload`, because compile uses `--no-compile-autoload-bunfig`. This is a **package build** concern, not a separate public install recipe beyond the npm and `install.sh` paths above.

## Run modes after install

Pi runs in four modes (post-install choice, not separate packages):

| Mode | Typical entry |
|------|----------------|
| Interactive TUI | `pi` |
| Print / JSON | `pi --print` / `-p`, `--mode text\|json` |
| RPC | `--mode rpc` or package export `./rpc-entry` |
| SDK | import package main export |

Default CLI provider name in help text: `google` (`--provider <name>`).

## Troubleshooting install and first start

| Symptom | Evidence-backed note |
|---------|----------------------|
| Engine too old | Package requires Node `>=22.19.0` |
| Lifecycle script policy | Prefer `npm install -g --ignore-scripts`; install scripts are not required for normal installs |
| Wrong `pi install` usage | That subcommand manages extension **sources** in settings, not npm package install |
| Auth before first useful turn | Set a provider API key env var, or use `/login`; use `pi auth check` for readiness |
| Bun standalone + `bunfig.toml` preload | Fixed in 0.84.1 via `--no-compile-autoload-bunfig` on binary compile |

Platform notes referenced in the package README (not expanded in this evidence set): Windows, Termux (Android), tmux, terminal setup, shell aliases under package `docs/`.

## Next

<CardGroup>
  <Card title="Quickstart" href="/quickstart">
    Install, authenticate, start interactive `pi`, confirm default tools.
  </Card>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth `/login`, credential storage, and auth checks.
  </Card>
  <Card title="Package exports" href="/package-exports">
    Full public export map: main, rpc-entry, client, bin, `piConfig`.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Flags, subcommands, and bin wiring for the `pi` binary.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed via the package main export and control hooks.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK invocation paths.
  </Card>
</CardGroup>

---

## 03. Quickstart

> Install, authenticate with API key or /login, start interactive pi, and confirm the default read/write/edit/bash tool surface.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/03-quickstart.md
- Generated: 2026-08-09T03:38:28.117Z

### Source Files

- `packages/coding-agent/README.md`
- `packages/coding-agent/package.json`
- `packages/coding-agent/examples/sdk/01-minimal.ts`
- `packages/coding-agent/src/cli/auth-command.ts`
- `packages/coding-agent/src/core/auth-storage.ts`

---
title: "Quickstart"
description: "Install, authenticate with API key or /login, start interactive pi, and confirm the default read/write/edit/bash tool surface."
---

`@earendil-works/pi-coding-agent` ships the `pi` binary (`dist/cli.js`) and exposes a coding agent CLI whose package description is a session-managed surface with `read`, `bash`, `edit`, and `write` tools. Interactive use is the default entry: install the package, authenticate with an environment API key or interactive `/login`, run `pi`, then talk to the model against that four-tool default.

## Prerequisites

| Requirement | Value |
|-------------|--------|
| Package | `@earendil-works/pi-coding-agent` |
| Node.js | `>=22.19.0` (`engines.node`) |
| CLI bin | `pi` → `dist/cli.js` |
| Config directory name | `piConfig.configDir` = `.pi` |

Pi is a minimal terminal coding harness that runs in four modes (interactive, print/JSON, RPC, SDK). This page covers the interactive path only.

## Install

<Tabs>
  <Tab title="npm">
    ```bash
    npm install -g --ignore-scripts @earendil-works/pi-coding-agent
    ```

    `--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs.
  </Tab>
  <Tab title="Installer">
    ```bash
    curl -fsSL https://pi.dev/install.sh | sh
    ```
  </Tab>
</Tabs>

### Verify the binary

After install, `pi` must resolve on your `PATH`. The published package wires:

```json
"bin": {
  "pi": "dist/cli.js"
}
```

## Authenticate

Authenticate with either a provider API key in the environment or a subscription via interactive `/login`. Both paths feed the same provider/model selection surface (`/model` or Ctrl+L after a successful login).

### Option A: API key (environment)

```bash
export ANTHROPIC_API_KEY=sk-ant-...
pi
```

API-key authentication is BYOK: set the provider’s key in the environment, then start `pi`. The README documents many API-key providers (Anthropic, OpenAI, Azure OpenAI, Google Gemini, xAI, OpenRouter, and others). Use the key that matches the provider you intend to select.

### Option B: Subscription (`/login`)

```bash
pi
/login  # Then select provider
```

Documented subscription targets include:

- Anthropic Claude Pro/Max
- OpenAI ChatGPT Plus/Pro (Codex)
- GitHub Copilot

Also available interactively: `/logout` for credential management, and `/login llama.cpp` for the llama.cpp router server (then `/llama` for model load/unload, `/model` to select).

### Credential storage (what `/login` writes)

Credentials are stored in `auth.json` under the agent directory (default path construction: `join(getAgentDir(), "auth.json")`).

| Property | Behavior |
|----------|----------|
| File mode | Written with mode `0o600` |
| Parent directory | Created with mode `0o700` when missing |
| Empty bootstrap | Missing file is created as `{}` before locked writes |
| Concurrency | File-locked reads/writes via `proper-lockfile` |

Supported credential shapes in `auth.json` (per provider id):

| `type` | Required fields |
|--------|-----------------|
| `api_key` | Optional `key` (string) and/or `env` (object of string values) |
| `oauth` | `access` (string), `refresh` (string), `expires` (finite number) |

Invalid shapes throw (`Invalid auth.json: expected an object`, or `Invalid auth.json credential for provider "<id>"`). Missing file loads as empty credentials for read-only storage (`ENOENT` → `{}`).

### Optional: non-interactive auth CLI

For scripts and checks (not required for first interactive use):

```text
pi auth print-api-key [--provider <provider>] [--model <model>]
pi auth print-bearer-token [--provider <provider>] [--model <model>] [--min-expiry <duration>]
pi auth check [--provider <provider>] [--model <model>] [--json] [--credentials] [--no-refresh]
```

| Rule | Detail |
|------|--------|
| Scope | Auth commands require at least one of `--provider` or `--model` |
| Flags accepted | Only `--provider` and `--model` (plus the subcommand-specific flags above) |
| OAuth refresh | `auth check` refreshes expired OAuth credentials by default; `--no-refresh` disables that |
| `--min-expiry` | Only for `print-bearer-token`; duration like `30m` or `1h` |
| `--json` / `--credentials` / `--no-refresh` | Only for `auth check` |

Unknown auth subcommands fail with:

```text
Unknown auth command "...". Use "pi auth print-api-key", "pi auth print-bearer-token", or "pi auth check".
```

## Start interactive pi

```bash
pi
```

Then talk to pi in the editor. The interactive layout is:

| Region | Contents |
|--------|----------|
| Startup header | Shortcuts (`/hotkeys` for all), loaded AGENTS.md files, prompt templates, skills, extensions |
| Messages | User messages, assistant responses, tool calls/results, notifications, errors, extension UI |
| Editor | Input area; border color indicates thinking level |
| Footer | Working directory, session name, token/cache usage, cost, context usage, current model |

### Useful first commands

Type `/` in the editor to open commands:

| Command | Purpose |
|---------|---------|
| `/login`, `/logout` | Manage provider credentials |
| `/model` | Switch models (also Ctrl+L) |
| `/settings` | Thinking level, theme, message delivery, transport |
| `/session` | Session info (file, ID, messages, tokens, cost) |
| `/new` | Start a new session |
| `/resume` | Pick from previous sessions |

Editor affordances for day-one use:

| Feature | How |
|---------|-----|
| File reference | Type `@` to fuzzy-search project files |
| Path completion | Tab |
| Multi-line | Shift+Enter (Ctrl+Enter on Windows Terminal) |
| Bash passthrough | `!command` runs and sends output to the model; `!!command` runs without sending |

## Confirm the default tool surface

By default, pi gives the model four tools:

| Tool | Role |
|------|------|
| `read` | Read project content |
| `write` | Create/overwrite files |
| `edit` | Edit existing files |
| `bash` | Run shell commands |

This matches the package description: *“Coding agent CLI with read, bash, edit, write tools and session management.”*

Verification signals in interactive mode:

1. After a prompt that needs workspace access, tool calls appear in the **Messages** region.
2. Footer totals include usage reported by tools (not only assistant text).
3. No extra built-in tools ship by default (features like subagents and plan mode are intentionally skipped; add them via skills, prompt templates, extensions, or pi packages).

### After auth: pick a model

Configured provider catalogs refresh automatically. Force a refresh with:

```bash
pi update --models
```

Then select any tool-capable model from an authenticated provider with `/model` (or Ctrl+L).

## Minimal programmatic path (optional)

If you embed instead of using the TUI, the package main export exposes `createAgentSession`. Defaults discover skills, extensions, tools, and context files from `cwd` and `~/.pi/agent`; the model comes from settings or the first available:

```ts
import { createAgentSession } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession();

try {
	session.subscribe((event) => {
		if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
			process.stdout.write(event.assistantMessageEvent.delta);
		}
	});

	await session.prompt("What files are in the current directory?");
	session.state.messages.forEach((msg) => {
		console.log(msg);
	});
} finally {
	session.dispose();
}
```

Package public surface used here:

| Export | Path |
|--------|------|
| Main | `@earendil-works/pi-coding-agent` → `./dist/index.js` |
| RPC entry | `@earendil-works/pi-coding-agent/rpc-entry` |
| Client | `@earendil-works/pi-coding-agent/client` |

## Quick failure checks

| Symptom | Check |
|---------|--------|
| Install scripts blocked | Expected when using `--ignore-scripts`; not required for normal npm installs |
| No model / no auth | Set an API key env var or run `/login` and select a provider |
| `auth check` / print credential errors | Pass `--provider` and/or `--model`; avoid unrelated flags |
| Invalid credentials file | Repair `auth.json` shape (`api_key` or `oauth` fields above); invalid objects throw on load |
| Auth file lock contention | Concurrent writers use file locks; compromise of the lock surfaces as `Auth storage lock was compromised` |
| Node too old | Use Node `>=22.19.0` |

## End-to-end checklist

<Steps>
  <Step title="Install">
    ```bash
    npm install -g --ignore-scripts @earendil-works/pi-coding-agent
    ```
    Confirm `pi` is on `PATH`.
  </Step>
  <Step title="Authenticate">
    Either export a provider API key (for example `ANTHROPIC_API_KEY`) or start `pi` and run `/login`.
  </Step>
  <Step title="Start interactive mode">
    ```bash
    pi
    ```
    Confirm startup header, editor, and footer render.
  </Step>
  <Step title="Select model">
    Use `/model` (or Ctrl+L) on an authenticated provider.
  </Step>
  <Step title="Confirm tools">
    Prompt the agent to inspect the working directory or edit a file. Expect tool activity under the default `read` / `write` / `edit` / `bash` surface only.
  </Step>
</Steps>

## Next

<CardGroup>
  <Card title="Installation" href="/installation">
    npm and installer paths, bin entry, package exports, and verification for `@earendil-works/pi-coding-agent`.
  </Card>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth login, credential storage, auth checks, and refresh behavior.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default `read`/`write`/`edit`/`bash` tools, extension tools, and allowlist filters.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK modes and when to choose each.
  </Card>
  <Card title="Providers and models" href="/providers-and-models">
    Built-in providers, model catalogs, refresh, and selection.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Binary flags, auth subcommands, and package bin wiring.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed with `createAgentSession` and the main package export.
  </Card>
  <Card title="Overview" href="/overview">
    What pi is, the four run modes, and first docs routes.
  </Card>
</CardGroup>

---

## 04. Run modes

> Interactive, print/JSON, RPC, and SDK modes: how each is invoked, entry points, and when to choose them.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/04-run-modes.md
- Generated: 2026-08-09T03:38:40.635Z

### Source Files

- `packages/coding-agent/README.md`
- `packages/coding-agent/package.json`
- `packages/coding-agent/src/cli/args.ts`
- `packages/coding-agent/src/core/agent-session-runtime.ts`
- `packages/coding-agent/examples/sdk/13-session-runtime.ts`

---
title: "Run modes"
description: "Interactive, print/JSON, RPC, and SDK modes: how each is invoked, entry points, and when to choose them."
---

Pi (`@earendil-works/pi-coding-agent`) exposes **four run modes** from one package: interactive TUI, print/JSON (non-interactive CLI), RPC for process integration, and an SDK for embedding. The CLI binary is `pi` (`dist/cli.js`). CLI non-interactive modes are selected with `--mode` (`text` | `json` | `rpc`) and/or `--print` / `-p`. Programmatic embedding uses the package main export and optionally `./rpc-entry`.

## Mode overview

| Mode | How you enter it | Primary surface | Typical use |
|------|------------------|-----------------|-------------|
| **Interactive** | `pi` (default TUI) | Terminal UI under `modes/interactive` | Day-to-day coding agent sessions |
| **Print / text** | `--print` / `-p`, and/or `--mode text` | Non-interactive CLI stdout | One-shot prompts, scripts, pipes |
| **JSON** | `--mode json` | Structured CLI output | Machine-readable agents and automation |
| **RPC** | `--mode rpc`, or package export `./rpc-entry` | JSON command stream / process integration | Host apps that spawn or control pi as a process |
| **SDK** | `import` from `@earendil-works/pi-coding-agent` | `createAgentSession*`, `AgentSessionRuntime` | Embed sessions in your own app without the TUI |

README product language groups print and JSON as “print or JSON,” and RPC as “process integration.” The CLI parser’s `Mode` type is only `"text" | "json" | "rpc"`—interactive is the default path when those mode flags are not used for a non-interactive run.

```mermaid
flowchart TB
  subgraph entry["Entry points"]
    BIN["bin: pi → dist/cli.js"]
    MAIN["export . → dist/index.js"]
    RPCX["export ./rpc-entry → dist/rpc-entry.js"]
    CLIENT["export ./client"]
  end

  subgraph cli["CLI modes"]
    INT["Interactive TUI\n(default)"]
    TEXT["--mode text / --print"]
    JSON["--mode json"]
    RPC["--mode rpc"]
  end

  subgraph sdk["In-process embedding"]
    SVC["createAgentSessionServices"]
    SESS["createAgentSessionFromServices"]
    RT["createAgentSessionRuntime\nAgentSessionRuntime"]
  end

  BIN --> INT
  BIN --> TEXT
  BIN --> JSON
  BIN --> RPC
  RPCX --> RPC
  MAIN --> SVC --> SESS
  MAIN --> RT
  RT --> SESS
```

## Package entry points

From `packages/coding-agent/package.json`:

| Surface | Path / export | Role |
|---------|---------------|------|
| CLI binary | `"bin": { "pi": "dist/cli.js" }` | User-facing `pi` command |
| Main / SDK | `"."` → `dist/index.js` (+ types) | Programmatic agent construction |
| RPC entry | `"./rpc-entry"` → `dist/rpc-entry.js` | Process-integration entry (chmod +x in build) |
| Client | `"./client"` → `dist/client/index.js` | Client library surface |
| Config dir name | `piConfig.configDir`: `".pi"` | Package-declared agent config directory name |

Requires **Node `>=22.19.0`**.

## CLI mode flags

Argument parsing lives in `packages/coding-agent/src/cli/args.ts`.

### Mode selection

<ParamField body="--mode" type='"text" \| "json" \| "rpc"'>
Sets `Args.mode`. Only these three string values are accepted; other values are ignored (mode stays unset).
</ParamField>

<ParamField body="--print, -p" type="boolean (+ optional message)">
Sets `Args.print = true`. If the next argument is present and is not a flag/`@file` arg (with a special exception for args starting with `---`), it is consumed as a prompt message on `Args.messages`.
</ParamField>

```bash
# Mode values accepted by the parser
pi --mode text ...
pi --mode json ...
pi --mode rpc ...

# Print flag (optionally with an immediate message)
pi --print "your prompt"
pi -p "your prompt"
```

### Related run-shaping flags

These apply across CLI modes (parser-supported); they do not select a mode by themselves:

| Flag | Effect on `Args` |
|------|------------------|
| `--provider`, `--model`, `--api-key` | Provider/model/API key overrides |
| `--system-prompt`, `--append-system-prompt` | System prompt control |
| `--thinking <level>` | `off` \| `minimal` \| `low` \| `medium` \| `high` \| `xhigh` \| `max` |
| `--continue` / `-c`, `--resume` / `-r` | Session continue/resume |
| `--session`, `--session-id`, `--session-dir`, `--fork`, `--no-session`, `--name` / `-n` | Session identity and persistence |
| `--tools` / `-t`, `--exclude-tools` / `-xt`, `--no-tools` / `-nt`, `--no-builtin-tools` / `-nbt` | Tool allow/exclude surface |
| `--extension` / `-e`, `--no-extensions` / `-ne` | Extensions |
| `--skill`, `--no-skills` / `-ns` | Skills |
| `--prompt-template`, `--no-prompt-templates` / `-np` | Prompt templates |
| `--theme`, `--no-themes` | Themes |
| `--no-context-files` / `-nc` | Skip context files |
| `--tui-mode regular\|fullscreen` | Interactive TUI layout mode |
| `--approve` / `-a`, `--no-approve` / `-na` | Project trust override |
| `--offline`, `--verbose`, `--list-models`, `--export` | Misc operational flags |
| `@path` | File arguments (`fileArgs`) |
| bare strings | Prompt messages (`messages`) |
| unknown `--flags` | Collected as `unknownFlags` (extension CLI flags) |

Invalid short options produce `diagnostics` errors (`Unknown option: ...`). Invalid thinking levels produce warnings. Invalid `--tui-mode` values produce errors.

## Interactive mode

Default coding-agent experience: start authenticated `pi` with no print/RPC mode flags.

```bash
export ANTHROPIC_API_KEY=sk-ant-...
pi
# or: pi → /login (subscription)
```

By default the model gets four tools: **`read`**, **`write`**, **`edit`**, and **`bash`**. Capabilities expand via skills, prompt templates, extensions, or Pi packages.

### UI layout

| Region | Contents |
|--------|----------|
| Startup header | Shortcuts (`/hotkeys`), loaded AGENTS.md, prompt templates, skills, extensions |
| Messages | User/assistant turns, tool calls/results, notifications, errors, extension UI |
| Editor | Input; border color reflects thinking level. Can be replaced by built-in `/settings` or extension UI |
| Footer | Working directory, session name, token/cache usage, cost, context usage, current model |

Theme and asset files for this mode live under `src/modes/interactive/` (copied into `dist/modes/interactive/` at build).

### Editor inputs

| Feature | Behavior |
|---------|----------|
| `@` | Fuzzy-search project files |
| Tab | Path completion |
| Shift+Enter (Ctrl+Enter on Windows Terminal) | Multi-line input |
| Ctrl+G | External editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, else `nano`) |
| Ctrl+V (Alt+V on Windows) | Paste image/text; drag images onto terminal |
| `!command` | Run bash and send output to the LLM |
| `!!command` | Run bash without sending |

### Commands (selected)

Type `/` in the editor. Extensions register custom commands; skills appear as `/skill:name`; prompt templates expand as `/templatename`.

| Command | Description |
|---------|-------------|
| `/login`, `/logout` | Provider credentials |
| `/model` | Switch models |
| `/scoped-models` | Enable/disable models for Ctrl+P cycling |
| `/settings` | Thinking level, theme, message delivery, transport |
| `/resume`, `/new`, `/name`, `/session` | Session lifecycle and metadata |
| `/tree`, `/fork`, `/clone` | Session tree / branch operations |
| `/compact [prompt]` | Manual compaction |
| `/export [file]` | Export session to HTML or JSONL |
| `/trust` | Save project trust decision (restart required) |

### Interactive-only TUI flag

<ParamField body="--tui-mode" type='"regular" \| "fullscreen"'>
Sets interactive layout mode. Requires one of those two values.
</ParamField>

## Print / text and JSON modes

Use when you do **not** want the interactive TUI: one-shot prompts, scripting, or structured automation.

| Intent | Flags |
|--------|--------|
| Print-style non-interactive run | `--print` / `-p` (optional message immediately after the flag) |
| Explicit text mode | `--mode text` |
| Structured JSON mode | `--mode json` |

```bash
pi -p "summarize the last commit"
pi --mode json -p "list open TODOs in src/"
```

Prompt text can also be supplied as bare positional arguments (`Args.messages`). File inputs use `@path` → `fileArgs`.

<Note>
The parser stores `print` and `mode` separately. Exact dispatch (for example whether `--print` alone forces text mode, exit codes, or JSON schemas) is outside this page’s supplied CLI dispatch sources; use `--mode` when you need an explicit mode value.
</Note>

## RPC mode

RPC is for **process integration**: a host controls pi over a command/event channel rather than a human TUI.

| Invocation | Location |
|------------|----------|
| CLI | `pi --mode rpc ...` |
| Package export | `@earendil-works/pi-coding-agent/rpc-entry` → `dist/rpc-entry.js` |

Build marks `dist/rpc-entry.js` executable alongside `dist/cli.js`.

Choose RPC when another process must drive prompts, lifecycle, and streaming without embedding the TypeScript API in-process. Command IDs, compaction constraints, and stream details belong on the dedicated RPC page.

## SDK mode

Embed pi in your own Node application via the **main package export** (`"."` → `dist/index.js`). This path owns sessions without the interactive TUI.

### Session construction pattern

`examples/sdk/13-session-runtime.ts` shows the intended layering:

1. `createAgentSessionServices({ cwd })` — cwd-bound services  
2. `createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })` — session + related result fields  
3. `createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager })` — runtime that can **replace** the active session  
4. Rebind subscriptions/extensions after each replacement to `runtime.session`  
5. `runtime.dispose()` when finished  

```ts
import {
  type CreateAgentSessionRuntimeFactory,
  createAgentSessionFromServices,
  createAgentSessionRuntime,
  createAgentSessionServices,
  getAgentDir,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const createRuntime: CreateAgentSessionRuntimeFactory = async ({
  cwd,
  sessionManager,
  sessionStartEvent,
}) => {
  const services = await createAgentSessionServices({ cwd });
  return {
    ...(await createAgentSessionFromServices({
      services,
      sessionManager,
      sessionStartEvent,
    })),
    services,
    diagnostics: services.diagnostics,
  };
};

const runtime = await createAgentSessionRuntime(createRuntime, {
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  sessionManager: SessionManager.create(process.cwd()),
});

// bind to runtime.session, then:
await runtime.newSession();
// or: await runtime.switchSession(sessionFile);
await runtime.dispose();
```

### `AgentSessionRuntime` responsibilities

`AgentSessionRuntime` owns the current `AgentSession` plus cwd-bound `AgentSessionServices`.

| Member / method | Role |
|-----------------|------|
| `session`, `services`, `cwd`, `diagnostics`, `modelFallbackMessage` | Current runtime state |
| `setRebindSession(fn)` | Host rebinds after session replacement |
| `setBeforeSessionInvalidate(fn)` | Sync UI teardown after `session_shutdown`, before invalidate (must not yield) |
| `newSession({ parentSession?, setup?, withSession? })` | Teardown + create new session (`reason: "new"`) |
| `switchSession(path, { cwdOverride?, withSession?, projectTrustContextFactory? })` | Resume another session file (`reason: "resume"`) |
| Replacement hooks | `session_before_switch` / `session_before_fork` may **cancel** (`{ cancelled: true }`) |
| Teardown order | `session.abort()` → `session_shutdown` → `beforeSessionInvalidate` → `session.dispose()` |

`CreateAgentSessionRuntimeFactory` recreates cwd-bound services for the effective cwd, then builds the next `AgentSession`. Creation failures propagate to the caller.

`SessionImportFileNotFoundError` is thrown when `/import` points at a missing JSONL path.

## Choosing a mode

| If you need… | Choose | Entry |
|--------------|--------|--------|
| Human-in-the-loop coding with editor, slash commands, footer stats | Interactive | `pi` |
| One-shot or scripted prompts to stdout | Print / text | `pi -p …` or `--mode text` |
| Machine-readable non-interactive output | JSON | `--mode json` |
| External process driving pi over a protocol | RPC | `--mode rpc` or `./rpc-entry` |
| In-process control, custom UI, session replacement | SDK | main export + `AgentSessionRuntime` |

All modes share the same product defaults at the agent layer (tools, extensions, skills, providers) unless flags or SDK options narrow them. Auth remains BYOK/subscription-oriented (API keys or `/login`); modes do not hard-wire a single model vendor.

## Constraints and diagnostics

- **Mode enum is strict in the parser:** only `text`, `json`, and `rpc` assign `Args.mode`.
- **`--print` message consumption** skips the next token if it looks like a flag (except values starting with `---`) or `@file` argument.
- **Extension flags** that are not built-in land in `unknownFlags` for extension CLI handling.
- **Session replacement cancel:** extension `session_before_switch` / `session_before_fork` handlers can abort `newSession` / `switchSession` / fork paths.
- **SDK hosts** must rebind session-local subscriptions after `newSession` / `switchSession`; the example pattern unsubscribes and resubscribes on `runtime.session`.

## Related pages

<CardGroup>
  <Card title="Overview" href="/overview">
    What pi is, the four run modes, and first routes for CLI users and embedders.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    Install, authenticate, start interactive pi, confirm default tools.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Full flag and argument parsing surface for the `pi` binary.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    Process integration via rpc-entry: commands, streams, and constraints.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed via the main export: construction, hooks, models, tools, settings.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    AgentSessionRuntime services, lifecycle events, embedding without the TUI.
  </Card>
  <Card title="Package exports" href="/package-exports">
    npm surface: main, rpc-entry, client, bin, piConfig.configDir.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including session runtime and full-control setups.
  </Card>
</CardGroup>

---

## 05. Agent sessions

> Session lifecycle, prompt queue, concurrent behavior, stats, and runtime services that own a conversation turn.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/05-agent-sessions.md
- Generated: 2026-08-09T03:40:55.318Z

### Source Files

- `packages/coding-agent/src/core/agent-session.ts`
- `packages/coding-agent/src/core/agent-session-services.ts`
- `packages/coding-agent/src/core/agent-session-runtime.ts`
- `packages/coding-agent/test/suite/agent-session-queue.test.ts`
- `packages/coding-agent/test/agent-session-concurrent.test.ts`
- `packages/coding-agent/examples/sdk/11-sessions.ts`

---
title: "Agent sessions"
description: "Session lifecycle, prompt queue, concurrent behavior, stats, and runtime services that own a conversation turn."
---

`AgentSession` is the shared turn owner for interactive, print/JSON, RPC, and SDK modes. It wraps an `@earendil-works/pi-agent-core` `Agent`, persists conversation entries through `SessionManager`, and exposes prompting, queueing, compaction hooks, model/tool control, and session stats. Modes add I/O on top; they do not own the agent loop.

## Ownership model

Three construction layers keep cwd-bound infrastructure separate from the conversation object and from session replacement:

| Layer | Type / factory | Owns |
| --- | --- | --- |
| Services | `AgentSessionServices` via `createAgentSessionServices()` | `cwd`, `agentDir`, `ModelRuntime`, `SettingsManager`, `ResourceLoader`, diagnostics |
| Session | `AgentSession` via `createAgentSession()` or `createAgentSessionFromServices()` | Agent state, event fan-out, prompt queue, tools, compaction, stats |
| Runtime | `AgentSessionRuntime` via `createAgentSessionRuntime()` | Current session + services; `/new`, resume, fork, import replacement |

`AgentSessionServices` is infrastructure only. Callers create services first, resolve model/tools against that cwd, then build the session. `AgentSessionRuntime` tears down the current session (abort → `session_shutdown` → dispose), creates the next runtime with the same factory, then rebinds host UI/subscriptions.

```mermaid
flowchart TB
  subgraph host["Host (CLI / RPC / SDK)"]
    UI["I/O and bindings"]
  end
  subgraph runtime["AgentSessionRuntime"]
    S["AgentSession"]
    SVC["AgentSessionServices"]
  end
  subgraph persist["Persistence"]
    SM["SessionManager JSONL"]
  end
  subgraph agentcore["pi-agent-core"]
    A["Agent"]
  end
  UI --> runtime
  SVC --> S
  S --> A
  S --> SM
  S -->|"steer / followUp queues"| A
```

## Creating a session

### SDK entry

`createAgentSession(options?)` builds or reuses loaders, constructs the `Agent` with settings-derived stream options and queue modes, restores messages when the manager already has history, and returns:

```ts
interface CreateAgentSessionResult {
  session: AgentSession;
  extensionsResult: LoadExtensionsResult;
  modelFallbackMessage?: string;
}
```

Important `CreateAgentSessionOptions` fields:

| Option | Default | Role |
| --- | --- | --- |
| `cwd` | `process.cwd()` or session manager cwd | Project-local discovery |
| `agentDir` | `~/.pi/agent` | Auth, models, global resources |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence target |
| `model` / `thinkingLevel` | Settings / first available | Initial model; restored from session when continuing |
| `tools` / `excludeTools` / `noTools` | Default active: `read`, `bash`, `edit`, `write` | Tool allow/deny surface |
| `resourceLoader` | `DefaultResourceLoader` | Skills, extensions, prompts, context files |
| `modelRuntime` | Created from `agentDir` auth/models paths | Provider auth and streaming |

### SessionManager factories

| Factory | Behavior |
| --- | --- |
| `SessionManager.create(cwd, sessionDir?)` | New persisted JSONL under the session directory |
| `SessionManager.continueRecent(cwd, sessionDir?)` | Most recent session for the project, or create new |
| `SessionManager.open(path, sessionDir?, cwdOverride?)` | Open an existing JSONL file |
| `SessionManager.inMemory(cwd?)` | No file persistence |
| `SessionManager.list(cwd, sessionDir?)` | List project sessions |
| `SessionManager.forkFrom(sourcePath, targetCwd, …)` | Copy history into a new session in another project cwd |

Default storage layout (when `sessionDir` is omitted):

```text
~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<uuid>.jsonl
```

Override with `SessionManager.create(cwd, customDir)`, CLI `--session-dir`, or the session-directory env documented in the CLI help.

### CLI session controls

| Flag | Effect |
| --- | --- |
| `-c` / `--continue` | Continue most recent session |
| `-r` / `--resume` | Interactive session picker |
| `--session <path\|id>` | Open file or partial UUID |
| `--session-id <id>` | Exact project session id (create if missing) |
| `--fork <path\|id>` | Fork into a new session file |
| `--session-dir <dir>` | Storage/lookup directory |
| `--no-session` | Ephemeral; do not save |
| `-n` / `--name <name>` | Display name at startup |

Interactive slash commands that act on the same surface: `/session`, `/name`, `/new`, `/resume`, `/fork`, `/clone`, `/tree`, `/compact`, `/export`, `/share`.

### Minimal SDK patterns

```ts
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

// Ephemeral
const { session: mem } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
});
mem.dispose();

// New file-backed session
const { session } = await createAgentSession({
  sessionManager: SessionManager.create(process.cwd()),
});
console.log(session.sessionId, session.sessionFile);

// Continue recent (may return modelFallbackMessage if saved model is unavailable)
const { session: continued, modelFallbackMessage } = await createAgentSession({
  sessionManager: SessionManager.continueRecent(process.cwd()),
});
```

Always call `session.dispose()` (or `runtime.dispose()`) when finished.

## Prompt turn lifecycle

`prompt(text, options?)` is the primary entry for a user turn.

### Preflight and expansion order

1. **Extension slash commands** (`/command …` registered via `pi.registerCommand`) run immediately, including while streaming, and do not start an agent prompt.
2. Reject if **manual compaction** is in progress (`_compactionAbortController` set).
3. Emit extension **`input`** handlers (`source` defaults to `"interactive"`; pass streaming behavior when already streaming). Handlers may `handled` or `transform` text/images.
4. Expand `/skill:name` and file-based prompt templates when `expandPromptTemplates` is true (default).
5. If `isStreaming`, queue with `streamingBehavior: "steer" | "followUp"` (required); otherwise start a new run.
6. When idle: validate model and provider auth, optionally auto-compact from the last assistant message, inject `nextTurn` asides, emit `before_agent_start`, then run the agent.

### Run and settle

```ts
// Conceptual flow inside AgentSession
_isAgentRunActive = true
await agent.prompt(messages)
while (await _handlePostAgentRun()) {
  await agent.continue()  // retry, compaction recovery, or extension-queued messages
}
// finally:
_systemPromptOverride = undefined
flush pending bash messages
await _emitAgentSettled()  // agent_settled + resolve waitForIdle
```

`_handlePostAgentRun()` may continue the loop for:

- retryable provider errors (`auto_retry_*` events),
- auto-compaction after threshold/overflow,
- messages queued by `agent_end` extension handlers (`agent.hasQueuedMessages()`).

### Idle / streaming flags

| Accessor | Meaning |
| --- | --- |
| `isStreaming` | An agent run or post-run continuation is active (`_isAgentRunActive`) |
| `isIdle` | Negation of `isStreaming` |
| `isCompacting` | Manual compaction, auto-compaction, or branch summary in progress |
| `waitForIdle()` | Resolves after `agent_settled` clears the active run |
| `abort()` | Aborts retry + agent, then `waitForIdle()` |

## Prompt queue and concurrent behavior

Only one agent run owns the loop. A second bare `prompt()` while streaming throws:

```text
Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.
```

### Queue channels

| Channel | API | When delivered |
| --- | --- | --- |
| **Steer** | `steer(text, images?)` or `prompt(..., { streamingBehavior: "steer" })` | After the current assistant tool calls finish, before the next LLM call |
| **Follow-up** | `followUp(text, images?)` or `prompt(..., { streamingBehavior: "followUp" })` | After the run has no more tool calls or steering messages |
| **Next turn** | `sendCustomMessage(..., { deliverAs: "nextTurn" })` | Injected as context with the *next* idle user prompt |

Extension helpers:

- `sendUserMessage(content, { deliverAs })` → `prompt` with `expandPromptTemplates: false`, `source: "extension"`.
- `sendCustomMessage` with `deliverAs: "steer" | "followUp"` while streaming, or `triggerTurn` when idle.

### Queue modes

Settings keys (defaults `"one-at-a-time"`):

| Setting | Values | Effect |
| --- | --- | --- |
| `steeringMode` | `"all"` \| `"one-at-a-time"` | Batch all pending steers into one LLM turn, or drain one per turn |
| `followUpMode` | `"all"` \| `"one-at-a-time"` | Same for follow-ups |

`AgentSession.setSteeringMode` / `setFollowUpMode` update the agent and persist via `SettingsManager`. Modes are applied when the underlying `Agent` is constructed from settings.

### Queue inspection

| API | Returns |
| --- | --- |
| `pendingMessageCount` | Steer + follow-up length |
| `getSteeringMessages()` / `getFollowUpMessages()` | Pending text snapshots |
| `clearQueue()` | Clears both queues and agent queues; returns `{ steering, followUp }` |

UI/extensions observe `queue_update` events with current `steering` and `followUp` arrays. When a queued user message starts, it is removed from the matching local list before fan-out so listeners see the updated queue.

### Hard constraints

| Situation | Behavior |
| --- | --- |
| `prompt` while streaming without `streamingBehavior` | Throws concurrent-processing error |
| `prompt` during manual compaction | Throws: wait for compaction to finish |
| Extension command via `steer` / `followUp` | Throws: extension commands cannot be queued; use `prompt` when idle (or they already run immediately via `prompt`) |
| Extension command via idle `prompt` | Executes immediately; no user message written |

## Events

Subscribe with `session.subscribe(listener)`; returns an unsubscribe function. Session persistence runs on internal agent event handling (for example `message_end` → session append) before or alongside listener delivery.

Session-specific event types beyond core agent events include:

| Event | Notes |
| --- | --- |
| `agent_settled` | Run fully finished; idle waiters resolve after this |
| `queue_update` | Pending steer/follow-up text lists |
| `compaction_start` / `compaction_end` | Manual, threshold, or overflow |
| `auto_retry_start` / `auto_retry_end` | Provider retry loop |
| `session_info_changed` | Display name change |
| `thinking_level_changed` | Thinking level updates |
| `entry_appended` | Raw session entry append |
| `bash_execution_update` | Streaming bash output |
| `agent_end` (session form) | Includes `willRetry` |

`agent_settled` is also emitted to extension handlers, so host code and extensions can synchronize on turn completion.

## Session stats

`getSessionStats(): SessionStats` walks **all** session entries (including compacted history), so totals reflect billed work across the whole file, not only the active LLM context.

```ts
interface SessionStats {
  sessionFile: string | undefined;
  sessionId: string;
  userMessages: number;
  assistantMessages: number;
  toolCalls: number;
  toolResults: number;
  totalMessages: number;
  tokens: {
    input: number;
    output: number;
    cacheRead: number;
    cacheWrite: number;
    total: number;
  };
  cost: number;
  contextUsage?: ContextUsage;
}
```

`getContextUsage()` estimates current-branch context against the model `contextWindow`. After compaction, if no successful post-compaction assistant usage exists yet, it returns `{ tokens: null, contextWindow, percent: null }` until the next LLM response.

Interactive `/session` surfaces the same identity, counts, tokens, and cost fields.

## Display name and identity

| Accessor / API | Role |
| --- | --- |
| `sessionId` | Stable session UUID from the manager |
| `sessionFile` | JSONL path, or `undefined` when not persisted |
| `sessionName` | Optional display name |
| `setSessionName(name)` | Appends session-info entry; emits `session_info_changed` |

## Runtime replacement

Use `AgentSessionRuntime` when the host must replace the active session without rebuilding the whole process.

| Method | Shutdown reason | Notes |
| --- | --- | --- |
| `newSession({ parentSession?, setup?, withSession? })` | `"new"` | New file if persisted, else new in-memory session |
| `switchSession(path, { cwdOverride?, withSession?, … })` | `"resume"` | Opens target; recreates services for session cwd |
| `fork(entryId, { position?, withSession? })` | `"fork"` | `"before"` (default, user entry) or `"at"` leaf; new branched file when persisted |
| `importFromJsonl(path, cwdOverride?)` | `"resume"` | Copies into session dir when needed; throws `SessionImportFileNotFoundError` |
| `dispose()` | `"quit"` | Final shutdown |

Replacement sequence (all switch/new/fork/import paths):

1. Optional `session_before_switch` / `session_before_fork` (cancellable).
2. `session.abort()` so the aborted turn (including tool results) is persisted.
3. `session_shutdown` extension event.
4. Synchronous `beforeSessionInvalidate` (host UI detach).
5. `session.dispose()` (stales extension contexts).
6. `createRuntime(...)` and apply.
7. Optional `rebindSession` + `withSession(replacedCtx)`.

After replacement, rebind subscriptions and `bindExtensions` to `runtime.session`. Captured extension `ctx` / `pi` from the previous session is invalid; post-replacement work belongs in `withSession`.

```ts
import {
  createAgentSessionRuntime,
  createAgentSessionServices,
  createAgentSessionFromServices,
  SessionManager,
  getAgentDir,
} from "@earendil-works/pi-coding-agent";

const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
  const services = await createAgentSessionServices({ cwd });
  return {
    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),
    services,
    diagnostics: services.diagnostics,
  };
};

const runtime = await createAgentSessionRuntime(createRuntime, {
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  sessionManager: SessionManager.create(process.cwd()),
});

let unsub = runtime.session.subscribe((e) => {
  if (e.type === "agent_settled") console.log("turn idle");
});

await runtime.newSession();
unsub();
unsub = runtime.session.subscribe(/* rebind */);
await runtime.dispose();
```

## Diagnostics

`createAgentSessionServices` and runtime creation accumulate non-fatal `AgentSessionRuntimeDiagnostic` values (`info` | `warning` | `error`) instead of exiting. Typical sources: extension provider registration failures, unknown extension CLI flags, flags that require a value. The host decides whether to print or abort startup.

## Dispose

`AgentSession.dispose()`:

- aborts retry, compaction, branch summary, bash, and the agent (errors from abort hooks are swallowed),
- invalidates the extension runner with a stale-context message,
- disconnects agent subscriptions and clears listeners,
- runs `cleanupSessionResources(sessionId)`.

## Failure modes

| Symptom | Likely cause | Mitigation |
| --- | --- | --- |
| Concurrent prompt error | Second `prompt` while streaming without queue mode | Pass `streamingBehavior`, or call `steer` / `followUp` |
| Compaction prompt error | User/RPC prompt during manual compact | Await compaction / `compaction_end` |
| Extension command queue error | `/cmd` passed to `steer`/`followUp` | Use idle `prompt` (commands already run immediately) |
| Auth error on idle prompt | Missing API key or expired OAuth | Configure credentials or `/login <provider>` |
| No model selected | No model resolved at create or restore | Pass `model` or fix settings/auth so a model is available |
| `modelFallbackMessage` set | Continued session model unavailable | Session still opens; using fallback model from resolver |
| Stale extension context after `/new` | Using old `ctx` after replacement | Use `withSession` / rebind to `runtime.session` |
| Fork/clone before first save | Persisted session file not yet written | Wait for first assistant response before forking |
| Import path missing | `importFromJsonl` path does not exist | Catch `SessionImportFileNotFoundError` |

## Verification signals

- After a turn: `agent_settled` fires and `isIdle === true`.
- Queue: `queue_update` lengths match `getSteeringMessages` / `getFollowUpMessages`; `pendingMessageCount` decreases as messages start.
- Stats: `getSessionStats().sessionId` matches `session.sessionId`; token totals include pre-compaction assistant usage when present in the file.
- Replacement: `runtime.session` identity changes; previous `sessionFile` differs after `newSession` when persisted.
- Ephemeral: `SessionManager.inMemory()` → `session.sessionFile === undefined`.

## Related pages

<CardGroup>
  <Card title="Session runtime" href="/session-runtime">
    Embedding with AgentSessionRuntime, lifecycle events, and host rebind patterns without the TUI.
  </Card>
  <Card title="Branching and session trees" href="/branching-and-tree">
    In-file tree navigation, fork/clone, branch summaries, and cancel-during-compact interactions.
  </Card>
  <Card title="Context compaction" href="/compaction">
    Auto/manual compaction triggers, overflow recovery, and interaction with in-flight prompts.
  </Card>
  <Card title="SDK" href="/sdk">
    Package main export, createAgentSession options, and embedder control hooks.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste session and runtime recipes, including sessions and full-control setups.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    Process integration constraints for prompts during compaction and JSON stream behavior.
  </Card>
  <Card title="Run modes" href="/run-modes">
    How interactive, print/JSON, RPC, and SDK modes attach I/O to the same AgentSession.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Retry/network failures, SIGTERM cleanup, and session settlement issues.
  </Card>
</CardGroup>

---

## 06. Branching and session trees

> Session branching, tree navigation, cancel-during-compact interactions, and name/event constraints for branches.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/06-branching-and-session-trees.md
- Generated: 2026-08-09T03:41:19.831Z

### Source Files

- `packages/coding-agent/test/agent-session-branching.test.ts`
- `packages/coding-agent/test/agent-session-tree-navigation.test.ts`
- `packages/coding-agent/test/suite/regressions/3688-tree-cancel-compacting.test.ts`
- `packages/coding-agent/test/suite/regressions/3686-session-name-event.test.ts`
- `packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts`
- `packages/coding-agent/src/core/agent-session.ts`

---
title: "Branching and session trees"
description: "Session branching, tree navigation, cancel-during-compact interactions, and name/event constraints for branches."
---

`AgentSession.navigateTree()` moves the active leaf inside one JSONL session file; `AgentSessionRuntime.fork()` extracts a path into a **new** session. Sessions are an append-only tree of entries linked by `id` / `parentId`, with one active `leafId`. Interactive mode exposes this as `/tree`, `/fork`, and `/clone`; SDK and RPC use the same core APIs.

## Session tree model

Each session JSONL file is a tree, not a flat transcript:

| Concept | Behavior |
|---------|----------|
| Entry `id` | Stable id on every tree entry (messages, labels, summaries, …) |
| `parentId` | Parent entry, or `null` for a root |
| Leaf | Current position; next appends attach as children of the leaf |
| Active path | Root → leaf via `SessionManager.getBranch()` |
| Abandoned branches | Remain in the file; only the active path builds LLM context |

```text
[user] ── [assistant] ── [user] ── [assistant] ─┬─ [user]          ← current leaf
                                               │
                                               └─ [branch_summary] ── [user]  ← alternate
```

Context for the model is built from the active path. A `branch_summary` entry on that path becomes a `branchSummary` message (`role: "branchSummary"`) so prior branch work can stay available without replaying the abandoned turns.

## `/tree` vs `/fork` vs `/clone`

| Operation | Same file? | Typical input | Result |
|-----------|------------|---------------|--------|
| `/tree` / `navigateTree` | Yes | Any entry id | Move leaf; optional branch summary |
| `/fork` / `runtime.fork(entryId)` | No (new file) | User message entry | New session before that user message; text returned for re-edit |
| `/clone` / `runtime.fork(leafId, { position: "at" })` | No (new file) | Current leaf | New session containing root→leaf path |

Use **tree navigation** to keep alternatives in one file. Use **fork/clone** when you need a separate session (handoff, export isolation, parallel files).

<Note>
Fork and clone are session-replacement operations on `AgentSessionRuntime`. In-place leaf moves are `AgentSession.navigateTree()`. After fork/clone, extension contexts from the previous session are stale; use `withSession` on the replacement.
</Note>

### Fork semantics

`AgentSessionRuntime.fork(entryId, options?)`:

| Option | Default | Meaning |
|--------|---------|---------|
| `position: "before"` | yes | Fork from a **user** message; new leaf is that message’s parent; returns `selectedText` for the editor |
| `position: "at"` | — | Fork at the given entry (used by clone); no selected text |

Persisted forks call `SessionManager.createBranchedSession()` (root→leaf copy into a new JSONL, with `parentSession` set). In-memory / `--no-session` mode still forks without writing a file. `session_before_fork` can cancel. Unsaved sessions that have never been written throw until the first assistant response is persisted.

`getUserMessagesForForking()` returns `{ entryId, text }[]` for every user message entry (fork picker / RPC `get_fork_messages`).

## In-place navigation (`navigateTree`)

```ts
await session.navigateTree(targetId, {
  summarize?: boolean;
  customInstructions?: string;
  replaceInstructions?: boolean;
  label?: string;
});
// → { editorText?, cancelled, aborted?, summaryEntry? }
```

### Preconditions and errors

| Condition | Result |
|-----------|--------|
| `session.isStreaming` | Throws: `Wait for the current response to finish before navigating the session tree.` |
| `targetId === current leaf` | No-op: `{ cancelled: false }` |
| `summarize: true` and no model | Throws: `No model available for summarization` |
| Unknown `targetId` | Throws: `Entry … not found` |

Interactive `/tree` aborts an in-flight agent turn **before** calling `navigateTree` so the streaming guard does not block UI navigation.

### Leaf placement by target type

| Target | New leaf | Editor text |
|--------|----------|-------------|
| User message | Parent of target (`null` if root) | Target user text |
| `custom_message` | Parent of target | Custom message text |
| Assistant / tool / other | Target itself | None |

Selecting the root user message resets the leaf (`resetLeaf()` → empty conversation) and loads the original prompt into the editor so resubmit creates a sibling branch.

### Labels

If `label` is set:

- With a generated summary → label attaches to the new `branch_summary` entry
- Without a summary → label attaches to the navigation target

## Branch summarization

When leaving a path with `summarize: true`, pi summarizes entries from the old leaf back to the common ancestor with the target (via `collectEntriesForBranchSummary`). Compaction entries on that path are included; their summaries become context for the branch summarizer.

Summary attachment uses `SessionManager.branchWithSummary(newLeafId, …)`:

- New `branch_summary` entry’s `parentId` is the **navigation target position** (`newLeafId`), not the abandoned leaf
- New leaf becomes the summary entry
- Optional `details` (default: `{ readFiles, modifiedFiles }`) and `usage` are stored; usage rolls into session token/cost totals
- `fromHook: true` when an extension supplied the summary text

### Defaults and settings

| Setting | Default | Effect |
|---------|---------|--------|
| `branchSummary.reserveTokens` | `16384` | Tokens reserved for summarizer prompt + response |
| `branchSummary.skipPrompt` | `false` | Interactive only: skip “Summarize branch?” and default to **no** summary |
| `treeFilterMode` | `"default"` | Default filter when opening `/tree` |
| `doubleEscapeAction` | `"tree"` | Empty-editor double-escape: `"tree"`, `"fork"`, or `"none"` |

Custom instructions:

- Default: append as `Additional focus: …` on the built-in branch-summary prompt
- `replaceInstructions: true`: replace the default prompt entirely

### Interactive prompt choices

Unless `branchSummary.skipPrompt` is true, `/tree` asks:

1. No summary  
2. Summarize  
3. Summarize with custom prompt  

Escape during the prompt returns to the tree selector. Escape during generation calls `abortBranchSummary()`.

## Cancel and compact-state interactions

Branch summarization shares the **compacting** signal with manual/auto compaction:

```ts
session.isCompacting  // true while _branchSummaryAbortController is set
session.abortBranchSummary()
```

| Outcome | `navigateTree` return | Leaf / entries | `isCompacting` after |
|---------|----------------------|----------------|----------------------|
| Success | `{ cancelled: false, summaryEntry? }` | Moved; optional summary appended | `false` |
| Extension cancel (`session_before_tree` → `{ cancel: true }`) | `{ cancelled: true }` | Unchanged | `false` (controller cleared in `finally`) |
| Abort mid-summary | `{ cancelled: true, aborted: true }` | Unchanged | `false` |

<Warning>
If `session_before_tree` cancels, no leaf move and no summary entry are written. The abort controller is always cleared in a `finally` block so `isCompacting` does not stick true after a cancelled tree navigation (regression covered for cancel-during-compact/summary state).
</Warning>

During summarization, transient provider failures reuse the same retry settings and events as compaction, with `source: "branchSummary"` (no compaction `reason` field).

## Extension hooks

| Event | When | Handler can |
|-------|------|-------------|
| `session_before_tree` | Before move / summary | `{ cancel: true }`; supply `{ summary, details?, usage? }`; override `customInstructions`, `replaceInstructions`, `label` |
| `session_tree` | After successful navigation | Observe `newLeafId`, `oldLeafId`, `summaryEntry`, `fromExtension` |
| `session_before_fork` | Before fork/clone replacement | Cancel fork/clone |
| `session_info_changed` | After name change | Observe normalized `name` |

```ts
pi.on("session_before_tree", (event) => {
  // event.preparation: targetId, oldLeafId, commonAncestorId,
  //   entriesToSummarize, userWantsSummary, customInstructions, …
  // event.signal — abort signal for long-running work
  if (!event.preparation.userWantsSummary) return;
  return { summary: { summary: "Left branch explored approach B." } };
});

pi.on("session_tree", (event) => {
  // event.newLeafId, event.oldLeafId, event.summaryEntry
});
```

Command context also exposes `navigateTree` and `fork` for extension-driven navigation. After `fork` / `newSession` / `switchSession`, do not reuse a captured `pi` from the old session.

## Session display names

Names are metadata for pickers and footers; they do not change the tree structure.

| Surface | Behavior |
|---------|----------|
| CLI | `pi --name "…"`, `-n` |
| Interactive | `/name <name>` |
| API | `session.setSessionName(name)`, `pi.setSessionName(name)` |
| RPC | `set_session_name` with non-empty trimmed `name` |
| Persistence | `session_info` entry on the active leaf path |

### Constraints

| Rule | Detail |
|------|--------|
| Newlines | `appendSessionInfo` replaces `[\r\n]+` with a single space, then `trim()` |
| Clear name | Empty / whitespace-only name after sanitize stores cleared display name (`getSessionName()` → `undefined`) |
| Events | Every `setSessionName` emits `session_info_changed` on the session subscriber stream **and** to extensions, with the **normalized** name |
| RPC empty | `set_session_name` with only whitespace returns an error (`Session name cannot be empty`) without calling `setSessionName` |

```ts
session.setSessionName("hello\nworld\r\nagain");
// stored + event name: "hello world again"
```

## Programmatic surfaces

### SDK

```ts
// In-place tree navigation (same session file)
const nav = await session.navigateTree(entryId, { summarize: true });

// New session file from a user message
const forked = await runtime.fork(userEntryId);
// Clone current branch
const cloned = await runtime.fork(session.sessionManager.getLeafId()!, { position: "at" });

// Tree inspection
const tree = session.sessionManager.getTree();
const leafId = session.sessionManager.getLeafId();
session.sessionManager.branch(entryId);
session.sessionManager.branchWithSummary(entryId, "Summary…");
```

### RPC

| Command | Role |
|---------|------|
| `get_tree` | Full tree + `leafId` |
| `get_entries` | Append-order entries (incl. abandoned branches); optional `since` cursor |
| `get_fork_messages` | User messages for fork UI |
| `fork` | Fork from user entry |
| `clone` | Fork at current leaf |
| `set_session_name` | Set display name |

There is no dedicated RPC `navigate_tree` command; embedders use the SDK `AgentSession.navigateTree` path or drive tree moves through the session manager when building custom UIs.

### SessionManager branching primitives

| Method | Effect |
|--------|--------|
| `branch(id)` | Set leaf to existing entry; next append creates a new child |
| `resetLeaf()` | Leaf `null` (next append is a new root) |
| `branchWithSummary(id \| null, summary, …)` | Set leaf, append `branch_summary` child |
| `createBranchedSession(leafId)` | Write new session file for root→leaf path |
| `getTree()` / `getChildren(id)` / `getBranch(id?)` | Read structure |

## Troubleshooting

| Symptom | Likely cause | What to check |
|---------|--------------|---------------|
| Navigate throws while agent is streaming | Direct `navigateTree` without abort | Wait for idle or abort; interactive `/tree` aborts first |
| `isCompacting` stuck after cancel | Pre-fix cancel path | Confirm controller cleared; cancel should return `{ cancelled: true }` and `isCompacting === false` |
| Escape mid-summary returns to tree | Expected abort | `aborted: true`; no new entries; leaf unchanged |
| Session name shows newlines / breaks UI | Unsanitized write outside API | Always use `setSessionName` / `appendSessionInfo` |
| Fork fails “session has not been saved yet” | Empty on-disk file before first assistant write | Wait for first persisted assistant response |
| Extension lost after fork | Stale context | Use `withSession` on fork/new/switch |

## Related pages

<CardGroup>
  <Card title="Agent sessions" href="/agent-sessions">
    Session lifecycle, prompt queue, and runtime ownership of a turn.
  </Card>
  <Card title="Context compaction" href="/compaction">
    Auto/manual compaction, shared retry events, and summarization settings.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    AgentSessionRuntime services, fork/newSession replacement, and embedding without the TUI.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    Process integration: get_tree, fork, clone, set_session_name.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed navigateTree, fork, and SessionManager tree APIs.
  </Card>
  <Card title="Extensions" href="/extensions">
    session_before_tree, session_tree, and session_info_changed handlers.
  </Card>
  <Card title="Settings" href="/settings">
    branchSummary.*, treeFilterMode, doubleEscapeAction.
  </Card>
</CardGroup>

---

## 07. Context compaction

> Auto and manual compaction triggers, reasons, retries on stream drop, and interactions with in-flight prompts and RPC.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/07-context-compaction.md
- Generated: 2026-08-09T03:41:34.692Z

### Source Files

- `packages/coding-agent/test/agent-session-compaction.test.ts`
- `packages/coding-agent/test/suite/agent-session-compaction.test.ts`
- `packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts`
- `packages/coding-agent/test/suite/regressions/6647-compaction-retries-transient-stream-drop.test.ts`
- `packages/coding-agent/test/suite/regressions/7253-manual-compact-during-response.test.ts`
- `packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts`

---
title: "Context compaction"
description: "Auto and manual compaction triggers, reasons, retries on stream drop, and interactions with in-flight prompts and RPC."
---

`AgentSession` owns context compaction for every run mode (interactive, print/JSON, RPC, SDK). When history approaches the model context window, Pi summarizes older branch entries into a `compaction` session entry and rebuilds LLM context as **summary + kept recent messages**. Pure preparation and summarization live under `src/core/compaction/`; session I/O, reasons, retry/overflow policy, and prompt gating live in `src/core/agent-session.ts`.

## Compaction reasons

Every compaction run carries a `reason` on session events and extension hooks:

| Reason | Trigger | Auto-retry the interrupted turn? |
|--------|---------|----------------------------------|
| `manual` | `/compact [instructions]`, `session.compact()`, RPC `compact` | No (`willRetry: false`) |
| `threshold` | Context usage crosses the reserve-token floor after a turn (or on pre-prompt check) | No |
| `overflow` | Context overflow error, or recoverable `length` stop below the model’s desired output limit | Yes once when the assistant did not finish with `stopReason: "stop"` |

Extension events `session_before_compact` and `session_compact` report the same `reason` and `willRetry` values.

## Auto-compaction triggers

Auto-compaction runs only when `settings.compaction.enabled` is true (default). `AgentSession` checks the last assistant message:

1. After each agent run (`_handlePostAgentRun` → `_checkCompaction`)
2. Immediately before a new non-streaming `prompt()` when a prior assistant exists (covers aborted responses that left large context)

### Threshold

```
contextTokens > contextWindow - reserveTokens
```

Defaults: `reserveTokens = 16384`, `keepRecentTokens = 20000`. Threshold compaction does **not** call `agent.continue()` for a retry; the user (or a queued message) continues manually. If agent-level follow-up/steer messages are already queued, auto-compaction returns true so the post-run loop can continue delivery.

### Overflow recovery

Same-model failures classified as context overflow (`isContextOverflow`) or recoverable length truncation (`isRecoverableLength`) use reason `"overflow"`:

- Failed / truncated assistant (`stopReason` is not `"stop"`): remove the bad trailing assistant from agent state, compact once, set `willRetry: true`, then continue the turn.
- Successful assistant that finished over the window (`stopReason: "stop"`): compact with `willRetry: false` so a completed answer is not re-prompted via `agent.continue()`.
- At most **one** compact-and-retry attempt per recovery cycle (`_overflowRecoveryAttempted`). A second overflow after that emits:

  `Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.`

Guards that skip auto-compaction:

| Guard | Effect |
|-------|--------|
| `compaction.enabled === false` | No auto path (manual still works) |
| Aborted assistant after agent_end | Skipped unless pre-prompt check (`skipAbortedCheck: false`) |
| Assistant from a different provider/model | No overflow recovery for the previous model’s error |
| Assistant timestamp ≤ latest compaction entry | Stale pre-compaction usage/error cannot re-fire |
| `prepareCompaction` returns undefined | Nothing to summarize / already compacted |

Context overflow is **not** treated as a normal provider auto-retry; `_isRetryableError` returns false when `isContextOverflow` matches so compaction owns recovery.

## Manual compaction

| Surface | Invocation |
|---------|------------|
| Interactive | `/compact` or `/compact <custom instructions>` |
| SDK / session API | `await session.compact(customInstructions?)` |
| RPC | `{"type":"compact"}` or with `customInstructions` |
| Toggle auto | `session.setAutoCompactionEnabled(enabled)` / RPC `set_auto_compaction` |

`compact()`:

1. Aborts the current agent operation (`await this.abort()`).
2. Sets `_compactionAbortController` and emits `compaction_start` with `reason: "manual"`.
3. Runs `prepareCompaction` on the current branch path.
4. Emits `session_before_compact` (extensions may cancel or supply a custom `CompactionResult`).
5. Otherwise calls `compact()` summarization with `settings.retry` and summarization retry callbacks.
6. Appends a `CompactionEntry` via `sessionManager.appendCompaction`, rebuilds `agent.state.messages`, emits `session_compact` and `compaction_end`.

Failure modes from preparation:

- `Already compacted` — leaf entry is already type `compaction`
- `Nothing to compact (session too small)` — no messages in the summarizable span
- `No model selected` / missing auth for summarization
- `Compaction cancelled` — extension cancel or `abortCompaction()`

Cancel any in-flight manual or auto compaction with `session.abortCompaction()` (aborts both abort controllers). Branch summarization uses `abortBranchSummary()` separately; both contribute to `isCompacting`.

## What the LLM sees after compaction

```
path entries (append-only tree)
  ... older messages ...
  compaction { summary, firstKeptEntryId, tokensBefore, details?, usage? }
  ... messages from firstKeptEntryId ...

LLM context rebuild
  system prompt
  + compactionSummary message (wrapped summary text)
  + messages from firstKeptEntryId onward
```

Preparation (`prepareCompaction`):

1. Finds the previous compaction boundary (iterative update uses its summary and `firstKeptEntryId`).
2. Estimates `tokensBefore` from the rebuilt session context.
3. Walks backward until `keepRecentTokens` is satisfied; cut points are user, assistant, bash, or custom/branch-summary messages — never tool results alone.
4. May produce a **split turn** when one turn exceeds the keep budget; history summary and turn-prefix summary are merged.
5. Tracks cumulative `readFiles` / `modifiedFiles` in `details` for default (non-extension) summaries.

Summarization requests use a fresh routing `sessionId` and `cacheRetention: "none"` so one-off summary prompts do not write reusable prompt cache entries.

## Summarization retries on stream drop

Compaction and branch-summary LLM calls go through `completeSummarization`, which wraps the single call in `retryAssistantCall` using **`settings.retry`** (same budget as turn auto-retry):

| Setting | Default |
|---------|---------|
| `retry.enabled` | `true` |
| `retry.maxRetries` | `3` |
| `retry.baseDelayMs` | `2000` (exponential backoff) |

Transient mid-stream failures (for example error message `terminated` / socket death) are retried. Deterministic non-retryable errors (for example `insufficient_quota`) and aborts fail immediately. Retry is skipped when `retry.enabled` is false.

Session events during summarization retry:

| Event | When |
|-------|------|
| `summarization_retry_scheduled` | Backoff scheduled: `attempt`, `maxAttempts`, `delayMs`, `errorMessage` |
| `summarization_retry_attempt_start` | Next attempt begins; `source: "compaction"` includes `reason` |
| `summarization_retry_finished` | Retry loop ends (success or exhaustion) |

`abortCompaction()` during backoff aborts the signal; the run ends as aborted (`compaction_end.aborted: true`).

## In-flight prompts and concurrent behavior

```mermaid
stateDiagram-v2
  [*] --> Idle
  Idle --> Streaming: prompt()
  Streaming --> Checking: agent_end
  Checking --> ThresholdCompact: shouldCompact
  Checking --> OverflowCompact: overflow / recoverable length
  Checking --> Idle: no compact
  OverflowCompact --> Streaming: willRetry continue
  ThresholdCompact --> Idle: no auto-retry
  Idle --> ManualCompact: compact() / /compact
  Streaming --> ManualCompact: compact() aborts agent first
  ManualCompact --> Idle: compaction_end
  note right of ManualCompact
    prompt() while manual compact
    throws until controller cleared
  end note
```

### Session API / SDK / RPC

While **manual** compaction holds `_compactionAbortController`, `prompt()` throws:

```text
Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.
```

The prompt is not persisted, does not start an agent run, and RPC `preflightResult` is false. Wait for `compaction_end` (or `isCompacting === false` from `get_state`) before retrying.

`compaction_end` clears the controller **before** listeners run so handlers may safely submit a queued prompt.

### Interactive TUI

While `session.isCompacting`:

- Normal user input is **queued** (`queueCompactionMessage`) as steer/follow-up for after compaction.
- Extension slash commands still run immediately via `session.prompt`.
- On `compaction_end`, `flushCompactionQueue` drains the queue; if `willRetry` is true, messages are steered/followed into the retry turn instead of starting a new top-level prompt.

### Manual compact during an active response

`compact()` aborts the agent, then runs only the **manual** compaction. A concurrent threshold that would have fired from the in-flight turn does not produce a second auto-compaction for that same turn—only the requested manual `compaction_start` / `compaction_end` with `reason: "manual"`.

### Auto-compaction and queues

Threshold auto-compaction with existing agent-level queued messages (follow-up/steer) signals continuation so those messages still deliver after rebuild. Overflow recovery with `willRetry: true` continues the interrupted turn after stripping a reappearing error/length assistant from rebuilt state.

## RPC surface

| Command | Role |
|---------|------|
| `compact` | Manual compaction; optional `customInstructions` |
| `set_auto_compaction` | Sets `compaction.enabled` via `setAutoCompactionEnabled` |
| `get_state` | Includes `isCompacting`, `autoCompactionEnabled` |

`compact` success payload matches `CompactionResult`:

| Field | Notes |
|-------|--------|
| `summary` | Summary text injected as context |
| `firstKeptEntryId` | First session entry id retained after the summary |
| `tokensBefore` | Pre-compaction context estimate |
| `estimatedTokensAfter` | Heuristic over rebuilt messages (not provider-exact) |
| `usage` | Optional LLM usage for summary generation |
| `details` | Default file ops or extension-defined data |

Streamed session events: `compaction_start`, `compaction_end`, `summarization_retry_*`. Overflow success with retry sets `willRetry: true` on `compaction_end`.

## Settings

Configure in `~/.pi/agent/settings.json` or project `.pi/settings.json`:

```json
{
  "compaction": {
    "enabled": true,
    "reserveTokens": 16384,
    "keepRecentTokens": 20000
  },
  "retry": {
    "enabled": true,
    "maxRetries": 3,
    "baseDelayMs": 2000
  }
}
```

| Key | Default | Effect |
|-----|---------|--------|
| `compaction.enabled` | `true` | Auto threshold/overflow only |
| `compaction.reserveTokens` | `16384` | Headroom for response; also summary max-token budget input |
| `compaction.keepRecentTokens` | `20000` | Recent tokens kept unsummarized |
| `retry.*` | see above | Summarization stream retries and turn auto-retry |

Interactive settings UI exposes auto-compact as **Auto-compact**. Disabling auto-compaction does not remove `/compact` or RPC `compact`.

## Extension hooks

| Hook | Can cancel | Can replace summary |
|------|------------|---------------------|
| `session_before_compact` | Yes (`{ cancel: true }`) | Yes (`{ compaction: CompactionResult }`) |
| `session_compact` | No | No (observe saved entry) |

`session_before_compact` receives `preparation` (`CompactionPreparation`), `branchEntries`, `customInstructions`, `reason`, `willRetry`, and `signal`. Extension-provided summaries skip the default LLM path but still append a session entry and rebuild context. See `examples/extensions/custom-compaction.ts` and `examples/extensions/trigger-compact.ts`.

## Failure modes

| Symptom | Likely cause |
|---------|----------------|
| Prompt / RPC error “compaction is in progress” | Manual compact still running; wait for `compaction_end` |
| `Already compacted` | Leaf is a compaction entry; send more turns before compacting again |
| `Nothing to compact (session too small)` | Keep budget covers entire path |
| Overflow recovery error after one attempt | Second overflow after compact-and-retry; reduce context or use a larger window model |
| Compaction fails with `terminated` then recovers | Expected when `retry.enabled`; watch `summarization_retry_*` |
| Compaction fails immediately with `terminated` | `retry.enabled: false` or non-retryable classification |
| Stale large usage after compact | Pre-compaction assistants are ignored by timestamp vs latest compaction entry |
| Auto never runs | `compaction.enabled: false` |

## Related pages

<CardGroup>
  <Card title="Agent sessions" href="/agent-sessions">
    Prompt queue, concurrent behavior, and turn ownership around compaction checks.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    `compact`, `set_auto_compaction`, `isCompacting`, and prompt-during-compaction constraints.
  </Card>
  <Card title="Branching and session trees" href="/branching-and-tree">
    Branch summarization, tree navigation, and cancel-during-compact interactions.
  </Card>
  <Card title="Settings" href="/settings">
    Compaction and retry setting load/merge surfaces.
  </Card>
  <Card title="Extensions" href="/extensions">
    `session_before_compact` / `session_compact` registration and abort signals.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Retry, network, and settlement failures related to long sessions.
  </Card>
</CardGroup>

---

## 08. Tools and allowlists

> Default read/write/edit/bash tools, extension tools, allowlist and exclude-tools filters, and blocked-tool termination behavior.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/08-tools-and-allowlists.md
- Generated: 2026-08-09T03:38:42.482Z

### Source Files

- `packages/coding-agent/examples/sdk/05-tools.ts`
- `packages/coding-agent/test/agent-session-dynamic-tools.test.ts`
- `packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts`
- `packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts`
- `packages/coding-agent/test/suite/regressions/5109-exclude-tools.test.ts`
- `packages/coding-agent/test/suite/regressions/5998-blocked-tool-terminate.test.ts`

---
title: "Tools and allowlists"
description: "Default read/write/edit/bash tools, extension tools, allowlist and exclude-tools filters, and blocked-tool termination behavior."
---

`createAgentSession` in `@earendil-works/pi-coding-agent` builds a session tool surface from built-ins (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`), optional `customTools`, and tools registered through extensions (`pi.registerTool`). Allowlists (`tools`), disables (`noTools`), and excludes (`excludedToolNames`) decide which names appear in `getAllTools()`, which stay in `getActiveToolNames()`, and what the system prompt lists under available tools.

## Built-in tools

Built-in names used by the SDK and session registry:

| Name | Role in evidence |
|------|------------------|
| `read` | File read; system prompt line uses snippet `Read file contents` |
| `write` | File write; default active with `edit` / `bash` |
| `edit` | File edit; default active |
| `bash` | Shell; default active; can expose `PI_*` session env |
| `grep` | Search; selectable via allowlist |
| `find` | Find; present in full tool set |
| `ls` | List; selectable via allowlist |

Tool name strings match against **all available** tools for the session. When you pass `cwd`, `createAgentSession()` applies that cwd when constructing the built-in tools.

### Default active vs available

The session distinguishes:

| API | Meaning in tests |
|-----|------------------|
| `session.getAllTools()` | Full registered/available set after filters |
| `session.getActiveToolNames()` | Tools treated as active for the turn / prompt |
| `session.systemPrompt` | Prompt text that lists active tools and guidelines |

With a normal session plus an extension tool, default active built-ins observed after excluding `read` are `bash`, `edit`, and `write` (so the default active built-in set is `read`, `write`, `edit`, `bash`). `find`, `grep`, and `ls` remain in the full tool set when built-ins are registered, but they are not required for the default active set.

### System prompt shape

Active tools appear as bullet lines:

```text
- read: Read file contents
- dynamic_tool: Run dynamic test behavior
- Use dynamic_tool when the user asks for dynamic behavior tests.
```

`promptSnippet` becomes the short description after the name. `promptGuidelines` are listed as additional bullets. When no tools are active:

```text
Available tools:
(none)
```

For bash-capable sessions, the prompt can also include:

```text
You can inspect PI_* environment variables for current model and session details.
```

## Configure tools in the SDK

### Allowlist with `tools`

Pass tool name strings to enable only those tools:

```ts
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

// Read-only (no edit/write)
const { session: readOnlySession } = await createAgentSession({
  tools: ["read", "grep", "find", "ls"],
  sessionManager: SessionManager.inMemory(),
});

// Custom selection
const { session: customToolsSession } = await createAgentSession({
  tools: ["read", "bash", "grep"],
  sessionManager: SessionManager.inMemory(),
});

// Custom cwd + selected tools
const customCwd = "/path/to/project";
const { session: customCwdSession } = await createAgentSession({
  cwd: customCwd,
  tools: ["read", "bash", "edit", "write"],
  sessionManager: SessionManager.inMemory(customCwd),
});
```

Allowlists apply to **built-in and extension** tool names. Example: `tools: ["read", "dynamic_tool"]` yields only those two in both `getAllTools()` and `getActiveToolNames()`, and the system prompt includes both snippets while omitting `- bash:` and `- edit:`.

| `tools` value | `getAllTools()` | `getActiveToolNames()` | System prompt |
|---------------|-----------------|------------------------|---------------|
| `["read", "dynamic_tool"]` | `dynamic_tool`, `read` | same | includes both; excludes bash/edit |
| `[]` (empty allowlist) | `[]` | `[]` | `Available tools:\n(none)` |

### Disable with `noTools`

`createAgentSession` and `createAgentSessionFromServices` accept:

| Value | Effect |
|-------|--------|
| `"builtin"` | Built-in tools are not active; extension tools can stay active |
| `"all"` | No tools available or active |

With `noTools: "builtin"` and an extension that registers `dynamic_tool` on `session_start`:

- `getAllTools()` still includes  
  `bash`, `dynamic_tool`, `edit`, `find`, `grep`, `ls`, `read`, `write`
- `getActiveToolNames()` is `["dynamic_tool"]` only
- System prompt lists the extension tool and **does not** list `- read:` or `- bash:`

With `noTools: "all"`:

- `getAllTools()` → `[]`
- `getActiveToolNames()` → `[]`
- System prompt → `Available tools:\n(none)`

Service-based construction propagates the same option:

```ts
const services = await createAgentSessionServices({ cwd, agentDir, settingsManager });
const { session } = await createAgentSessionFromServices({
  services,
  sessionManager,
  model,
  noTools: "builtin",
});
```

Without extension tools in that path, `noTools: "builtin"` leaves `getActiveToolNames()` empty and the prompt at `Available tools:\n(none)`.

### Exclude tools

Excludes remove names from both available and active sets. They apply to built-ins and extension tools.

| Input | Result |
|-------|--------|
| `excludedToolNames: ["read", "ask_question"]` | Neither name in `getAllTools()`; active set can still include `bash`, `edit`, `write`, and remaining extensions |
| Allowlist + exclude | **Exclude wins** over allowlist |

Example: allowlist `["read", "bash", "ask_question"]` with excludes `["read", "ask_question"]` leaves only `bash` in both `getAllTools()` and `getActiveToolNames()`.

```text
allowlist ──► candidates
exclude  ──► hard removal (overrides allowlist)
noTools  ──► "builtin" deactivates defaults; "all" clears everything
```

### SDK custom tools

Register tools at session construction with `customTools`:

```ts
const { session } = await createAgentSession({
  cwd,
  agentDir,
  model,
  settingsManager,
  sessionManager,
  resourceLoader,
  customTools: [
    {
      name: "sdk_tool",
      label: "SDK Tool",
      description: "Tool registered through createAgentSession",
      parameters: Type.Object({}),
      execute: async () => ({
        content: [{ type: "text", text: "ok" }],
        details: {},
      }),
    },
  ],
});
```

`sdk_tool` is active (`getActiveToolNames()` includes it). Source metadata:

| Field | Value |
|-------|--------|
| `path` | `<sdk:sdk_tool>` |
| `source` | `"sdk"` |
| `scope` | `"temporary"` |
| `origin` | `"top-level"` |

Custom tools for interactive packaging are also registered through extensions via `pi.registerTool()` (see extensions docs / `06-extensions.ts` in the SDK examples tree).

## Extension and dynamic tools

### Registration paths

| Path | When it appears |
|------|-----------------|
| `pi.registerTool(...)` in an extension factory | Immediately if registered at factory load |
| `pi.on("session_start", () => pi.registerTool(...))` | After `session.bindExtensions({})` refreshes the registry |
| `customTools` on `createAgentSession` | At session creation |

Before `bindExtensions`, tools registered only on `session_start` are **not** in `getAllTools()`. After bind:

- Name appears in `getAllTools()` and typically in `getActiveToolNames()`
- System prompt includes `- <name>: <promptSnippet>` and any `promptGuidelines`

### Tool definition fields used by the registry

| Field | Use |
|-------|-----|
| `name` | Tool id / allowlist / exclude key |
| `label` | Display label |
| `description` | Tool description |
| `promptSnippet` | Short line in system prompt |
| `promptGuidelines` | Extra prompt bullets |
| `parameters` | TypeBox schema |
| `execute` | Implementation |

### Source metadata (`sourceInfo`)

| Origin | `path` | `source` | `scope` | `origin` |
|--------|--------|----------|---------|----------|
| Built-in `read` | `<builtin:read>` | `"builtin"` | `"temporary"` | `"top-level"` |
| Inline extension tool | `<inline:1>` | `"inline"` | `"temporary"` | `"top-level"` |
| SDK `customTools` | `<sdk:sdk_tool>` | `"sdk"` | `"temporary"` | `"top-level"` |

### Bash session environment

`createBashTool(cwd, options)` (from the coding-agent bash tool module) can inject session state into the shell environment via `spawnHook`.

Default exposure includes:

| Variable | Source |
|----------|--------|
| `PI_SESSION_ID` | `session.sessionId` |
| `PI_SESSION_FILE` | `session.sessionFile` |
| `PI_PROVIDER` | `model.provider` |
| `PI_MODEL` | `model.id` |
| `PI_REASONING_LEVEL` | `session.thinkingLevel` |

Set `exposeSessionEnvironment: false` to omit those `PI_*` keys from the spawn env. Custom bash variants can re-register under a different `name` / `label` while reusing `createBashTool` options.

## Block tool calls and terminate the run

Extensions can handle `tool_call` and block execution before `execute` runs:

```ts
pi.on("tool_call", async () => ({
  block: true,
  reason: "Blocked by terminating policy",
  terminate: true,
}));
```

| Field | Effect |
|-------|--------|
| `block: true` | Tool body does not run |
| `reason` | Block reason string |
| `terminate: true` | Ends the agent run after the block |

Observed outcomes after a blocked terminating call:

1. `tool_execution_end` result includes `terminate: true`
2. A `toolResult` message with `isError` is present on the session
3. Subsequent queued model turns do not run (pending responses remain; later assistant text is not produced)

Use this for policy gates that must stop the turn, not only refuse a single tool.

## Session inspection checklist

After configuring tools:

```ts
session.getAllTools().map((t) => t.name);
session.getActiveToolNames();
session.systemPrompt; // look for "- name:" lines or "Available tools:\n(none)"
await session.bindExtensions({}); // required before session_start-registered tools appear
session.dispose();
```

| Goal | Options |
|------|---------|
| Read-only coding | `tools: ["read", "grep", "find", "ls"]` |
| Default editing surface | omit filters, or `tools: ["read", "bash", "edit", "write"]` |
| Extension-only active tools | `noTools: "builtin"` + extension `registerTool` |
| No tools | `tools: []` or `noTools: "all"` |
| Drop specific names | `excludedToolNames` (overrides allowlist) |
| Policy stop on call | `tool_call` handler with `block` + `terminate` |

## Related pages

<CardGroup>
  <Card title="SDK" href="/sdk">
    Embed pi with createAgentSession, custom tools, models, and settings.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including tools and full-control setups.
  </Card>
  <Card title="Extensions" href="/extensions">
    TypeScript extension registration, active tools on next turn, and lifecycle hooks.
  </Card>
  <Card title="Extension examples" href="/extension-examples">
    Reference packages that register tools and policies.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    AgentSessionRuntime services and embedding sessions without the TUI.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    Confirm the default read/write/edit/bash surface after install.
  </Card>
</CardGroup>

---

## 09. Authentication

> API keys, OAuth login, credential storage, auth checks, ambient auth, and refresh hang failure modes.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/09-authentication.md
- Generated: 2026-08-09T03:42:56.432Z

### Source Files

- `packages/coding-agent/src/cli/auth-command.ts`
- `packages/coding-agent/src/cli/auth-check.ts`
- `packages/coding-agent/src/core/auth-storage.ts`
- `packages/coding-agent/src/core/auth-guidance.ts`
- `packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts`
- `packages/coding-agent/test/auth-storage.test.ts`

---
title: "Authentication"
description: "API keys, OAuth login, credential storage, auth checks, ambient auth, and refresh hang failure modes."
---

Pi authenticates providers through `ModelRuntime`: interactive `/login` and `/logout`, CLI `pi auth` subcommands, environment variables, `auth.json`, runtime API-key overrides, and ambient cloud credentials. Provider auth is BYOK/BYOC — credentials stay on the local machine (or in your process environment) and are never assumed from a hosted pi service.

## Credential resolution

When `ModelRuntime` resolves request auth for a provider, priority is:

| Order | Source | Persistence |
|------:|--------|-------------|
| 1 | CLI `--api-key` → `setRuntimeApiKey()` | Process only (not written to disk) |
| 2 | `auth.json` (API key or OAuth) | `~/.pi/agent/auth.json` by default |
| 3 | Provider environment variables (for example `ANTHROPIC_API_KEY`) | Process environment |
| 4 | `models.json` / extension fallback keys | Config file / extension registration |

`getProviderAuthStatus()` labels the active source as `runtime`, `stored`, `environment`, `fallback`, `models_json_key`, or `models_json_command`.

Auth-related guidance messages point users at `/login` and the local providers/models docs when no models or keys are available.

## Interactive login and logout

In interactive mode:

| Command | Behavior |
|---------|----------|
| `/login` | Choose auth type (OAuth vs API key), then provider |
| `/login <provider>` | Jump to that provider (match by id or display name) |
| `/logout` | Remove **stored** credentials only |

`/logout` does not clear environment variables or `models.json` keys. Stored credentials write to `getAuthPath()` (`~/.pi/agent/auth.json` by default).

Login paths:

- **OAuth** — browser / device-code / paste-redirect flows via `LoginDialogComponent` and provider-owned `oauth.login`
- **API key with `login` method** — prompt for a key (or provider-specific setup) and persist `{ type: "api_key", key }`
- **Ambient** — providers that are configured outside pi (for example AWS profiles for Bedrock, or ADC for Vertex) open an informational ambient-auth dialog instead of saving a secret

After a successful login, pi:

1. Synchronizes local provider composition and availability (`CredentialSynchronizationError` if credentials committed but local sync fails)
2. Tries to select the provider’s default model when available
3. Starts a **bounded 15s** background catalog `refresh` for that provider; on abort or error it warns and keeps cached models

Subscription OAuth examples include Codex, Claude Pro/Max, GitHub Copilot, xAI subscription, OpenRouter PKCE (mints a user-controlled API key), and Radius. API-key providers use env vars or stored keys; full provider tables live under Providers docs in the package.

## Auth file storage

Default path: `~/.pi/agent/auth.json` (`getAuthPath()` / `AuthStorage.create()`).

```json
{
  "anthropic": { "type": "api_key", "key": "sk-ant-..." },
  "openai-codex": {
    "type": "oauth",
    "access": "...",
    "refresh": "...",
    "expires": 1735689600000
  },
  "cloudflare-ai-gateway": {
    "type": "api_key",
    "key": "$CLOUDFLARE_API_KEY",
    "env": {
      "CLOUDFLARE_API_KEY": "...",
      "CLOUDFLARE_ACCOUNT_ID": "account-id",
      "CLOUDFLARE_GATEWAY_ID": "gateway-id"
    }
  }
}
```

### Credential shapes

| Type | Required fields | Notes |
|------|-----------------|-------|
| `api_key` | `type`, optional `key`, optional `env` | `key` may be literal, `$ENV` / `${ENV}`, or `!command` |
| `oauth` | `type`, `access`, `refresh`, `expires` (finite number) | Managed by provider OAuth refresh |

`api_key` resolution:

- **Literal** — used as-is
- **Env interpolation** — `$VAR` / `${VAR}`; missing vars leave the key unresolved (auth check → not configured)
- **Command** — `!…` runs once and uses stdout (process-lifetime cache via config-value resolution)
- **Escapes** — `$$` → `$`, `$!` → `!`
- **Credential-scoped `env`** — preferred over process env when resolving that credential’s key, headers, and provider config

### Storage behavior (`AuthStorage`)

- Parent dir created as `0700`; file written as `0600`
- File lock with retries (sync and async); async locks treat stale locks (~30s) and honor `AbortSignal`
- Concurrent readers coalesce reloads by file revision
- `modify` / `delete` re-read under lock so external concurrent edits to other providers are preserved
- `AuthStorage.inMemory()` and injectable `CredentialStore` backends for tests and SDK embeds
- `ReadOnlyAuthStorage` — validates shape, never creates or mutates the file (used by `auth check --no-refresh`)
- `RuntimeCredentials` — overlays non-persistent `setRuntimeApiKey` / `removeRuntimeApiKey` on top of a store

One-time migration (`migrateAuthToAuthJson`) runs only when `auth.json` is missing: merges legacy `oauth.json` and `settings.json` `apiKeys`, then renames oauth to `oauth.json.migrated` and rewrites settings without `apiKeys`.

## Ambient auth

Some providers authenticate without a pi-owned API key string:

| Pattern | Examples |
|---------|----------|
| Cloud SDK / profiles | Amazon Bedrock (`AWS_PROFILE`, IAM keys, IRSA, ECS roles); Google Vertex ADC |
| Skip-auth proxies | `AWS_BEDROCK_SKIP_AUTH=1` against a corporate Bedrock proxy |
| Provider resolve without key | Extension/provider `resolve` returns headers or ambient identity |

Interactive `/login` for an API-key method that has no interactive `login` handler shows the ambient-auth dialog (“configured outside pi”). Branch summarization and other internal model calls must tolerate request auth with **no** `apiKey` (ambient providers stream with headers/env only).

Ambient configuration is independent of `/logout`: logout only deletes stored `auth.json` entries.

## CLI auth commands

```bash
pi auth print-api-key --provider <provider> [--model <model>]
pi auth print-bearer-token --provider <provider> [--model <model>] [--min-expiry <duration>]
pi auth check --provider <provider> | --model <model> [--json] [--credentials] [--no-refresh]
```

All three require at least one of `--provider` or `--model`. They accept only those flags (no free-form messages, files, or `--api-key` on the auth command itself).

### `print-api-key` / `print-bearer-token`

| | print-api-key | print-bearer-token |
|--|---------------|--------------------|
| Credential type | Non-OAuth (API key path) | OAuth only |
| Refresh | Via normal `getAuth()` (OAuth refreshed when remaining validity is low) | Same; default minimum remaining validity **30 minutes** (`--min-expiry` like `30m`, `1h`) |
| Timeout | 15s `AbortSignal` on the whole resolve path | Same |
| Output | Raw secret on stdout | Access token on stdout |
| Ambiguity | Error if multiple providers match a bare `--model` | Same |

### `auth check`

Preflight without starting a full session. Default path **may refresh** expired OAuth; `--no-refresh` uses `ReadOnlyAuthStorage` and returns the stored access token without calling refresh.

| Flag | Effect |
|------|--------|
| `--json` | Emit status object (and optional credentials) as JSON |
| `--credentials` | Include resolved secret when status is `ready` |
| `--no-refresh` | Do not refresh OAuth; do not create missing auth files |

**Exit codes:** `0` ready · `1` not_ready · `2` invalid / check failure

| `status` | Typical `reason` | Meaning |
|----------|------------------|---------|
| `ready` | — | Provider known and credentials usable; `authType` is `api_key` or `oauth` |
| `not_ready` | `provider_not_found`, `credentials_not_configured`, `credential_not_available` | Missing provider, unresolved key, or credential extract failed |
| `invalid` | `invalid_state` | Malformed auth file, runtime composition error, or unexpected throw |

Human mode prints `ready` / `not_ready` / `invalid`, or the raw credential when `--credentials` is set. JSON mode:

```json
{"status":"ready","provider":"openai","authType":"api_key","credentials":"sk-..."}
```

Auth-check runtimes use an in-memory models store, `allowModelNetwork: false`, and `refreshOnCreate: false` so checks stay offline and do not touch catalog storage.

## SDK and embedding

```typescript
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create(); // ~/.pi/agent/auth.json + models.json
await modelRuntime.setRuntimeApiKey("anthropic", "sk-my-temp-key"); // not persisted

const customRuntime = await ModelRuntime.create({
  authPath: "/tmp/my-app/auth.json",
  modelsPath: "/tmp/my-app/models.json",
});

const { session } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime: customRuntime,
});
```

Important `ModelRuntime` surfaces:

| Method | Role |
|--------|------|
| `checkAuth(providerId)` | Presence / type without necessarily minting request headers |
| `getAuth(providerId \| model, overrides?)` | Full request auth; OAuth refresh when remaining validity &lt; `minOAuthValidityMs` (default **five minutes**) |
| `login` / `logout` | Persist credentials, then synchronize local provider snapshot |
| `setRuntimeApiKey` / `removeRuntimeApiKey` | Process-only override |
| `listCredentials` | Provider ids and types |
| `getProviderAuthStatus` | Configured source label |

`login`, `logout`, and runtime key mutations queue **per-provider** so concurrent credential ops do not interleave. They wait for local catalog/composition consistency, not remote network freshness. If the credential write succeeded but local sync failed, they throw `CredentialSynchronizationError` (`providerId`, `operation`, `credential`, `cause`) — do not blindly retry the mutation.

Pass `AbortSignal` on create, refresh, and auth ops when the embedder needs deadlines. Credential print paths use a 15s timeout; interactive post-login catalog refresh uses the same bound.

## OAuth refresh and export

- Normal request auth refreshes OAuth when remaining lifetime is below the five-minute default (`minOAuthValidityMs`).
- `pi auth print-bearer-token` defaults to requiring **30 minutes** of remaining validity so exported tokens are usable by external clients.
- `pi auth check` refreshes by default; `--no-refresh` returns the stored access token as-is.
- OpenRouter login mints a user-controlled API key (not a short-lived OAuth access token with auto-expiry semantics).

## Failure modes

### Credential refresh hang (issues #7027 / #7113)

**Symptom:** `/login` or credential mutation appears stuck behind a stalled network model catalog refresh.

**Expected behavior:**

- Login / credential ops do **not** wait on an older stalled network `refresh`
- A new provider generation can publish local availability without waiting for the hung network call
- Interactive post-login catalog refresh is capped at 15s; timeout yields a warning and cached models, without undoing the saved credential

**Mitigations for embeds:** pass a timeout signal to `refresh({ providers, signal })`; treat `result.aborted` and `result.errors` as non-fatal after a successful login.

### Auth check invalid state

Malformed `auth.json`, unresolved `$MISSING_VAR` keys, or composition errors surface as `invalid` / `not_ready` rather than crashing. `--no-refresh` never creates parent dirs or the auth file.

### CredentialSynchronizationError

Credentials may already be on disk while models/availability are stale. Inspect the error fields; re-run `refresh({ providers: [id], signal })` rather than re-login unless the stored credential itself is wrong.

### Ambient vs API-key assumptions

Do not require `options.apiKey` for internal streams (branch summaries, custom compaction, etc.). Providers may resolve only headers or cloud SDK auth.

### Experimental CLI auth input

Experimental command plumbing accepts mutually exclusive `--auth-token` / `--auth-token-file` (`parseAuthInput`). That path is separate from `pi auth check|print-*` and the interactive `/login` surface.

## Security notes

- `auth.json` is mode `0600`; agent dir parent creation uses `0700`
- Mounting host `~/.pi/agent` into containers exposes sessions **and** credentials
- Project trust gates loading project extensions/settings; it does **not** sandbox tool execution or isolate credentials
- Prefer short-lived keys or ambient cloud roles for unattended automation

## Next

<CardGroup>
  <Card title="Providers and models" href="/providers-and-models">
    Built-in catalogs, models.json, and provider-specific env vars.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    First /login or API-key session.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Full flag surface including auth subcommands.
  </Card>
  <Card title="SDK" href="/sdk">
    ModelRuntime, custom authPath, and createAgentSession wiring.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Recipe including 09-api-keys-and-oauth.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Network retries, credential refresh hangs, and process cleanup.
  </Card>
</CardGroup>

---

## 10. Providers and models

> Built-in and dynamic providers, scoped model order and refresh, models.json hot reload, and provider-retry message behavior.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/10-providers-and-models.md
- Generated: 2026-08-09T03:43:20.666Z

### Source Files

- `packages/coding-agent/test/agent-session-dynamic-provider.test.ts`
- `packages/coding-agent/test/suite/agent-session-model-extension.test.ts`
- `packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts`
- `packages/coding-agent/test/suite/regressions/6999-models-json-hot-reload.test.ts`
- `packages/coding-agent/test/suite/regressions/7153-scoped-models-refresh.test.ts`
- `packages/coding-agent/test/suite/regressions/6949-unavailable-scoped-model.test.ts`

---
title: "Providers and models"
description: "Built-in and dynamic providers, scoped model order and refresh, models.json hot reload, and provider-retry message behavior."
---

`ModelRuntime` is the coding-agent surface that composes built-in catalogs, `~/.pi/agent/models.json`, extension-registered providers, and credential availability into a single model list for sessions, CLI selection, and SDK embedding.

## Runtime surface

| Surface | Role |
|---------|------|
| `ModelRuntime` | Owns providers, model snapshots, refresh, auth checks, and streaming |
| `ModelRegistry` | Sync compatibility facade for extensions; delegates to `ModelRuntime` |
| `ModelConfig` | Immutable load/validate of `models.json` |
| `FileModelsStore` | Locked cache at `models-store.json` for dynamic remote catalogs |
| `composeModelProvider` | Layers built-in → `models.json` → extension → `modelOverrides` |

Default paths (under the agent config dir, typically `~/.pi/agent/`):

| Path | Contents |
|------|----------|
| `models.json` | User provider/model config (hot-reloaded on refresh) |
| `models-store.json` | Cached dynamic catalogs for offline reuse |
| `auth.json` | API keys and OAuth credentials (see [Authentication](/authentication)) |

Create a runtime:

```ts
import { ModelRuntime, createAgentSession } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create({
  // modelsPath defaults to ~/.pi/agent/models.json
  // allowModelNetwork: true  // optional create-time network catalog refresh
  // refreshOnCreate: false   // skip initial refresh; static models still load
});

const available = await modelRuntime.getAvailable();
const model = modelRuntime.getModel("anthropic", "claude-opus-4-8");
```

<ParamField body="allowModelNetwork" type="boolean">
When `true` and not offline, create-time refresh may hit the network. Default `false`.
</ParamField>

<ParamField body="refreshOnCreate" type="boolean">
When not `false`, `create()` calls `refresh()`. Static catalogs remain usable if skipped.
</ParamField>

<ParamField body="modelsPath" type="string | null">
Path to `models.json`. `null` disables the file. Default: agent dir `models.json`.
</ParamField>

Offline: `--offline` or `PI_OFFLINE=1` disables network model operations (`PI_OFFLINE` also forces `modelNetworkEnabled` off).

## Provider composition

Each provider id is recomposed from up to four layers:

```text
builtin / native extension base
        │
        ▼
  models.json (baseUrl, models, apiKey, oauth: "radius", …)
        │
        ▼
  extension registerProvider config (legacy config or native Provider)
        │
        ▼
  models.json modelOverrides  ← topmost user-config layer
```

Rules:

- **Untouched built-in**: if only a built-in exists (no `models.json` entry, no extension overlay), the native provider is used as-is so auth/login/stream behavior stays exact.
- **Native extension provider**: `registerProvider(provider: Provider)` or `registerNativeProvider` replaces the base for that id and clears any prior legacy extension config for the same id.
- **Legacy config form**: `registerProvider(name, config)` merges defined fields over a previous registration; `undefined` fields are preserved.
- **models.json overrides**: `modelOverrides` apply after custom-model upserts and extension model lists.
- **Broken recompose**: composition errors are recorded on the runtime; a healthy base falls back when present.

Availability: models appear in `/model`, `cycleModel`, and `--list-models` only when the provider has configured auth (stored credential, environment key, runtime key, or resolved `models.json` `apiKey` presence). Shell-command `apiKey` values are treated as configured for availability without executing the command.

## Built-in providers

Built-ins come from `@earendil-works/pi-ai` catalogs. Most wrap with remote catalog refresh and cache into `models-store.json`. Radius stays special: custom gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.

Default model ids per known provider live in `defaultModelPerProvider` (used when building fallback model objects for unknown ids on a known provider). Auth is separate: OAuth via `/login`, API keys via env/`auth.json`, or ambient cloud credentials. See [Authentication](/authentication).

CLI selection:

```bash
pi --provider anthropic --model claude-opus-4-8
pi --model openai/gpt-5.5
pi --model sonnet:high
pi --list-models
pi --list-models sonnet
```

## Dynamic providers

### models.json

Add or override providers under `~/.pi/agent/models.json`. Schema is validated by `ModelConfig` (`providers` record). Supported API types include `openai-completions`, `openai-responses`, `anthropic-messages`, and `google-generative-ai`.

Minimal local example:

```json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "compat": {
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false
      },
      "models": [{ "id": "llama3.1:8b", "reasoning": true }]
    }
  }
}
```

| Field | Notes |
|-------|--------|
| `baseUrl` | Required for non-built-in custom models |
| `api` | Provider or per-model |
| `apiKey` | Literal, `$ENV`, `${ENV}`, `!command`, or omit if auth is elsewhere |
| `oauth` | `"radius"` only; requires `baseUrl` |
| `headers` / `authHeader` | Same value-resolution rules as `apiKey` |
| `models` | Upsert by model `id` onto base catalog |
| `modelOverrides` | Patch built-in or extension models by id |
| `compat` | Provider/API compatibility flags (merged model-over-provider) |

Model defaults when omitted: `name` → `id`, `reasoning` → `false`, `input` → `["text"]`, `contextWindow` → `128000`, `maxTokens` → `16384`, zero costs.

### Extension registration

Extensions call `pi.registerProvider` in two forms:

```ts
// Native pi-ai Provider (preferred for custom auth/stream)
pi.registerProvider(createProvider({ id: "native-local", /* … */ }));

// Legacy config: override or add models
pi.registerProvider("anthropic", { baseUrl: "https://proxy.example.com" });
pi.registerProvider("my-provider", {
  baseUrl: "https://api.example.com",
  api: "openai-completions",
  apiKey: "$MY_API_KEY",
  models: [/* … */],
  refreshModels: async (ctx) => [/* dynamic list */],
});
```

Registration timing:

| When | Behavior |
|------|----------|
| Extension factory (top-level) | Available at session construction and `--list-models` |
| `session_start` handler | Applied after bind; active model re-read from runtime |
| Slash command handler | Applied immediately; no full resource reload |

On register/unregister, the session refreshes the **current** model object from the runtime so `baseUrl`/catalog changes apply to the next stream without a restart.

`unregisterProvider(name)` removes extension overlays and recomposes the built-in/`models.json` base.

## Scoped models

Scoped models limit which models are available for cycling (`cycleModel` / Ctrl+P) and optional default selection.

### Sources

Patterns come from, in practice:

1. CLI `--models <patterns>` (comma-separated)
2. Else settings `enabledModels` (same pattern language)

```bash
pi --models claude-sonnet,claude-haiku,gpt-4o
pi --models "github-copilot/*"
pi --models sonnet:high,haiku:low
```

Pattern rules (`resolveModelScope` / `resolveModelScopeFromModels`):

- Exact `provider/id` or unambiguous bare id
- Glob (`*`, `?`, `[…]`) against `provider/id` or id
- Optional `:thinkingLevel` suffix (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`)
- Prefer alias ids over dated variants when partial-matching
- Unmatched patterns warn; order of **matched** models follows pattern list order

Startup without an existing session and without `--model`: if a scope exists, prefer the saved default when it is in scope; otherwise use the first scoped model (and its explicit thinking level when set). Explicit `--thinking` wins over scoped thinking levels.

### Session order and cycling

`AgentSession.setScopedModels` stores an ordered list. `cycleModel`:

- Uses scoped list when non-empty (filtering to currently available ids)
- Otherwise cycles all available models
- Explicit scoped `thinkingLevel` overrides the session level; `undefined` inherits the current session preference (then clamps to model capability)

Interactive **Model Configuration** (`ScopedModelsSelectorComponent`):

- Session-only until save (settings `enabledModels`)
- Reorder enabled ids (order is preserved in the `/model` scoped tab)
- Toggle/enable-all/clear
- Persist only via the save keybinding

### Unavailable scoped entries

Enabled ids that no longer resolve in the catalog still appear as `provider/id [unavailable] ✗`. They can be toggled off and persisted. A partial scope is **not** cleared merely because one enabled id is missing.

### Background refresh in selectors

Opening model selectors:

1. Renders the current `getAvailableSnapshot()` immediately
2. Starts `modelRuntime.refresh({ signal })` with a 15s abort timeout
3. Updates the list when refresh completes
4. Shows status: refreshing → refreshed / timed out / per-catalog errors
5. Closing the selector aborts the in-flight refresh

## models.json hot reload

`ModelRuntime.refresh()` always reloads `models.json` from disk via `ModelConfig.load`, recomposes providers, refreshes dynamic catalogs (when network allowed), and rebuilds the availability snapshot.

Implications:

- Edit `models.json` during a session; open `/model` (or any path that calls refresh) to pick up changes—no process restart
- Invalid schema/parse errors surface through `getError()`; prior healthy state may remain until a successful load
- Missing file is empty config (not an error)
- Extension `registerProvider` triggers a non-network `refresh({ allowNetwork: false })` so composition stays consistent

```ts
const result = await modelRuntime.refresh({ signal, allowNetwork: true, providers: ["openrouter"] });
// result.aborted, result.errors: Map<providerId, Error>
```

## Auto-retry and provider retry messages

Two related retry layers exist.

### Agent-turn auto-retry

`AgentSession` retries assistant errors classified by `isRetryableAssistantError` (from `@earendil-works/pi-ai`), **except** context-overflow errors (those go through compaction).

Settings (`settings.retry`):

| Key | Default | Meaning |
|-----|---------|---------|
| `enabled` | `true` | Master switch |
| `maxRetries` | `3` | Max agent-turn retry attempts |
| `baseDelayMs` | `2000` | Exponential backoff base (2s, 4s, 8s, …) |

Events:

- `auto_retry_start` — `attempt`, `maxAttempts`, `delayMs`, `errorMessage` (provider text preserved)
- `auto_retry_end` — `success`, `attempt`, optional `finalError` (`"Retry cancelled"` if aborted)

Explicit provider retry guidance is treated as retryable. Verified examples include OpenAI help-center retry text and Bedrock “Try your request again” payloads: when `retry.enabled` is true, the agent re-prompts after backoff instead of failing the turn on the first error.

### Provider/SDK retry settings

Nested `settings.retry.provider`:

| Key | Default | Meaning |
|-----|---------|---------|
| `timeoutMs` | unset | Provider request timeout |
| `maxRetries` | unset | SDK/provider-level retries |
| `maxRetryDelayMs` | `60000` | Cap for server-requested delays |

Project and global provider retry objects merge field-wise (project overrides individual keys without wiping the rest of the nested object). See [Settings](/settings).

RPC: `set_auto_retry` / `abort_retry` control the agent-turn layer.

## Interactive and CLI commands

| Command / flag | Behavior |
|----------------|----------|
| `/model` | Model selector; snapshot first, then hot-reload refresh |
| Model configuration UI | Scope enable/order/save to `enabledModels` |
| `/login` / `/logout` | Credentials; then availability refresh |
| `--models` | Scope patterns for this run |
| `--list-models [search]` | Print available models (optional fuzzy filter) |
| `--offline` / `PI_OFFLINE` | No network catalog refresh |

## SDK notes

```ts
const modelRuntime = await ModelRuntime.create();
const available = await modelRuntime.getAvailable();

const { session } = await createAgentSession({
  model: available[0],
  thinkingLevel: "medium",
  scopedModels: [{ model: available[0], thinkingLevel: "high" }, { model: available[1] }],
  modelRuntime,
});

await session.setModel(available[1]); // requires configured auth
await session.cycleModel();           // respects scoped list order
session.setScopedModels(/* … */);
```

`setModel` throws if the provider has no configured API key/auth. Extension `ctx.setModel` returns `false` when auth is missing instead of throwing.

## Failure modes

| Symptom | Likely cause |
|---------|----------------|
| Model missing from `/model` | No auth for provider; or catalog not refreshed |
| `models.json` ignored after edit | Selector/refresh not opened; wait for background refresh |
| `Invalid models.json schema` | `ModelConfig` validation failure—fix schema and re-open `/model` |
| `Provider "x": …` in errors | Composition error; base may still be used |
| Scoped list empty after patterns | Patterns matched nothing (warnings on CLI resolve) |
| Unavailable ✗ entries stick | Expected; remove and save to clean `enabledModels` |
| Retry not firing | `retry.enabled: false`, non-retryable error, or overflow path |
| Refresh shows cached only | Timeout (15s), `aborted`, or network/`PI_OFFLINE` |

## Related pages

<CardGroup>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth login, credential storage, and ambient auth.
  </Card>
  <Card title="Settings" href="/settings">
    Settings load/reload and merge rules for `retry` and `enabledModels`.
  </Card>
  <Card title="Extensions" href="/extensions">
    Extension registration, including `registerProvider` lifecycle.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed with custom models, tools, and settings.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including custom model selection.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Retry, network, and credential failure patterns.
  </Card>
</CardGroup>

---

## 11. Settings

> Settings load and in-memory reload, stale resource refresh, and merge rules for provider retry and related session options.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/11-settings.md
- Generated: 2026-08-09T03:40:00.446Z

### Source Files

- `packages/coding-agent/examples/sdk/10-settings.ts`
- `packages/coding-agent/test/suite/regressions/3616-settings-inmemory-reload.test.ts`
- `packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts`
- `packages/coding-agent/test/suite/regressions/7572-provider-retry-settings-merge.test.ts`
- `packages/coding-agent/README.md`
- `packages/coding-agent/package.json`

---
title: "Settings"
description: "Settings load and in-memory reload, stale resource refresh, and merge rules for provider retry and related session options."
---

`SettingsManager` in `@earendil-works/pi-coding-agent` owns settings load, merge, in-memory overrides, queued persistence, and reload. Sessions receive a manager instance (disk-backed, storage-backed, or pure in-memory) and read options such as compaction, retry, thinking level, images, theme, and prompt filters through that surface.

## Construction surfaces

| Factory | Purpose |
|---------|---------|
| `SettingsManager.create(cwd)` | Load merged global + project settings from disk for a working directory |
| `SettingsManager.inMemory(initial?)` | Seed settings with no file I/O (tests and ephemeral embeds) |
| `SettingsManager.fromStorage(storage)` | Build from an `InMemorySettingsStorage` (or compatible storage) with explicit global/project layers |

Public package entry for embedders:

```ts
import {
  createAgentSession,
  SessionManager,
  SettingsManager,
} from "@earendil-works/pi-coding-agent";
```

Package `piConfig.configDir` is `.pi` (agent config root used with per-agent directories such as `agentDir` in runtime factories).

## Load and merge

`SettingsManager.create(cwd)` loads **global + project** settings into one effective view. `getGlobalSettings()` returns the effective object the manager holds after load and overrides (naming reflects the global store surface used by the manager, not “global-only keys”).

Project and global layers merge at nested objects. For **provider retry**, project values override only the keys they set; sibling global keys remain:

| Layer | Path | Keys in evidence |
|-------|------|------------------|
| Global | `retry.provider` | `timeoutMs`, `maxRetryDelayMs` |
| Project | `retry.provider` | `maxRetries` |

After merge, `getProviderRetrySettings()` flattens the effective provider retry object:

```json
{
  "timeoutMs": 30000,
  "maxRetries": 2,
  "maxRetryDelayMs": 45000
}
```

Storage for layered tests uses `InMemorySettingsStorage.withLock("global" | "project", () => JSON.stringify(...))`, then `SettingsManager.fromStorage(storage)`.

## Overrides, setters, and persistence

### Runtime overrides

`applyOverrides(partial)` patches the live manager without requiring a full replace. Documented example keys:

```ts
settingsManager.applyOverrides({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 5, baseDelayMs: 1000 },
});
```

Pass the manager into session construction:

```ts
const { session } = await createAgentSession({
  settingsManager,
  sessionManager: SessionManager.inMemory(),
});
```

### Immediate memory vs durable write

- **Setters** (for example `setDefaultThinkingLevel`, `setTheme`) update memory immediately and **queue** persistence writes.
- Call **`await settingsManager.flush()`** when the app needs a durability boundary.
- **`drainErrors()`** returns queued I/O failures for the app layer:

```ts
const settingsErrors = settingsManager.drainErrors();
for (const { scope, error } of settingsErrors) {
  console.warn(`Warning (${scope} settings): ${error.message}`);
}
```

Each error entry has `scope` and `error` (`Error` with `.message`).

## In-memory managers and reload

`SettingsManager.inMemory({ ... })` is the no-disk path. **`reload()` must not wipe seeded values.**

Verified invariants:

| Operation | Result |
|-----------|--------|
| `inMemory({...})` then `await reload()` | Initial keys and getters unchanged |
| Same manager under `DefaultResourceLoader.reload()` | Thinking level, image auto-resize, compaction flags preserved |
| `setTheme("dark")` → `flush()` → `reload()` | Theme plus prior in-memory keys all retained |

Example seed and getters:

```ts
const settingsManager = SettingsManager.inMemory({
  defaultThinkingLevel: "high",
  images: { autoResize: false },
  compaction: { enabled: false },
});

await settingsManager.reload();

settingsManager.getDefaultThinkingLevel(); // "high"
settingsManager.getImageAutoResize();      // false
settingsManager.getCompactionEnabled();    // false
settingsManager.getGlobalSettings();       // full effective object
```

`DefaultResourceLoader` accepts `settingsManager` plus resource toggles (`noExtensions`, `noSkills`, `noPromptTemplates`, `noThemes`, `noContextFiles`) so resource reloads can run without reloading those asset classes.

## Disk settings and stale resource refresh

Agent-directory `settings.json` participates in session reload. After startup, writing filters and calling `session.reload()` refreshes both settings and dependent resources.

Example: exclude a prompt template that was already loaded:

```json
{
  "prompts": ["-prompts/test.md"]
}
```

After `await runtime.session.reload()`:

1. `runtime.services.settingsManager.getGlobalSettings().prompts` equals `["-prompts/test.md"]`.
2. Session `promptTemplates` no longer include the excluded template name (`test`).

This is the stale-resource path: reload re-reads settings and re-applies resource filters so the session does not keep templates that settings now exclude.

Interactive mode also exposes a built-in **`/settings`** UI that can temporarily replace the editor; embedders use the same manager APIs rather than that TUI.

## Settings keys in evidence

Keys appear in SDK examples, regressions, or both. This table is the evidenced surface only—not a full schema.

| Key / path | Role |
|------------|------|
| `compaction.enabled` | Compaction on/off |
| `retry.enabled` | Retry feature flag |
| `retry.maxRetries` | Top-level retry count (override example) |
| `retry.baseDelayMs` | Top-level retry base delay (override example) |
| `retry.provider.timeoutMs` | Provider retry timeout |
| `retry.provider.maxRetries` | Provider retry count |
| `retry.provider.maxRetryDelayMs` | Provider max delay between retries |
| `defaultThinkingLevel` | Default thinking level (e.g. `"high"`, `"low"`) |
| `images.autoResize` | Image auto-resize |
| `theme` | Theme id (e.g. `"dark"`) |
| `prompts` | Prompt include/exclude patterns (e.g. `"-prompts/test.md"`) |

### Accessors

| Method | Returns |
|--------|---------|
| `getGlobalSettings()` | Effective settings object |
| `getProviderRetrySettings()` | Flattened `retry.provider` merge result |
| `getDefaultThinkingLevel()` | Thinking level string |
| `getImageAutoResize()` | boolean |
| `getCompactionEnabled()` | boolean |
| `getTheme()` | Theme string |

## Lifecycle diagram

```text
  create(cwd) | inMemory(seed) | fromStorage(storage)
              │
              ▼
     ┌────────────────────┐
     │  SettingsManager   │
     │  effective settings│
     └─────────┬──────────┘
               │
     applyOverrides / setters ──► memory update
               │                    │
               │                    └─ queue write ─► flush() ─► drainErrors()
               │
     createAgentSession({ settingsManager, ... })
               │
               ▼
     session / DefaultResourceLoader / AgentSessionRuntime
               │
               ├── reload()  (in-memory: keep seed + flushed sets)
               └── session.reload()  (disk: re-read settings.json,
                                      refresh prompts / resources)
```

## SDK patterns

### Disk-backed session with overrides

```ts
const cwd = process.cwd();
const settingsManager = SettingsManager.create(cwd);
settingsManager.applyOverrides({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 5, baseDelayMs: 1000 },
});

const { session } = await createAgentSession({
  settingsManager,
  sessionManager: SessionManager.inMemory(),
});
// ...
session.dispose();
```

### Test / no I/O

```ts
const inMemorySettings = SettingsManager.inMemory({
  compaction: { enabled: false },
  retry: { enabled: false },
});

const { session } = await createAgentSession({
  settingsManager: inMemorySettings,
  sessionManager: SessionManager.inMemory(),
});
```

### Durability boundary

```ts
settingsManager.setDefaultThinkingLevel("low");
await settingsManager.flush();
const settingsErrors = settingsManager.drainErrors();
```

Reference implementation: `packages/coding-agent/examples/sdk/10-settings.ts`.

## Failure modes and regression contracts

| Issue | Contract |
|-------|----------|
| Nested project `retry.provider` | Must deep-merge with global provider retry; do not drop sibling global keys |
| In-memory `reload()` | Must keep initial seed; must keep values set and flushed before reload |
| Resource loader reload | Must not reset in-memory settings when reloading other resources |
| Session reload after `settings.json` change | Settings and filtered resources (e.g. prompt templates) must match new disk settings |

<Warning>
Setters only queue writes. Without `flush()`, process exit may miss persistence. Always surface `drainErrors()` at the app boundary after flush or bulk updates.
</Warning>

<Note>
Provider retry lives under `retry.provider` in storage layers and is read as a flat object via `getProviderRetrySettings()`. Top-level `retry` fields used in `applyOverrides` (`enabled`, `maxRetries`, `baseDelayMs`) are a separate override path from the nested provider merge.
</Note>

## Related pages

<CardGroup>
  <Card title="Providers and models" href="/providers-and-models">
    Built-in and dynamic providers, model refresh, and provider-retry messaging.
  </Card>
  <Card title="Compaction" href="/compaction">
    Auto and manual compaction triggers and session interactions.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed pi with custom models, tools, and settings managers.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including settings and full-control setups.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    AgentSessionRuntime services and embedding without the interactive TUI.
  </Card>
  <Card title="Prompt templates" href="/prompt-templates">
    Prompt templates as session configuration and filter targets.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Retry and network failures and related operational issues.
  </Card>
</CardGroup>

---

## 12. Context files

> How project context files are discovered and injected into sessions, including SDK wiring for context file inputs.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/12-context-files.md
- Generated: 2026-08-09T03:40:14.584Z

### Source Files

- `packages/coding-agent/examples/sdk/07-context-files.ts`
- `packages/coding-agent/README.md`
- `packages/coding-agent/src/core/agent-session.ts`
- `packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts`
- `packages/coding-agent/examples/sdk/README.md`

---
title: "Context files"
description: "How project context files are discovered and injected into sessions, including SDK wiring for context file inputs."
---

Context files in `@earendil-works/pi-coding-agent` are `AGENTS.md` documents: project-specific instructions that `DefaultResourceLoader` discovers from the working directory tree and that sessions load into the system prompt. Discovery and injection are owned by the resource loader; `createAgentSession({ resourceLoader })` wires the loader into `AgentSession`, which is shared across interactive, print, and RPC run modes.

## What counts as a context file

| Term | Meaning in this package |
|------|-------------------------|
| Context file | An `AGENTS.md` entry with `path` and `content` |
| Purpose | Project-specific instructions for the model |
| Injection surface | System prompt (not a user-turn message) |
| Loader API | `DefaultResourceLoader` / `ResourceLoader` |
| Override hook | `agentsFilesOverride` |

Interactive mode surfaces loaded files in the **startup header** alongside prompt templates, skills, and extensions.

## Discovery

`DefaultResourceLoader` discovers `AGENTS.md` files by **walking up from `cwd`**.

```typescript
const loader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
});
await loader.reload();

const discovered = loader.getAgentsFiles().agentsFiles;
for (const file of discovered) {
  console.log(`  - ${file.path} (${file.content.length} chars)`);
}
```

### Loader construction inputs

| Input | Role |
|-------|------|
| `cwd` | Root of the upward walk for discovery |
| `agentDir` | Agent config directory (via `getAgentDir()` in the SDK example) |
| `agentsFilesOverride` | Optional transform or full replace of the discovered list |
| `reload()` | Must be awaited after construction (and after changing overrides) before reading files |

### Discovered file shape

Each entry in `getAgentsFiles().agentsFiles` exposes at least:

| Field | Type (observed) | Notes |
|-------|-----------------|-------|
| `path` | string | Filesystem path, or a virtual path when injected via override |
| `content` | string | Full file body; length is usable as a size signal |

<Note>
Evidence documents discovery as “walking up from `cwd`” and the `path` / `content` fields only. Stop conditions, merge order across multiple `AGENTS.md` files, and system-prompt formatting details are not specified in the available sources.
</Note>

## Injection into sessions

Context files are not passed as a separate `createAgentSession` option. They flow through the resource loader:

1. Build and `reload()` a `DefaultResourceLoader` (with or without `agentsFilesOverride`).
2. Pass `resourceLoader: loader` into `createAgentSession`.
3. The session uses that loader for extensions, skills, prompts, themes, **context files**, and the system prompt.

```typescript
const { session } = await createAgentSession({
  resourceLoader: loader,
  sessionManager: SessionManager.inMemory(),
});
```

`AgentSessionConfig.resourceLoader` is required and documented as the loader for extensions, skills, prompts, themes, context files, and the system prompt. `AgentSession` is the shared lifecycle layer for interactive, print, and RPC modes; each mode adds its own I/O on top.

### Related loader controls

Context files are orthogonal to other `DefaultResourceLoader` overrides used in the same session setup:

| Override | Effect |
|----------|--------|
| `agentsFilesOverride` | Append, replace, or clear `AGENTS.md` context |
| `systemPromptOverride` | Modify or replace the base system prompt string |
| `skillsOverride` | Control skill discovery |
| `promptsOverride` | Control prompt templates |
| `extensionFactories` | Register extensions |

Disabling discovery and replacing the system prompt are separate knobs. Full-control embeds often set both.

## SDK wiring

### Package surface

Import from `@earendil-works/pi-coding-agent`:

- `createAgentSession`
- `DefaultResourceLoader`
- `getAgentDir`
- `SessionManager`

Reference example: `packages/coding-agent/examples/sdk/07-context-files.ts` (run from the package with `npx tsx examples/sdk/07-context-files.ts`).

### `createAgentSession` options (context-relevant)

| Option | Default | Context-file role |
|--------|---------|-------------------|
| `resourceLoader` | `DefaultResourceLoader` | Owns discovery, overrides, and context-file access |
| `cwd` | `process.cwd()` | Working directory used when the session is created with defaults |
| `agentDir` | `~/.pi/agent` | Config directory for the agent |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence; in-memory is fine for loader-only demos |
| `modelRuntime` | Runtime from `agentDir` auth/models | Not required for loader discovery itself; required for model turns |

### Override patterns

#### Append a virtual context file

Spread discovered files and add an in-memory entry (paths may be virtual):

```typescript
const loader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  agentsFilesOverride: (current) => ({
    agentsFiles: [
      ...current.agentsFiles,
      {
        path: "/virtual/AGENTS.md",
        content: `# Project Guidelines

## Code Style
- Use TypeScript strict mode
- No any types
- Prefer const over let`,
      },
    ],
  }),
});
await loader.reload();
```

After reload, `getAgentsFiles().agentsFiles` includes both discovered and virtual entries. The example logs session creation as using `discovered.length + 1` files when one virtual file is appended.

#### Disable context files

Return an empty list from the override (no discovery retained):

```typescript
const resourceLoader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are helpful.",
  agentsFilesOverride: () => ({ agentsFiles: [] }),
  // ...other full-control overrides
});
await resourceLoader.reload();
```

<Warning>
Call `await loader.reload()` after configuring overrides. Reading `getAgentsFiles()` before reload does not apply the configured discovery and override pipeline.
</Warning>

### End-to-end checklist

<Steps>
  <Step title="Construct the loader">
    Pass `cwd`, `agentDir`, and optional `agentsFilesOverride`.
  </Step>
  <Step title="Reload resources">
    `await loader.reload()` so discovery and overrides materialize.
  </Step>
  <Step title="Inspect context files (optional)">
    Read `loader.getAgentsFiles().agentsFiles` and log `path` / `content.length`.
  </Step>
  <Step title="Create the session">
    `await createAgentSession({ resourceLoader: loader, sessionManager, ... })`.
  </Step>
  <Step title="Dispose when finished">
    `session.dispose()` for short-lived SDK scripts.
  </Step>
</Steps>

## Interactive visibility

In interactive mode, the startup header reports **loaded `AGENTS.md` files** (with prompt templates, skills, and extensions). That is the user-visible confirmation that context files were discovered for the current project session.

## Architecture

```text
cwd (walk up)
   │
   ▼
DefaultResourceLoader
   ├─ discover AGENTS.md
   ├─ agentsFilesOverride(current) → { agentsFiles }
   ├─ getAgentsFiles().agentsFiles  → [{ path, content }, ...]
   └─ system prompt (+ other resources)
          │
          ▼
createAgentSession({ resourceLoader })
          │
          ▼
     AgentSession  ── shared by interactive / print / RPC
```

## Constraints and unknowns

| Supported by evidence | Not specified in available evidence |
|----------------------|-------------------------------------|
| Name: `AGENTS.md` | Exact walk stop rules (repo root, filesystem root, etc.) |
| Upward walk from `cwd` | Ordering when multiple `AGENTS.md` files exist |
| `path` + `content` entries | Exact system-prompt wrapping or delimiters |
| Virtual paths via override | Size limits, truncation, or encoding rules |
| Empty override disables files | CLI flags dedicated only to context files |
| Loaded files shown in interactive startup header | Hot-reload of on-disk `AGENTS.md` during a live session |

## Related pages

<CardGroup>
  <Card title="SDK" href="/sdk">
    Embed pi with `createAgentSession`, custom models, tools, and settings.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes, including `07-context-files.ts` and full-control setups.
  </Card>
  <Card title="Agent sessions" href="/agent-sessions">
    Session lifecycle and the runtime that owns a conversation turn.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    `AgentSessionRuntime` services and embedding without the interactive TUI.
  </Card>
  <Card title="Prompt templates" href="/prompt-templates">
    Custom system prompts and reusable prompt configuration.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK modes that share `AgentSession`.
  </Card>
</CardGroup>

---

## 13. Skills

> SKILL.md frontmatter rules, naming constraints, collision precedence, disable-model-invocation, and valid versus rejected skill fixtures.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/13-skills.md
- Generated: 2026-08-09T03:40:03.558Z

### Source Files

- `packages/coding-agent/examples/sdk/04-skills.ts`
- `packages/coding-agent/test/fixtures/skills/valid-skill/SKILL.md`
- `packages/coding-agent/test/fixtures/skills/disable-model-invocation/SKILL.md`
- `packages/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts`
- `packages/coding-agent/test/fixtures/skills-collision/first/calendar/SKILL.md`
- `packages/coding-agent/test/fixtures/skills/missing-description/SKILL.md`

---
title: "Skills"
description: "SKILL.md frontmatter rules, naming constraints, collision precedence, disable-model-invocation, and valid versus rejected skill fixtures."
---

Skills are specialized instruction documents (`SKILL.md`) that `DefaultResourceLoader` discovers and loads into the agent session. Each skill has a `name`, `description`, optional `disableModelInvocation` flag, and markdown body content. Discovery covers project, user (agent), and package skill directories; same-name collisions resolve with a fixed precedence order and emit diagnostics for the losing skill.

## Skill file layout

A skill is a directory that contains a single `SKILL.md` file. Layouts used by auto-discovery and package registration:

:::files
project/
  .pi/
    skills/
      {name}/
        SKILL.md
    settings.json          # optional: { "packages": ["..."] }

{agentDir}/                # user agent dir from getAgentDir()
  skills/
    {name}/
      SKILL.md
  settings.json            # optional: { "packages": ["..."] }

{package}/
  package.json             # pi.skills: ["skills/{name}"]
  skills/
    {name}/
      SKILL.md
:::

| Scope | Path | How it is registered |
| --- | --- | --- |
| Project | `{cwd}/.pi/skills/{name}/SKILL.md` | Auto-discovered |
| User | `{agentDir}/skills/{name}/SKILL.md` | Auto-discovered |
| Package | `{pkgDir}/skills/{name}/SKILL.md` | `package.json` → `pi.skills`, then listed in `settings.json` `packages` |

SDK example comments document discovery from `cwd/.pi/skills`, `~/.pi/agent/skills` (via `getAgentDir()`), and related locations.

## SKILL.md frontmatter

`SKILL.md` is YAML frontmatter plus a markdown body. Fields observed in fixtures and runtime `Skill` objects:

| Frontmatter key | Runtime field | Required | Notes |
| --- | --- | --- | --- |
| `name` | `name` | Yes (all fixtures set it) | Skill identifier used for lookup and collision |
| `description` | `description` | Expected for a valid skill | Absent in the `missing-description` fixture |
| `disable-model-invocation` | `disableModelInvocation` | No | Boolean; `true` means manual-only invocation |

### Valid skill

```markdown
---
name: valid-skill
description: A valid skill for testing purposes.
---

# Valid Skill

This is a valid skill that follows the Agent Skills standard.
```

Minimum valid shape used in package/user/project collision tests:

```markdown
---
name: web-fetch
description: Package web-fetch skill
---
Package skill content
```

### Manual-only skill (`disable-model-invocation`)

```markdown
---
name: disable-model-invocation
description: A skill that cannot be invoked by the model.
disable-model-invocation: true
---

# Manual Only Skill

This skill can only be invoked via /skill:disable-model-invocation.
```

When `disable-model-invocation: true`, the model does not invoke the skill; the documented manual path is `/skill:{name}` (for example `/skill:disable-model-invocation`).

### Incomplete fixture (missing description)

```markdown
---
name: missing-description
---

# Missing Description

This skill has no description field.
```

This fixture is the contrast case for a valid skill: `name` is present, `description` is not. Treat a complete skill as having both `name` and `description` in frontmatter.

### Collision fixture sample

```markdown
---
name: calendar
description: First calendar skill.
---

# Calendar (First)

This is the first calendar skill.
```

## Runtime `Skill` shape

SDK-constructed skills use the exported `Skill` type:

| Field | Type / example | Purpose |
| --- | --- | --- |
| `name` | `"my-skill"` | Stable skill id |
| `description` | string | Human/model-facing summary |
| `filePath` | `"/virtual/SKILL.md"` | Path or virtual path |
| `baseDir` | `"/virtual"` | Skill base directory |
| `sourceInfo` | from `createSyntheticSourceInfo(...)` | Provenance; SDK example uses `{ source: "sdk" }` |
| `disableModelInvocation` | `boolean` | Maps from frontmatter `disable-model-invocation` |

## Discovery and load API

`DefaultResourceLoader` loads skills and exposes them after `reload()`:

```ts
import {
  createAgentSession,
  createSyntheticSourceInfo,
  DefaultResourceLoader,
  getAgentDir,
  SessionManager,
  type Skill,
} from "@earendil-works/pi-coding-agent";

const loader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
});
await loader.reload();

const { skills: allSkills, diagnostics } = loader.getSkills();
```

| API | Role |
| --- | --- |
| `new DefaultResourceLoader({ cwd, agentDir, skillsOverride? })` | Construct loader for project + agent dirs |
| `await loader.reload()` | Discover and resolve skills (and other resources) |
| `loader.getSkills()` | Returns `{ skills, diagnostics }` |
| `createAgentSession({ resourceLoader, sessionManager })` | Attach loaded skills to a session |

### `skillsOverride`

Optional hook to filter, merge, or replace discovered skills before session use:

```ts
const customSkill: Skill = {
  name: "my-skill",
  description: "Custom project instructions",
  filePath: "/virtual/SKILL.md",
  baseDir: "/virtual",
  sourceInfo: createSyntheticSourceInfo("/virtual/SKILL.md", { source: "sdk" }),
  disableModelInvocation: false,
};

const loader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  skillsOverride: (current) => {
    const filteredSkills = current.skills.filter(
      (s) => s.name.includes("browser") || s.name.includes("search"),
    );
    return {
      skills: [...filteredSkills, customSkill],
      diagnostics: current.diagnostics,
    };
  },
});
```

Override contract:

- Input `current` has `skills` and `diagnostics`.
- Return value must include `skills` and `diagnostics` (pass through or replace).
- Use cases: keep only matching names, inject inline `Skill` objects, preserve diagnostics.

## Package skills

A package skill is declared on the package and enabled via settings.

**Package manifest** (`package.json`):

```json
{
  "name": "fake-pkg-web-fetch",
  "version": "1.0.0",
  "pi": {
    "skills": ["skills/web-fetch"]
  }
}
```

**Settings** (`settings.json` under agent dir or project `.pi`):

```json
{
  "packages": ["/path/to/fake-package-web-fetch"]
}
```

| Setting key | Value | Effect |
| --- | --- | --- |
| `packages` | array of package directory paths | Registers package resources, including skills listed under `pi.skills` |

User-scope settings live under `agentDir`; project-scope under `{cwd}/.pi`.

## Collision precedence

When multiple skills share the same `name`, exactly one wins. Precedence (highest first):

```text
project (.pi/skills)  >  user (agentDir/skills)  >  package (pi.skills)
```

| Winner | Over | Verified behavior |
| --- | --- | --- |
| User auto-discovered skill | Package skill | Winner `filePath` and `description` are the user skill |
| Project auto-discovered skill | Package skill | Winner is the project skill |
| Project skill | User skill (and package) | Full stack: project beats user beats package |

Winning skill identity is the loaded `Skill` entry with that `name` after `reload()`; losing definitions are not returned in `skills`.

### Collision diagnostics

`getSkills().diagnostics` can include collision entries:

| Field | Meaning |
| --- | --- |
| `type` | `"collision"` |
| `collision.name` | Shared skill name (e.g. `"web-fetch"`) |
| `collision.loserPath` | Path of the skill that lost (package path contains the package dir name) |

Example assertion shape from the regression suite: when a user skill overrides a package skill, diagnostics include a `collision` for `web-fetch` whose `loserPath` contains the package directory (e.g. `fake-package`).

## Fixture catalog

| Fixture path | Frontmatter | Role |
| --- | --- | --- |
| `test/fixtures/skills/valid-skill/SKILL.md` | `name` + `description` | Valid Agent Skills–style skill |
| `test/fixtures/skills/disable-model-invocation/SKILL.md` | + `disable-model-invocation: true` | Manual-only; `/skill:disable-model-invocation` |
| `test/fixtures/skills/missing-description/SKILL.md` | `name` only | Incomplete: no `description` |
| `test/fixtures/skills-collision/first/calendar/SKILL.md` | `name: calendar` + description | Named collision sample |

## Authoring checklist

<Steps>
  <Step title="Create the skill directory">
    Place the skill under project (`.pi/skills/{name}/`), user (`{agentDir}/skills/{name}/`), or a package (`skills/{name}/` with `pi.skills` entry).
  </Step>
  <Step title="Write SKILL.md frontmatter">
    Set `name` and `description`. Optionally set `disable-model-invocation: true` for manual-only skills.
  </Step>
  <Step title="Add markdown body">
    Body after the closing `---` is the skill content loaded as specialized instructions.
  </Step>
  <Step title="Register packages if needed">
    For package skills, list the skill path in `package.json` `pi.skills` and add the package path to `settings.json` `packages`.
  </Step>
  <Step title="Verify with the loader">
    Construct `DefaultResourceLoader`, `await reload()`, then inspect `getSkills()` for the expected `name`, `description`, `filePath`, and any collision diagnostics.
  </Step>
</Steps>

## SDK session wiring

Minimal session with a custom loader and in-memory session manager:

```ts
const { session } = await createAgentSession({
  resourceLoader: loader,
  sessionManager: SessionManager.inMemory(),
});
// ...
session.dispose();
```

Skills contribute specialized instructions to the system prompt for that session. Filter or replace them only through `skillsOverride` (or by controlling which files/packages are present on disk).

## Related pages

<CardGroup>
  <Card title="Themes and Pi packages" href="/themes-and-packages">
    How shareable Pi packages declare and bundle skills with extensions, templates, and themes.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed pi with the package main export: resource loaders, settings, and session construction.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including skills configuration with DefaultResourceLoader.
  </Card>
  <Card title="Settings" href="/settings">
    Settings load paths and merge behavior for package lists and related session options.
  </Card>
  <Card title="Extensions" href="/extensions">
    TypeScript extension registration alongside skills and other resource types.
  </Card>
  <Card title="Context files" href="/context-files">
    Separate project context file discovery and injection into sessions.
  </Card>
</CardGroup>

---

## 14. Extensions

> TypeScript extension registration, inline naming, active tools on next turn, OAuth prompt input, and shutdown cleanup contracts.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/14-extensions.md
- Generated: 2026-08-09T03:41:34.979Z

### Source Files

- `packages/coding-agent/examples/extensions/README.md`
- `packages/coding-agent/examples/sdk/06-extensions.ts`
- `packages/coding-agent/examples/extensions/auto-commit-on-exit.ts`
- `packages/coding-agent/test/suite/regressions/6260-inline-extension-naming.test.ts`
- `packages/coding-agent/test/suite/regressions/6162-extension-active-tools-next-turn.test.ts`
- `packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts`

---
title: "Extensions"
description: "TypeScript extension registration, inline naming, active tools on next turn, OAuth prompt input, and shutdown cleanup contracts."
---

Extensions are TypeScript modules that receive an `ExtensionAPI` instance and can register tools, commands, event handlers, and UI hooks. Discovery uses standard agent directories and optional loader paths; SDK embeds can also pass inline factories through `DefaultResourceLoader`.

## Extension shape

An extension is a TypeScript file whose **default export** is a factory:

```ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  // register tools, commands, and event handlers
}
```

The factory may be:

| Form | How it is supplied | Display path after load |
|------|--------------------|-------------------------|
| File path | Discovery, `--extension`, `additionalExtensionPaths`, or settings | Real filesystem path |
| Bare factory | `extensionFactories: [(pi) => { ... }]` | `<inline:N>` (1-based) |
| Named wrapper | `extensionFactories: [{ name, factory, hidden? }]` | `<inline:name>` |

## Discovery and load

By default, extension files are discovered from:

- `~/.pi/agent/extensions/`
- `<cwd>/.pi/extensions/`
- Paths listed in settings.json under the `"extensions"` array

CLI one-off load:

```bash
pi --extension examples/extensions/permission-gate.ts
```

Or install for auto-discovery:

```bash
cp permission-gate.ts ~/.pi/agent/extensions/
```

### SDK / `DefaultResourceLoader`

Embedders construct a loader, optionally add paths and factories, then `reload()` before `createAgentSession`:

```ts
import {
  createAgentSession,
  DefaultResourceLoader,
  getAgentDir,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const resourceLoader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  additionalExtensionPaths: [
    "./my-logging-extension.ts",
    "./my-safety-extension.ts",
  ],
  extensionFactories: [
    (pi) => {
      pi.on("agent_start", () => {
        console.log("[Inline Extension] Agent starting");
      });
    },
  ],
});
await resourceLoader.reload();

const { session } = await createAgentSession({
  resourceLoader,
  sessionManager: SessionManager.inMemory(),
});
```

After load, `loader.getExtensions()` returns the registered extension list (used for identity, hidden state, and diagnostics).

## Inline extension naming

When factories are passed via `extensionFactories` (not file paths), `DefaultResourceLoader` assigns synthetic paths:

| Input | Resulting `path` | Notes |
|-------|------------------|--------|
| Bare `(pi) => {}` | `<inline:1>`, `<inline:2>`, … | Index is 1-based over bare factories in load order |
| `{ name: "my-provider", factory }` | `<inline:my-provider>` | Name is used as the path suffix |
| `{ name: "built-in", factory, hidden: true }` | `<inline:built-in>` | `hidden: true` is preserved on the extension record |
| Mixed list: bare, named, bare | `<inline:1>`, `<inline:named-ext>`, `<inline:3>` | Named entries do not consume bare indices; bare numbering continues |

```ts
// Bare factories
extensionFactories: [noop, noop]
// → paths: <inline:1>, <inline:2>

// Named wrappers
extensionFactories: [
  { name: "my-provider", factory: noop },
  { name: "my-commands", factory: noop },
]
// → paths: <inline:my-provider>, <inline:my-commands>
```

## Registration surface

Inside the factory, typical `ExtensionAPI` operations include:

| API | Role |
|-----|------|
| `pi.on(event, handler)` | Subscribe to lifecycle / tool events |
| `pi.registerTool({ ... })` | Register a custom tool (name, label, description, parameters, execute) |
| `pi.registerCommand(name, { description, handler })` | Register a slash command |
| `pi.setActiveTools(names)` | Replace the active tool name set |
| `pi.getActiveTools()` | Read the current active tool names |
| `pi.exec(cmd, args)` | Run a subprocess (e.g. git) |
| `pi.events` | Inter-extension event bus (see example extensions) |

### Example: tools, commands, and tool blocking

```ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("agent_start", async () => {
    console.log("[Extension] Agent starting");
  });

  pi.on("tool_call", async (event) => {
    console.log(`[Extension] Tool: ${event.toolName}`);
    // Return { block: true, reason: "..." } to block execution
    return undefined;
  });

  pi.on("agent_end", async (event) => {
    console.log(`[Extension] Done, ${event.messages.length} messages`);
  });

  pi.registerTool({
    name: "my_tool",
    label: "My Tool",
    description: "Does something useful",
    parameters: Type.Object({
      input: Type.String(),
    }),
    execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => ({
      content: [{ type: "text", text: `Processed: ${params.input}` }],
      details: {},
    }),
  });

  pi.registerCommand("mycommand", {
    description: "Do something",
    handler: async (args, ctx) => {
      ctx.ui.notify(`Command executed with: ${args}`);
    },
  });
}
```

### Common events (from examples and tests)

| Event | Typical use |
|-------|-------------|
| `agent_start` / `agent_end` | Logging, session bookends |
| `tool_call` | Gate or log tool execution; optional block |
| `before_agent_start` | Override `systemPrompt` for the run |
| `session_start` | Late tool registration after startup |
| `session_shutdown` | Cleanup (git commit, sockets, resources) |
| `project_trust` | Trust prompts for user/global and CLI extensions |
| `model_select` | React to model changes (status bar, etc.) |
| `input` | Transform user input (e.g. expand `!{command}`) |
| `user_bash` | Interactive shell passthrough |
| `resources_discover` | Load skills, prompts, and themes dynamically |

## Active tools on the next provider turn

`pi.setActiveTools(names)` changes which tools the provider sees **on the next request in the same run**, not only on a later user turn.

```mermaid
sequenceDiagram
  participant Provider
  participant Session
  participant ExtensionTool

  Provider->>Session: tools = [switch_tools]
  Session->>ExtensionTool: execute switch_tools
  ExtensionTool->>Session: pi.setActiveTools(["after_switch"])
  ExtensionTool-->>Session: tool result
  Session->>Provider: tools = [after_switch]
  Provider-->>Session: final assistant message
```

### Contracts (regression-backed)

| Behavior | Contract |
|----------|----------|
| Mid-run refresh | After a tool calls `pi.setActiveTools(["after_switch"])`, the **next** provider context lists `after_switch`, not the previous set |
| Session API | `session.getActiveToolNames()` matches the post-switch set after the prompt completes |
| Additive load | `pi.setActiveTools([...pi.getActiveTools(), "after_load"])` keeps prior tools and adds the new one |
| Tool-result audit | Additive names appear on the current tool-result message as `addedToolNames` (e.g. `["after_load"]`) |
| System prompt stability | A `before_agent_start` handler that returns `{ systemPrompt: ... }` keeps that override on **both** provider requests when tools change mid-run |

Minimal switch pattern:

```ts
pi.registerTool({
  name: "switch_tools",
  // ...
  execute: async () => {
    pi.setActiveTools(["after_switch"]);
    return {
      content: [{ type: "text", text: "switched" }],
      details: {},
    };
  },
});
```

Additive pattern (records delta on the tool result):

```ts
execute: async () => {
  pi.setActiveTools([...pi.getActiveTools(), "after_load"]);
  return {
    content: [{ type: "text", text: "loaded" }],
    details: {},
  };
};
```

## Shutdown cleanup contracts

Extensions that must release resources on exit should handle `session_shutdown`. `runtimeHost.dispose` is what emits that event for the interactive runtime.

### `session_shutdown` handler example

```ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("session_shutdown", async (_event, ctx) => {
    const { stdout: status, code } = await pi.exec("git", ["status", "--porcelain"]);
    if (code !== 0 || status.trim().length === 0) {
      return;
    }
    // ... build commit message from ctx.sessionManager.getEntries()
    await pi.exec("git", ["add", "-A"]);
    const { code: commitCode } = await pi.exec("git", ["commit", "-m", commitMessage]);
    if (commitCode === 0 && ctx.hasUI) {
      ctx.ui.notify(`Auto-committed: ${commitMessage}`, "info");
    }
  });
}
```

### Ordering: signal vs interactive quit

Graceful shutdown order depends on the trigger. Extension teardown that does **not** write to the TTY (sockets, files, git) must not be skipped when a later terminal restore fails.

| Trigger | Order | Rationale |
|---------|-------|-----------|
| Signal (`SIGTERM` / `SIGHUP`, `fromSignal: true`) | `runtimeHost.dispose` → terminal `drainInput` → `stop` | Emit `session_shutdown` **before** terminal writes so extension cleanup runs even if the TTY is dead or stalled |
| Interactive quit (Ctrl+D, `/quit`) | `drainInput` → `stop` → `dispose` | Preserve the final TUI frame before extension dispose |
| Re-entrant (`isShuttingDown` already true) | No-op | `dispose` is not called again |

```text
Signal path:     dispose  →  drainInput  →  stop
Interactive:     drainInput  →  stop  →  dispose
Re-entrant:      (no-op)
```

### Resume hint (interactive only)

On interactive quit with a **persisted** session file and a TTY stdout, the process may print:

```text
To resume this session: pi --session <session-id>
```

Signal-triggered shutdown does **not** print that resume hint.

### Extension guidance for shutdown

- Put non-TTY cleanup on `session_shutdown` so signal teardown still runs it.
- Prefer work that does not depend on a live terminal (filesystem, subprocesses, sockets).
- For an explicit quit command surface, see the `shutdown-command` example (`ctx.shutdown()` / `/quit`).
- Treat re-entrancy as safe: handlers should tolerate a single dispose path and no double-fire from re-entry.

## Example catalog (reference)

Under `packages/coding-agent/examples/extensions/`, sample extensions cover lifecycle gates, custom tools, commands/UI, git hooks, system prompt and compaction, resources, and messaging. Representative entries:

| Area | Examples |
|------|----------|
| Lifecycle & safety | `permission-gate.ts`, `project-trust.ts`, `protected-paths.ts`, `confirm-destructive.ts`, `dirty-repo-guard.ts`, `sandbox/`, `gondolin/` |
| Custom tools | `todo.ts`, `hello.ts`, `dynamic-tools.ts`, `tool-override.ts`, `structured-output.ts`, `subagent/` |
| Commands & UI | `preset.ts`, `plan-mode/`, `tools.ts`, `status-line.ts`, `doom-overlay/`, `shutdown-command.ts`, `reload-runtime.ts` |
| Git | `git-checkpoint.ts`, `auto-commit-on-exit.ts` |
| Prompt / compaction | `pirate.ts`, `claude-rules.ts`, `custom-compaction.ts`, `trigger-compact.ts` |
| Resources / messages | `dynamic-resources/`, `message-renderer.ts`, `entry-renderer.ts`, `event-bus.ts` |

Load any of them with `pi --extension <path>` for local experiments.

## Constraints and failure modes

| Topic | Behavior |
|-------|----------|
| Tool gate | `tool_call` may return `{ block: true, reason }` to stop execution |
| Active tools | Changes apply on the **next** provider request within the same agent run |
| Mid-run prompt | `before_agent_start` system prompt overrides must remain effective after tool-set changes |
| Signal cleanup | Dispose (and thus `session_shutdown`) runs before terminal drain on signal paths |
| Interactive quit | TUI stop precedes dispose; resume hint may appear for persisted sessions |
| Re-entrant shutdown | Second shutdown is a no-op; dispose is not re-invoked |
| Inline identity | Bare factories are numbered; named wrappers use `<inline:name>`; `hidden` is retained |

## Related pages

<CardGroup>
  <Card title="Tools and allowlists" href="/tools">
    Default tools, extension tools, allowlists, and blocked-tool termination.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed pi: resource loaders, custom tools, settings, and session construction.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including extensions configuration.
  </Card>
  <Card title="Extension examples" href="/extension-examples">
    Reference packages: subagent, plan-mode, doom-overlay, dynamic resources, auto-commit-on-exit.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    Runtime services, lifecycle events, and dispose without the interactive TUI.
  </Card>
  <Card title="Themes and packages" href="/themes-and-packages">
    Shareable Pi packages that bundle extensions, skills, templates, and themes.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    SIGTERM cleanup ordering, credential issues, and related operational failures.
  </Card>
</CardGroup>

---

## 15. Prompt templates

> Custom system prompts and prompt templates as reusable session configuration, with SDK entry points and related prompt tests.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/15-prompt-templates.md
- Generated: 2026-08-09T03:44:57.144Z

### Source Files

- `packages/coding-agent/examples/sdk/08-prompt-templates.ts`
- `packages/coding-agent/examples/sdk/03-custom-prompt.ts`
- `packages/coding-agent/test/suite/agent-session-prompt.test.ts`
- `packages/coding-agent/README.md`
- `packages/coding-agent/examples/sdk/README.md`

---
title: "Prompt templates"
description: "Custom system prompts and prompt templates as reusable session configuration, with SDK entry points and related prompt tests."
---

Pi exposes two related prompt surfaces in `@earendil-works/pi-coding-agent`: **prompt templates** (Markdown files expanded when the user types `/name`) and **system prompts** (the session system message built by `buildSystemPrompt`, overridable via `SYSTEM.md`, CLI flags, or `DefaultResourceLoader` hooks). Templates are session configuration resources; system prompts shape every model turn.

## Concepts

| Surface | What it is | How it is invoked | Owns the text |
|--------|------------|-------------------|---------------|
| Prompt template | Reusable user-message Markdown snippet | `/template-name [args]` in the editor, `session.prompt("/name ...")`, or RPC/print input | Template body after argument substitution |
| Skill command | On-demand skill body | `/skill:name [args]` | Skill expansion path (not template substitution) |
| Extension command | Registered command handler | `/command [args]` | Extension handler; no provider prompt by default |
| System prompt | Model system message for the session | Built at session start / tool rebuild | `buildSystemPrompt` + loader overrides |

Slash input handling in `AgentSession.prompt()` runs in this order when `expandPromptTemplates` is true (default):

1. Extension commands (`pi.registerCommand`) — executed immediately; no LLM turn.
2. Extension `input` handlers (may transform or fully handle text).
3. Skill expansion (`/skill:name`).
4. Prompt template expansion (`/name` → template body with `$1` / `$@` substitution).

Pass `{ expandPromptTemplates: false }` to skip command handling and template expansion (used for internal message paths that must send literal text).

## Prompt template format

Templates are `.md` files. The filename without extension becomes the slash command name: `review.md` → `/review`.

```markdown
---
description: Review staged git changes
argument-hint: "[focus]"
---
Review the staged changes (`git diff --cached`). Focus on:
- Bugs and logic errors
- Security issues
- Error handling gaps

Optional focus: ${1:-general quality}
```

### Frontmatter fields

| Field | Required | Behavior |
|-------|----------|----------|
| `description` | No | Autocomplete label. If omitted, the first non-empty body line is used (truncated to 60 chars + `...`). |
| `argument-hint` | No | Shown before the description in autocomplete. Convention: `<required>` and `[optional]`. Empty values are ignored. |

### Loaded shape (`PromptTemplate`)

| Field | Type | Notes |
|-------|------|-------|
| `name` | `string` | Basename of the `.md` file |
| `description` | `string` | From frontmatter or first body line |
| `argumentHint` | `string?` | From `argument-hint` |
| `content` | `string` | Markdown body after frontmatter |
| `filePath` | `string` | Absolute path (or synthetic SDK path) |
| `sourceInfo` | `SourceInfo` | Provenance (`source`, `scope`, `origin`, optional `baseDir`) |

SDK-defined templates must set `filePath` and `sourceInfo` (typically via `createSyntheticSourceInfo`).

## Argument substitution

`parseCommandArgs` splits the text after `/name` with bash-style quoting (`"` / `'`). `substituteArgs` then rewrites the template body.

| Placeholder | Meaning |
|-------------|---------|
| `$1`, `$2`, … | Positional args (1-indexed). Missing indices become `""`. `$0` is empty. |
| `$@` or `$ARGUMENTS` | All args joined with spaces |
| `${N:-default}` | Arg `N` if present and non-empty; otherwise `default` |
| `${@:-default}` / `${ARGUMENTS:-default}` | All args, or `default` when empty |
| `${@:N}` | Args from Nth position onward (1-indexed; `0` treated as `1`) |
| `${@:N:L}` | `L` args starting at N |

Rules enforced in tests:

- Substitution is **not recursive**: values that contain `$1` / `$@` / `$ARGUMENTS` stay literal.
- Default values are not expanded either.
- There is no escape syntax for `$` (a leading `\` is kept as a literal character and does not protect `$100`-style text from the `$1` match rules).
- Case-sensitive: `$arguments` is not `$ARGUMENTS`.

```text
/component Button "onClick handler"
→ content with $1 = Button, $2 = onClick handler, $@ = Button onClick handler
```

## Discovery and load order

### File locations

| Source | Path | Trust / gate |
|--------|------|--------------|
| User auto | `~/.pi/agent/prompts/*.md` | Always considered when discovery is on |
| Project auto | `.pi/prompts/*.md` | Only when the project is trusted |
| Settings | `prompts` array (files or directories) in user/project settings | Project settings require trust |
| Packages | `pi.prompts` in package manifest, or conventional `prompts/` | Via package resolution |
| CLI | `--prompt-template <path>` (repeatable) | Temporary; still available with `--no-prompt-templates` |
| SDK | `additionalPromptTemplatePaths`, `promptsOverride` | Embedder-controlled |

`prompts/` discovery is **non-recursive**. Nested templates must be listed explicitly in settings `prompts`, package manifest entries, or CLI paths.

Disable auto-discovery with `--no-prompt-templates` / `-np` (or `noPromptTemplates: true` on `DefaultResourceLoader`). Explicit CLI/SDK paths still load; when discovery is off and no explicit paths remain, the prompt set is empty.

### Name collision precedence

Resolved prompt paths are ordered by resource precedence (lower rank wins; first matching name keeps the template; losers emit a collision diagnostic):

1. Project settings entry (`local` + `project`)
2. Project auto-discovered (`.pi/prompts`)
3. User settings entry
4. User auto-discovered (`~/.pi/agent/prompts`)
5. Package resources

CLI/temporary paths are merged ahead of discovered package/user/project paths in the resource loader path list. Duplicates of the same filesystem path are canonicalized away before load.

## System prompts

### Default construction

When no custom system prompt is set, `buildSystemPrompt` builds a coding-assistant prompt that includes:

- Available tools that supply one-line `toolSnippets`
- Guidelines (tool-derived + always-on + extension/tool `promptGuidelines`)
- Pointers to pi docs/examples absolute paths
- Optional `appendSystemPrompt` section
- `<project_context>` from loaded context files
- Skills section when the `read` tool is available
- `Current working directory: …`

### Replacing or appending

| Mechanism | Effect |
|-----------|--------|
| `.pi/SYSTEM.md` (trusted project) or `~/.pi/agent/SYSTEM.md` | Replaces the default base prompt text |
| `.pi/APPEND_SYSTEM.md` or `~/.pi/agent/APPEND_SYSTEM.md` | Appended after the base/custom prompt |
| `--system-prompt <text-or-file>` | Sets loader system prompt (file path if the path exists, else literal text) |
| `--append-system-prompt <text-or-file>` | Append entries (repeatable) |
| `systemPromptOverride` / `appendSystemPromptOverride` on `DefaultResourceLoader` | Function hooks over discovered base values |

**Important:** even when the base prompt is fully replaced (`customPrompt` / `SYSTEM.md` / `systemPromptOverride`), `buildSystemPrompt` still appends project context files and skills (skills only if `read` is selected), then the cwd line. To drop discovered append files in the SDK, set `appendSystemPromptOverride: () => []`.

### Effective system prompt in sessions

`AgentSession` rebuilds the system prompt from the resource loader, active tools, tool snippets, skills, and context files. Extensions can inspect it with `ctx.getSystemPrompt()` and `ctx.getSystemPromptOptions()` (see `examples/extensions/system-prompt-header.ts` and `prompt-customizer.ts`).

## CLI surface

| Flag | Role |
|------|------|
| `--prompt-template <path>` | Load a template file or directory (repeatable) |
| `--no-prompt-templates`, `-np` | Disable discovery; explicit `--prompt-template` paths still work |
| `--system-prompt <text>` | Replace default system prompt base |
| `--append-system-prompt <text>` | Append text or file contents (repeatable) |

Interactive usage: type `/` in the editor for autocomplete of templates (source `prompt`), skills (`skill:name`), and extension commands. `/reload` reloads prompts with other resources.

## SDK entry points

Public package surface used for prompts:

- `createAgentSession`, `DefaultResourceLoader`, `getAgentDir`, `SessionManager`
- `type PromptTemplate`
- `createSyntheticSourceInfo`
- Session: `session.prompt(text, options?)`, `session.promptTemplates`

### Custom system prompt

```typescript
import {
  createAgentSession,
  DefaultResourceLoader,
  getAgentDir,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const cwd = process.cwd();
const agentDir = getAgentDir();

// Replace base prompt; clear APPEND_SYSTEM.md discovery
const loader = new DefaultResourceLoader({
  cwd,
  agentDir,
  systemPromptOverride: () =>
    `You are a helpful assistant that speaks like a pirate.\nAlways end responses with "Arrr!"`,
  appendSystemPromptOverride: () => [],
});
await loader.reload();

const { session } = await createAgentSession({
  resourceLoader: loader,
  sessionManager: SessionManager.inMemory(),
});

await session.prompt("What is 2 + 2?");
session.dispose();
```

Append-only:

```typescript
const loader = new DefaultResourceLoader({
  cwd,
  agentDir,
  appendSystemPromptOverride: (base) => [
    ...base,
    "## Additional Instructions\n- Always be concise",
  ],
});
```

### Inject or replace prompt templates

```typescript
import {
  createAgentSession,
  createSyntheticSourceInfo,
  DefaultResourceLoader,
  getAgentDir,
  type PromptTemplate,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const deployTemplate: PromptTemplate = {
  name: "deploy",
  description: "Deploy the application",
  filePath: "/virtual/prompts/deploy.md",
  sourceInfo: createSyntheticSourceInfo("/virtual/prompts/deploy.md", { source: "sdk" }),
  content: `# Deploy Instructions\n\n1. Build\n2. Test\n3. Deploy`,
};

const loader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  promptsOverride: (current) => ({
    prompts: [...current.prompts, deployTemplate],
    diagnostics: current.diagnostics,
  }),
});
await loader.reload();

const discovered = loader.getPrompts().prompts;
// session.prompt("/deploy") expands to deployTemplate.content
const { session } = await createAgentSession({
  resourceLoader: loader,
  sessionManager: SessionManager.inMemory(),
});
session.dispose();
```

Full isolation (no discovered prompts):

```typescript
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
// optionally also:
noPromptTemplates: true,
```

### Loader getters

| Method | Returns |
|--------|---------|
| `getPrompts()` | `{ prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] }` |
| `getSystemPrompt()` | Base custom system prompt string or `undefined` |
| `getAppendSystemPrompt()` | `string[]` of append sections |
| `getSystemPromptSource()` / `getAppendSystemPromptSources()` | File provenance when loaded from disk |

### Prompt options

```typescript
await session.prompt("/review src/index.ts"); // expands template

await session.prompt("/review src/index.ts", {
  expandPromptTemplates: false, // send literal slash text
});
```

## Packages and settings

Share templates by shipping a pi package:

```json
{
  "name": "my-package",
  "keywords": ["pi-package"],
  "pi": {
    "prompts": ["./prompts"]
  }
}
```

Without a `pi` manifest, packages auto-load `prompts/*.md`. Settings:

```json
{
  "prompts": ["./prompts", "~/shared/review.md"],
  "packages": [
    {
      "source": "npm:my-package",
      "prompts": ["prompts/review.md"]
    }
  ]
}
```

Paths in `~/.pi/agent/settings.json` resolve relative to `~/.pi/agent`; paths in `.pi/settings.json` resolve relative to `.pi`.

## Runtime expansion behavior

Verified session behavior (`test/suite/agent-session-prompt.test.ts`):

- `session.prompt("/review src/index.ts")` with a template named `review` and body `Review this code: $1` yields user text `Review this code: src/index.ts`.
- `/skill:name` expands skill markup before the provider sees the message.
- Extension `/commands` run without consuming a provider response.
- Images and multi-tool turns are independent of template expansion; expansion only rewrites the text string before queue/send.

RPC mode exposes templates in `get_commands` with `source: "prompt"`.

## Verification

| Check | Signal |
|-------|--------|
| Template discovered | Startup resource listing / `loader.getPrompts().prompts` contains `name` |
| Expansion works | `session.prompt("/name arg")` stores substituted body as the user message |
| Collision | `getPrompts().diagnostics` includes `type: "collision"` for the losing path |
| No discovery | `--no-prompt-templates` yields empty prompts unless CLI/SDK paths override |
| System replace | Model system message starts with custom text; context/skills may still append |
| Append cleared | `appendSystemPromptOverride: () => []` avoids `APPEND_SYSTEM.md` |

Unit coverage lives in `test/prompt-templates.test.ts` (parse, substitute, defaults, slices, argument-hint, expand) and `test/system-prompt.test.ts` (tools, guidelines, docs path lines). Run those suites when changing prompt behavior; package policy is to avoid full e2e unless requested.

## Constraints and failure modes

| Case | Behavior |
|------|----------|
| Unknown `/name` | Left as literal text (not an error) |
| Unreadable template file | Skipped at load (`null`) |
| Missing CLI template path | Diagnostic: path does not exist; other templates still load |
| Compaction in progress | `prompt()` throws until compaction finishes |
| Streaming without `streamingBehavior` | Throws; after expansion, use `steer` or `followUp` |
| Untrusted project | Project `.pi/prompts`, project `SYSTEM.md` / `APPEND_SYSTEM.md`, and project settings paths are not loaded |
| Provider neutrality | Templates and system prompts are local files/strings; no fixed model provider is required |

## Related pages

<CardGroup>
  <Card title="Skills" href="/skills">
    `/skill:name` expansion, frontmatter rules, and collision precedence for skills.
  </Card>
  <Card title="Context files" href="/context-files">
    Project context injection into the system prompt after custom or default bases.
  </Card>
  <Card title="SDK" href="/sdk">
    `createAgentSession`, `DefaultResourceLoader`, and full control hooks.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including custom prompts and prompt templates.
  </Card>
  <Card title="Themes and packages" href="/themes-and-packages">
    Packaging prompts with extensions, skills, and themes.
  </Card>
  <Card title="Agent sessions" href="/agent-sessions">
    Prompt queue, concurrent behavior, and turn lifecycle around `session.prompt()`.
  </Card>
</CardGroup>

---

## 16. Themes and Pi packages

> Theme export requirements, package manifest validation, and how shareable Pi packages bundle extensions, skills, templates, and themes.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/16-themes-and-pi-packages.md
- Generated: 2026-08-09T03:44:28.189Z

### Source Files

- `packages/coding-agent/README.md`
- `packages/coding-agent/package.json`
- `packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts`
- `packages/coding-agent/test/suite/regressions/7187-malformed-package-manifest.test.ts`
- `packages/coding-agent/examples/extensions/README.md`

---
title: "Themes and Pi packages"
description: "Theme export requirements, package manifest validation, and how shareable Pi packages bundle extensions, skills, templates, and themes."
---

Pi themes are JSON color packs for the interactive TUI and HTML session export. Pi packages are installable bundles that declare extensions, skills, prompt templates, and themes under a `package.json` `pi` manifest (or conventional directories), managed with `pi install` / `pi remove` / `pi update` / `pi list` / `pi config` and persisted in `settings.packages`.

## Themes

### Load locations

Pi discovers themes from these sources (later discovery still dedupes by theme `name`; first name wins within a load pass):

| Source | Path / mechanism |
|--------|------------------|
| Built-in | `dark`, `light` (shipped theme JSON) |
| Global | `~/.pi/agent/themes/*.json` |
| Project | `.pi/themes/*.json` (after project trust) |
| Settings | `themes` array (files or directories) |
| Packages | `themes/` or `pi.themes` from installed packages |
| CLI | `--theme <path>` (repeatable) |

Disable discovery with `--no-themes`.

Select the active theme in `/settings` or `settings.json`:

```json
{
  "theme": "my-theme"
}
```

On first run, pi probes the terminal background and defaults to `dark` or `light`. Auto light/dark selection uses a slash form in settings (`lightTheme/darkTheme`); theme **file** `name` values must not contain `/` because that character is reserved for that setting.

Active custom theme files hot-reload when edited on disk.

### Theme JSON shape

```json
{
  "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
  "name": "my-theme",
  "vars": {
    "primary": "#00aaff",
    "secondary": 242
  },
  "colors": {
    "accent": "primary"
  },
  "export": {
    "pageBg": "#18181e",
    "cardBg": "#1e1e24",
    "infoBg": "#3c3728"
  }
}
```

| Field | Required | Rules |
|-------|----------|--------|
| `name` | Yes | Unique display/id name; must not contain `/` |
| `colors` | Yes | All required color tokens (51 required; see schema) |
| `vars` | No | Reusable palette; colors may reference var names |
| `export` | No | Optional HTML export backgrounds |
| `$schema` | No | Editor validation only |

**Color value formats:** `#RRGGBB` hex, 256-color integer `0–255`, var name string, or `""` for terminal default.

**Optional color tokens with fallbacks:**

| Token | Fallback when omitted |
|-------|------------------------|
| `thinkingMax` | `thinkingXhigh` |
| `scrollbarThumb` | `selectedBg` |

Invalid JSON or missing required tokens throw load errors that list missing color keys and point at the built-in themes as references. Prefer the schema at `src/modes/interactive/theme/theme-schema.json` for the full required token set.

### HTML export colors

Session HTML export (`/export`, `AgentSession.exportToHtml`, RPC export) resolves CSS from the active theme:

1. Prefer explicit `export.pageBg`, `export.cardBg`, `export.infoBg` from the theme file.
2. Those values resolve through the same `vars` references and 256→hex conversion as TUI colors.
3. If `export` is omitted or a field is unset/empty, export derives `pageBg` / `cardBg` / `infoBg` from `userMessageBg` (with fixed fallbacks if that color cannot be parsed).

Built-in `dark` / `light` themes ship explicit `export` blocks (for example dark uses `#18181e` / `#1e1e24` / `#3c3728`).

### Missing configured theme during export

If `settings.theme` names a theme that is not loadable, export does **not** fail. `exportToHtml` only passes a theme name when `getThemeByName` succeeds; otherwise it exports with the active runtime theme (fallback), while leaving the configured setting value unchanged.

```text
settings.theme = "missing-theme"
        │
        ▼
getThemeByName("missing-theme") ──fail──► themeName = undefined
        │
        ▼
exportSessionToHtml(... active Theme object ...)
```

### CLI theme flags

| Flag | Effect |
|------|--------|
| `--theme <path>` | Load additional theme path (repeatable) |
| `--no-themes` | Disable theme discovery |

---

## Pi packages

### What a package is

A Pi package is a directory (npm, git clone, or local path) that contributes any of:

| Resource | Conventional dir | Manifest key | File pattern |
|----------|------------------|--------------|--------------|
| Extensions | `extensions/` | `pi.extensions` | `.ts`, `.js` |
| Skills | `skills/` | `pi.skills` | `SKILL.md` trees / `.md` skills |
| Prompt templates | `prompts/` | `pi.prompts` | `.md` |
| Themes | `themes/` | `pi.themes` | `.json` |

Packages are **provider-neutral** portable skill/extension catalogs: they do not require a hosted connector or a specific model vendor. They run with full local privileges—review third-party source before install.

### Package manager commands

```bash
pi install npm:@foo/bar@1.0.0
pi install git:github.com/user/repo@v1
pi install https://github.com/user/repo
pi install /absolute/path/to/package
pi install ./relative/path/to/package

pi remove npm:@foo/bar
pi list
pi update                 # update pi only
pi update --all           # pi + packages
pi update --extensions    # packages only
pi update npm:@foo/bar    # one package
pi config                 # enable/disable resources
```

| Option | Meaning |
|--------|---------|
| `-l` / `--local` | Write project settings (`.pi/settings.json`) and install under project tree |
| `--approve` / `--no-approve` | Trust or ignore project-local settings for this command |
| `-e` / `--extension <source>` | Ephemeral install for current run only (temp dir) |

Default install target is user settings (`~/.pi/agent/settings.json`). Project packages install after project trust; missing project packages can be installed on startup when trusted.

### Install locations

| Source | User scope | Project scope |
|--------|------------|---------------|
| npm | `~/.pi/agent/npm/node_modules/<name>` | `.pi/npm/node_modules/<name>` |
| git | `~/.pi/agent/git/<host>/<path>` | `.pi/git/<host>/<path>` |
| local | Path as configured (not copied) | Path as configured (not copied) |

Temporary (`-e`) installs use agent temp extension folders under the agent dir.

Offline: `--offline` or `PI_OFFLINE=1` disables startup network package update checks (and related install/update telemetry).

### Package sources

| Kind | Spec examples | Notes |
|------|---------------|-------|
| npm | `npm:pkg`, `npm:@scope/pkg@1.2.3` | Exact versions are pinned and skipped by bulk package updates |
| git | `git:github.com/user/repo@v1`, `ssh://…`, `https://…` | Refs are pinned; update reconciles clone to configured ref |
| local | `/abs/path`, `./rel/path` | File → single extension; directory → package rules |

`npmCommand` in settings pins the npm argv used for lookup/install (for example `["mise", "exec", "node@20", "--", "npm"]`).

---

## Package manifest validation

### `package.json` `pi` key

```json
{
  "name": "my-package",
  "keywords": ["pi-package"],
  "pi": {
    "extensions": ["./extensions"],
    "skills": ["./skills"],
    "prompts": ["./prompts"],
    "themes": ["./themes"]
  }
}
```

Paths are relative to the package root. Entries may use globs (`*`, `?`) and override patterns (`!`, `+`, `-`). Tag packages with the `pi-package` keyword for gallery discoverability. Optional gallery fields on `pi`: `video` (MP4), `image` (PNG/JPEG/GIF/WebP).

### Malformed field handling

`readPiManifest` only accepts resource fields that are **arrays of strings**. Invalid shapes are ignored field-by-field; valid sibling fields still load.

```text
pi: {
  skills: "./skills",          ← ignored (string, not string[])
  prompts: ["./prompts"]       ← kept
}
→ skills not loaded; prompts load
```

If `package.json` is missing, unreadable, or has no object `pi`, the manager falls back to conventional directories (`extensions/`, `skills/`, `prompts/`, `themes/`) when present.

### Manifest vs convention discovery

```text
package root
    │
    ├─ package.json with valid pi.* array(s)
    │      → expand globs, apply !/+/- overrides, collect files
    │
    └─ no pi manifest
           → load each existing conventional resource directory
```

Extensions also support smart discovery: `pi.extensions` paths, or `index.ts` / `index.js` in a directory, or flat `.ts`/`.js` files under `extensions/`.

---

## Package filtering in settings

`settings.packages` entries may be a source string or a filter object:

```json
{
  "packages": [
    "npm:simple-pkg",
    {
      "source": "npm:my-package",
      "extensions": ["extensions/*.ts", "!extensions/legacy.ts"],
      "skills": [],
      "prompts": ["prompts/review.md"],
      "themes": ["+themes/legacy.json"]
    }
  ]
}
```

| Filter form | Effect |
|-------------|--------|
| Omit key | Load all resources of that type allowed by the package |
| `[]` | Disable all of that type (still tracked as disabled) |
| globs / `!pattern` | Include / exclude |
| `+path` | Force-include exact path relative to package root |
| `-path` | Force-exclude exact path |
| `autoload: false` | Delta mode: start empty (or apply as delta over a global copy of the same package) and only apply listed patterns |

User filters **layer on** the manifest; they narrow already-allowed files rather than replacing the package definition.

### Pattern evaluation order

For normal filters:

1. Includes (or all files if no plain includes)
2. `!` excludes
3. `+` force-includes
4. `-` force-excludes

---

## Dependencies inside packages

| Dependency kind | Placement |
|-----------------|-----------|
| Runtime third-party libs | `dependencies` (installed via `npm install` on package install) |
| Core pi packages used only as imports | `peerDependencies` with `"*"`: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `typebox` — do not bundle them |
| Other pi packages whose resources you re-export | `dependencies` + `bundledDependencies`, then list `node_modules/...` paths in the `pi` manifest |

Packages load with separate module roots so independent installs do not share Node module identity.

---

## Resource precedence and enablement

When the same resource name collides, load order uses precedence (highest first):

1. Project settings entry
2. Project auto-discovered
3. User settings entry
4. User auto-discovered
5. Package resource

Use `pi config` (global by default; Tab or `-l` for project) to enable/disable package and local resources without uninstalling. `/reload` reloads themes along with extensions, skills, prompts, and context files.

Project vs user package identity:

| Type | Identity key |
|------|----------------|
| npm | package name |
| git | repository URL without ref |
| local | resolved absolute path |

Project entry wins over global for the same identity unless the project entry sets `autoload: false` (then it applies as a delta over global).

---

## Author checklist

<Steps>
  <Step title="Add a theme">
    Create `~/.pi/agent/themes/my-theme.json` (or package `themes/`) with required `name` + `colors`. Optionally set `export` for HTML. Select via `/settings` or `"theme": "my-theme"`.
  </Step>
  <Step title="Bundle a package">
    Add `keywords: ["pi-package"]` and a `pi` object (or conventional dirs). Keep core pi packages as peer deps; put runtime deps under `dependencies`.
  </Step>
  <Step title="Install and verify">
    `pi install ./my-package` or `npm:…` / `git:…`, then `pi list` and `pi config` to confirm resources. In interactive mode, open `/settings` for themes and use `/reload` after edits.
  </Step>
  <Step title="Export with theme colors">
    Run `/export` or `exportToHtml`. Prefer explicit `export` fields; if the configured theme is missing, export still succeeds with the active fallback theme.
  </Step>
</Steps>

---

## Failure modes

| Symptom | Cause / resolution |
|---------|-------------------|
| Theme load error listing missing tokens | Incomplete `colors`; compare against built-in themes / schema |
| `Invalid theme name … cannot contain "/"` | Use slash only in settings auto-theme form, not in JSON `name` |
| Package skills missing, prompts present | Manifest field not a string array (malformed field dropped) |
| Package does nothing | No valid `pi` arrays and no conventional dirs; check paths |
| Project packages not loading | Project not trusted; use trust flow or `--approve` on package commands |
| Pinned npm/git not moving on `pi update --extensions` | Expected for exact versions / pinned refs; reinstall with new version/ref |
| Third-party package security | Extensions and skills run with full system access—review before install |

---

## Related pages

<CardGroup>
  <Card title="Skills" href="/skills">
    SKILL.md rules, naming, and collision precedence for skill resources packages can ship.
  </Card>
  <Card title="Extensions" href="/extensions">
    TypeScript extension registration and lifecycle used by package-delivered extensions.
  </Card>
  <Card title="Prompt templates" href="/prompt-templates">
    Prompt template discovery and session wiring for package `prompts/` trees.
  </Card>
  <Card title="Settings" href="/settings">
    `packages`, `themes`, and related settings load/merge behavior.
  </Card>
  <Card title="Extension examples" href="/extension-examples">
    Reference extension packages and local extension layouts.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Runtime failures that can interact with installs, reloads, and session export.
  </Card>
</CardGroup>

---

## 17. SDK

> Embed pi with the package main export: minimal agent construction, full control hooks, custom models, tools, and settings.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/17-sdk.md
- Generated: 2026-08-09T03:42:34.400Z

### Source Files

- `packages/coding-agent/examples/sdk/README.md`
- `packages/coding-agent/examples/sdk/01-minimal.ts`
- `packages/coding-agent/examples/sdk/12-full-control.ts`
- `packages/coding-agent/package.json`
- `packages/coding-agent/src/core/agent-session.ts`
- `packages/coding-agent/examples/sdk/02-custom-model.ts`

---
title: "SDK"
description: "Embed pi with the package main export: minimal agent construction, full control hooks, custom models, tools, and settings."
---

The `@earendil-works/pi-coding-agent` main export is the embed surface for programmatic agents. Call `createAgentSession()` to construct an `AgentSession`, then drive turns with `session.prompt()`, observe progress through `session.subscribe()`, and release resources with `session.dispose()`. The same session abstraction powers interactive, print, and RPC run modes; SDK mode is the library path that wires model runtime, tools, settings, resources, and persistence without the TUI.

## Package surface

| Export | Path / field | Role |
|--------|----------------|------|
| Main | `@earendil-works/pi-coding-agent` (`exports["."]`, `main`: `./dist/index.js`) | SDK entry: `createAgentSession`, `ModelRuntime`, loaders, managers |
| RPC | `@earendil-works/pi-coding-agent/rpc-entry` | Process JSON RPC integration (separate from in-process SDK) |
| Client | `@earendil-works/pi-coding-agent/client` | Client package surface |
| Bin | `pi` → `dist/cli.js` | CLI entry (not required for SDK embeds) |
| Config dir | `piConfig.configDir`: `.pi` | Package-declared config directory name |
| Engine | `node` `>=22.19.0` | Runtime requirement |

Primary construction APIs used by the SDK examples:

| Symbol | Purpose |
|--------|---------|
| `createAgentSession` | Build a session with optional overrides |
| `createAgentSessionRuntime` | Runtime-backed session replacement (see session runtime docs) |
| `ModelRuntime` | Auth + models (`create`, `getModel`, `getAvailable`, `setRuntimeApiKey`) |
| `DefaultResourceLoader` | Default discovery for extensions, skills, prompts, themes, context files |
| `SessionManager` | Session persistence (`create`, `inMemory`) |
| `SettingsManager` | Settings (`create`, `inMemory`) |
| `createExtensionRuntime` | Empty extension runtime for custom loaders |

Model helpers may also come from `@earendil-works/pi-ai` or `@earendil-works/pi-ai/compat` (`getModel`).

## Minimal construction

With no options, `createAgentSession()` uses discovery defaults: skills, extensions, tools, and context files from the current working directory and `~/.pi/agent`. The model is taken from settings or the first available model with valid credentials.

```typescript
import { createAgentSession } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession();

try {
  session.subscribe((event) => {
    if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
      process.stdout.write(event.assistantMessageEvent.delta);
    }
  });

  await session.prompt("What files are in the current directory?");
  session.state.messages.forEach((msg) => {
    console.log(msg);
  });
} finally {
  session.dispose();
}
```

Run the package example with:

```bash
cd packages/coding-agent
npx tsx examples/sdk/01-minimal.ts
```

## `createAgentSession` options

| Option | Default | Description |
|--------|---------|-------------|
| `modelRuntime` | Runtime using `agentDir/auth.json` and `models.json` | Canonical model and authentication runtime |
| `cwd` | `process.cwd()` | Working directory |
| `agentDir` | `~/.pi/agent` | Config directory |
| `model` | From settings / first available | Model instance to use |
| `thinkingLevel` | From settings / `"off"` | `off`, `low`, `medium`, `high` |
| `tools` | `["read", "bash", "edit", "write"]` built-ins | Allowlist across built-in, extension, and custom tool names |
| `customTools` | `[]` | Additional tool definitions |
| `resourceLoader` | `DefaultResourceLoader` | Extensions, skills, prompts, themes, context files |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
| `settingsManager` | `SettingsManager.create(cwd, agentDir)` | Settings (compaction, retry, and related options) |

Return shape used in examples: `{ session }` where `session` is an `AgentSession`.

### Common option patterns

```typescript
import { getModel } from "@earendil-works/pi-ai";
import {
  createAgentSession,
  DefaultResourceLoader,
  ModelRuntime,
  SessionManager,
  SettingsManager,
} from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();

// Explicit runtime only
const { session } = await createAgentSession({ modelRuntime });

// Model + thinking level
const model = getModel("anthropic", "claude-opus-4-5");
const { session: s2 } = await createAgentSession({
  model,
  thinkingLevel: "high",
  modelRuntime,
});

// Prompt override via DefaultResourceLoader
const loader = new DefaultResourceLoader({
  systemPromptOverride: (base) => `${base}\n\nBe concise.`,
});
await loader.reload();
const { session: s3 } = await createAgentSession({ resourceLoader: loader, modelRuntime });

// Built-in tool allowlist (read-oriented)
const { session: s4 } = await createAgentSession({
  tools: ["read", "grep", "find", "ls"],
  modelRuntime,
});

// Ephemeral session
const { session: s5 } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime,
});
```

## Model selection

`ModelRuntime` is the SDK-facing registry for built-in models, custom entries from `models.json`, availability (valid API keys), and runtime API keys.

```typescript
import { createAgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();

// Built-in by provider / id
const opus = modelRuntime.getModel("anthropic", "claude-opus-4-5");

// Custom provider/model from models.json
const customModel = modelRuntime.getModel("my-provider", "my-model");

// Models with valid API keys
const available = await modelRuntime.getAvailable();

if (available.length > 0) {
  const { session } = await createAgentSession({
    model: available[0],
    thinkingLevel: "medium", // off | low | medium | high
    modelRuntime,
  });
  try {
    await session.prompt("Say hello in one sentence.");
  } finally {
    session.dispose();
  }
}
```

Custom auth and models paths:

```typescript
const customRuntime = await ModelRuntime.create({
  authPath: "/my/app/auth.json",
  modelsPath: "/my/app/models.json",
});
await customRuntime.setRuntimeApiKey("anthropic", process.env.MY_KEY!);
```

<Note>
Examples use concrete provider/model ids (for example `anthropic` / `claude-opus-4-5`) as illustrations. Any provider/model pair available through `ModelRuntime` or `getModel` can be passed; credentials and availability determine what actually runs.
</Note>

## Full control (no discovery)

Replace discovery with explicit loaders, settings, tools, and in-memory session state.

```typescript
import { getModel } from "@earendil-works/pi-ai/compat";
import {
  createAgentSession,
  createExtensionRuntime,
  ModelRuntime,
  type ResourceLoader,
  SessionManager,
  SettingsManager,
} from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create({
  authPath: "/tmp/my-agent/auth.json",
  modelsPath: "/tmp/my-agent/models.json",
});
if (process.env.MY_ANTHROPIC_KEY) {
  await modelRuntime.setRuntimeApiKey("anthropic", process.env.MY_ANTHROPIC_KEY);
}

const model = getModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Model not found");

const settingsManager = SettingsManager.inMemory({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 2 },
});

const cwd = process.cwd();

const resourceLoader: ResourceLoader = {
  getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
  getSkills: () => ({ skills: [], diagnostics: [] }),
  getPrompts: () => ({ prompts: [], diagnostics: [] }),
  getThemes: () => ({ themes: [], diagnostics: [] }),
  getAgentsFiles: () => ({ agentsFiles: [] }),
  getSystemPrompt: () => `You are a minimal assistant.
Available: read, bash. Be concise.`,
  getSystemPromptSource: () => undefined,
  getAppendSystemPrompt: () => [],
  getAppendSystemPromptSources: () => [],
  extendResources: () => {},
  reload: async () => {},
};

const { session } = await createAgentSession({
  cwd,
  agentDir: "/tmp/my-agent",
  model,
  thinkingLevel: "off",
  modelRuntime,
  resourceLoader,
  tools: ["read", "bash"],
  sessionManager: SessionManager.inMemory(cwd),
  settingsManager,
});
```

Equivalent full-control shape with `DefaultResourceLoader` overrides (from the SDK examples README):

```typescript
const resourceLoader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are helpful.",
  extensionFactories: [myExtension],
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
  agentsFilesOverride: () => ({ agentsFiles: [] }),
  promptsOverride: () => ({ prompts: [], diagnostics: [] }),
});
await resourceLoader.reload();

const { session } = await createAgentSession({
  model,
  modelRuntime: customRuntime,
  resourceLoader,
  tools: ["read", "bash", "my_tool"],
  customTools: [myTool],
  sessionManager: SessionManager.inMemory(),
  settingsManager: SettingsManager.inMemory(),
});
```

### `ResourceLoader` methods used for full control

| Method | Role in full-control example |
|--------|------------------------------|
| `getExtensions` | Return extensions list, errors, and runtime |
| `getSkills` / `getPrompts` / `getThemes` | Resource lists + diagnostics |
| `getAgentsFiles` | Agent context files |
| `getSystemPrompt` | Full system prompt string |
| `getSystemPromptSource` / `getAppendSystemPrompt` / `getAppendSystemPromptSources` | Prompt provenance and append list |
| `extendResources` | Mutation hook (noop in minimal loader) |
| `reload` | Async refresh |

## Session API surface used by embeds

| Member | Usage |
|--------|--------|
| `session.subscribe(listener)` | Register for `AgentSessionEvent` stream |
| `session.prompt(text)` | Queue/run a user turn (async) |
| `session.state.messages` | Read message history after a turn |
| `session.dispose()` | Tear down; always call in `finally` |

### Event subscription

Core event types documented in the SDK examples:

```typescript
session.subscribe((event) => {
  switch (event.type) {
    case "message_update":
      if (event.assistantMessageEvent.type === "text_delta") {
        process.stdout.write(event.assistantMessageEvent.delta);
      }
      break;
    case "tool_execution_start":
      console.log(`Tool: ${event.toolName}`);
      break;
    case "tool_execution_end":
      console.log(`Result: ${event.result}`);
      break;
    case "agent_end":
      console.log("Done");
      break;
  }
});
await session.prompt("Hello");
```

`AgentSession` also emits session-specific events beyond core agent events, including:

| `event.type` | Notable fields |
|--------------|----------------|
| `agent_end` | `messages`, `willRetry` |
| `agent_settled` | — |
| `queue_update` | `steering`, `followUp` |
| `compaction_start` / `compaction_end` | `reason`: `manual` \| `threshold` \| `overflow`; end adds `result`, `aborted`, `willRetry`, optional `errorMessage` |
| `entry_appended` | `entry` |
| `session_info_changed` | `name` |
| `thinking_level_changed` | `level` |
| `auto_retry_start` / `auto_retry_end` | attempt metadata and errors |
| `summarization_retry_scheduled` / `summarization_retry_attempt_start` / `summarization_retry_finished` | Compaction / branch-summary retries |
| `bash_execution_update` | optional `id`, `delta` |

Streaming assistant text uses nested `assistantMessageEvent.type === "text_delta"` with `delta`.

## What `AgentSession` owns

`AgentSession` is the shared core across interactive, print, and RPC modes. Modes add their own I/O; the session encapsulates:

- Agent state access
- Event subscription with automatic session persistence
- Model and thinking-level management
- Compaction (manual and auto)
- Bash execution
- Session switching and branching

Embedders therefore get the same lifecycle semantics as CLI modes, without the interactive UI layer.

```text
  createAgentSession(options)
            │
            ▼
  ┌─────────────────────┐     subscribe()      ┌──────────────────┐
  │    AgentSession     │ ───────────────────► │  Event consumer  │
  │  (shared core)      │                      └──────────────────┘
  │                     │     prompt()
  │  modelRuntime       │ ◄──────────────────  host application
  │  resourceLoader     │
  │  sessionManager     │     dispose()
  │  settingsManager    │ ◄──────────────────  finally / shutdown
  │  tools / customTools│
  └─────────────────────┘
```

## Example catalog

Package path: `packages/coding-agent/examples/sdk/`.

| File | Focus |
|------|--------|
| `01-minimal.ts` | Defaults only |
| `02-custom-model.ts` | Model + thinking level |
| `03-custom-prompt.ts` | System prompt replace/modify |
| `04-skills.ts` | Discover, filter, or replace skills |
| `05-tools.ts` | Built-in tool allowlists |
| `06-extensions.ts` | Logging, blocking, result modification |
| `07-context-files.ts` | AGENTS.md context files |
| `08-slash-commands.ts` | File-based slash commands |
| `09-api-keys-and-oauth.ts` | API key resolution, OAuth config |
| `10-settings.ts` | Compaction, retry, terminal settings |
| `11-sessions.ts` | In-memory, persistent, continue, list |
| `12-full-control.ts` | Replace everything, no discovery |
| `13-session-runtime.ts` | Runtime-backed session replacement |

The runtime example builds a recreate function that closes over process-global fixed inputs and recreates cwd-bound services and sessions as the active session cwd changes. Prefer `createAgentSessionRuntime` when the host needs that lifecycle; use plain `createAgentSession` for single-session embeds.

## Constraints and verification

| Constraint | Detail |
|------------|--------|
| Node version | `>=22.19.0` |
| Defaults discovery roots | `cwd` and `~/.pi/agent` when using default loaders |
| Default tools | `read`, `bash`, `edit`, `write` unless `tools` overrides |
| Thinking levels | `off`, `low`, `medium`, `high` |
| Disposal | Call `session.dispose()` in `finally` after construction |
| Model availability | Prefer `modelRuntime.getAvailable()` or explicit keys via `setRuntimeApiKey` |
| Full control | Supply `resourceLoader` (and usually in-memory managers) to avoid filesystem discovery |

**Verification signals**

- `createAgentSession()` resolves without throw
- `message_update` / `text_delta` events stream during `prompt`
- `agent_end` (and later `agent_settled` when applicable) after the turn
- `session.state.messages` non-empty after a successful prompt
- Full-control path: only allowlisted tools appear in tool events

## Related pages

<CardGroup cols={2}>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes for models, skills, tools, extensions, sessions, and full control.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    `AgentSessionRuntime`, settled events, and embedding without the interactive TUI.
  </Card>
  <Card title="Package exports" href="/package-exports">
    Main, rpc-entry, client exports, bin name, and `piConfig.configDir`.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK modes: when to choose each.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default tools, extension tools, allowlist filters, and blocked-tool behavior.
  </Card>
  <Card title="Settings" href="/settings">
    Settings load/reload and merge rules for compaction, retry, and session options.
  </Card>
  <Card title="Providers and models" href="/providers-and-models">
    Built-in and dynamic providers, model order, and `models.json` behavior.
  </Card>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth, credential storage, and ambient auth.
  </Card>
</CardGroup>

---

## 18. Session runtime

> AgentSessionRuntime services, lifecycle events (settled, start notify, event bus), and embedding sessions without the interactive TUI.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/18-session-runtime.md
- Generated: 2026-08-09T03:45:35.139Z

### Source Files

- `packages/coding-agent/src/core/agent-session-runtime.ts`
- `packages/coding-agent/src/core/agent-session-services.ts`
- `packages/coding-agent/examples/sdk/13-session-runtime.ts`
- `packages/coding-agent/test/suite/agent-session-runtime.test.ts`
- `packages/coding-agent/test/agent-session-runtime-events.test.ts`
- `packages/coding-agent/test/suite/regressions/6363-agent-settled-event.test.ts`

---
title: "Session runtime"
description: "AgentSessionRuntime services, lifecycle events (settled, start notify, event bus), and embedding sessions without the interactive TUI."
---

`AgentSessionRuntime` owns the active `AgentSession` plus its cwd-bound services. Session replacement (`newSession`, `switchSession`, `fork`, `importFromJsonl`) lives on the runtime, not on `AgentSession`. Interactive, print/JSON, and RPC modes all build through `createAgentSessionRuntime`; embedders use the same factories without the TUI.

## Layers

| Layer | Role |
| --- | --- |
| `AgentSessionServices` | Cwd-bound infrastructure: `cwd`, `agentDir`, `modelRuntime`, `settingsManager`, `resourceLoader`, `diagnostics` |
| `AgentSession` | One conversation: prompt queue, tools, compaction, tree navigation, `subscribe` events |
| `AgentSessionRuntime` | Current session + services; tears down and recreates both on replace |

```mermaid
flowchart TB
  subgraph Host["Host / mode / SDK"]
    Factory["CreateAgentSessionRuntimeFactory"]
    Rebind["setRebindSession / bindExtensions / subscribe"]
  end

  subgraph Runtime["AgentSessionRuntime"]
    Sess["session: AgentSession"]
    Svc["services: AgentSessionServices"]
    Diag["diagnostics"]
  end

  subgraph Services["AgentSessionServices"]
    MR["modelRuntime"]
    SM["settingsManager"]
    RL["resourceLoader"]
  end

  Factory -->|"createRuntime(cwd, sessionManager, …)"| Runtime
  Svc --> Services
  Sess -->|"subscribe(AgentSessionEvent)"| Rebind
  Runtime -->|"new / resume / fork / import"| Factory
  Rebind -->|"after replace"| Sess
```

Services are **recreated** when the effective session cwd changes. CLI resource paths should be absolute before they reach service creation so later switches do not reinterpret them.

## Factories

### `createAgentSessionServices`

Builds infrastructure only (no `AgentSession`):

```typescript
const services = await createAgentSessionServices({
  cwd,
  agentDir,                 // optional; default agent dir
  settingsManager,          // optional
  modelRuntime,             // optional; else auth.json + models.json under agentDir
  modelRuntimeSignal,       // optional AbortSignal
  extensionFlagValues,      // Map of --flag values for extensions
  resourceLoaderOptions,    // extensions, skills, themes, context files, …
  resourceLoaderReloadOptions,
});
```

Side effects during creation:

- Loads extensions via `DefaultResourceLoader.reload`
- Registers pending providers / native providers on `modelRuntime`
- Refreshes model runtime with `{ allowNetwork: false }`
- Applies extension flag values; unknown or mistyped flags become diagnostics

### `createAgentSessionFromServices`

Builds an `AgentSession` after model, thinking, and tool options are resolved against those services:

```typescript
const created = await createAgentSessionFromServices({
  services,
  sessionManager,
  sessionStartEvent,  // optional; default { type: "session_start", reason: "startup" }
  model,
  thinkingLevel,
  scopedModels,
  tools,
  excludeTools,
  noTools,
  customTools,
});
```

### `createAgentSessionRuntime`

Stores a `CreateAgentSessionRuntimeFactory` and runs it for the initial target, then reuses it for every replacement:

```typescript
type CreateAgentSessionRuntimeFactory = (options: {
  cwd: string;
  agentDir: string;
  sessionManager: SessionManager;
  sessionStartEvent?: SessionStartEvent;
  projectTrustContext?: ProjectTrustContext;
}) => Promise<CreateAgentSessionRuntimeResult>;
```

`CreateAgentSessionRuntimeResult` extends `CreateAgentSessionResult` with `services` and `diagnostics`.

Minimal embed pattern (`examples/sdk/13-session-runtime.ts`):

```typescript
import {
  type CreateAgentSessionRuntimeFactory,
  createAgentSessionFromServices,
  createAgentSessionRuntime,
  createAgentSessionServices,
  getAgentDir,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const createRuntime: CreateAgentSessionRuntimeFactory = async ({
  cwd,
  sessionManager,
  sessionStartEvent,
}) => {
  const services = await createAgentSessionServices({ cwd });
  return {
    ...(await createAgentSessionFromServices({
      services,
      sessionManager,
      sessionStartEvent,
    })),
    services,
    diagnostics: services.diagnostics,
  };
};

const runtime = await createAgentSessionRuntime(createRuntime, {
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  sessionManager: SessionManager.create(process.cwd()),
});
```

For a single fixed session without replace, `createAgentSession()` is enough. Prefer the runtime when the host must support new / resume / fork / import.

## Runtime API

| Member | Description |
| --- | --- |
| `session` | Active `AgentSession` (identity changes after replace) |
| `services` | Current `AgentSessionServices` |
| `cwd` | `services.cwd` |
| `diagnostics` | Last creation diagnostics (`info` \| `warning` \| `error`) |
| `modelFallbackMessage` | Optional restore warning from session creation |
| `setRebindSession(fn)` | Async hook after a successful replace; re-attach host bindings |
| `setBeforeSessionInvalidate(fn)` | Sync hook after `session_shutdown`, before dispose; for non-yielding host teardown |
| `newSession(options?)` | New empty session (persisted or in-memory matching current manager) |
| `switchSession(path, options?)` | Open/resume another session JSONL |
| `fork(entryId, options?)` | Branch; `position: "before"` (default) or `"at"` (clone leaf) |
| `importFromJsonl(path, cwdOverride?)` | Copy import into session dir and resume |
| `dispose()` | `session_shutdown` reason `"quit"`, then dispose session |

Replacement methods return `{ cancelled: boolean, … }`. Cancellation from `session_before_switch` / `session_before_fork` leaves the current session intact.

### Teardown order

For every replace:

1. `session_before_switch` or `session_before_fork` (cancellable)
2. `session.abort()` then wait until idle — persists aborted tool results on the outgoing session
3. `session_shutdown` with reason `new` \| `resume` \| `fork`
4. `beforeSessionInvalidate` (sync)
5. `session.dispose()`
6. `createRuntime(...)` for the next target
7. `rebindSession` then optional `withSession(ctx)`

`dispose()` on the runtime uses shutdown reason `"quit"` (no abort of an in-flight run first).

### Host rebind

After replace, `runtime.session` is a new instance. Re-subscribe and re-`bindExtensions`:

```typescript
let unsubscribe: (() => void) | undefined;

async function bindSession() {
  unsubscribe?.();
  const session = runtime.session;
  await session.bindExtensions({
    mode: "rpc", // or "print" / "json" / interactive UI context
    commandContextActions: {
      waitForIdle: () => session.waitForIdle(),
      newSession: (opts) => runtime.newSession(opts),
      fork: (entryId, opts) => runtime.fork(entryId, opts),
      switchSession: (path, opts) => runtime.switchSession(path, opts),
      navigateTree: (id, opts) => session.navigateTree(id, opts),
      reload: () => session.reload(),
    },
  });
  unsubscribe = session.subscribe((event) => {
    /* stream UI / JSON */
  });
}

runtime.setRebindSession(async () => {
  await bindSession();
});

await bindSession();
```

Print and RPC modes call `setRebindSession` the same way. Extension code that captures `pi` or command `ctx` across replace must move post-replace work into `withSession`; the old extension context throws if used after invalidation.

## Lifecycle events

### Session replace (extensions)

| Event | When | Cancel? |
| --- | --- | --- |
| `session_before_switch` | Before `new` / `resume` (including import) | Yes → `{ cancel: true }` |
| `session_before_fork` | Before fork/clone | Yes |
| `session_shutdown` | Old runtime teardown (`new`, `resume`, `fork`, `reload`, `quit`) | No |
| `session_start` | New session ready (`startup`, `reload`, `new`, `resume`, `fork`) | No |

Typical order for `/new`:

```text
session_before_switch { reason: "new" }
session_shutdown      { reason: "new", targetSessionFile? }
session_start         { reason: "new", previousSessionFile? }
```

`session_start` for initial process start uses `reason: "startup"` when no `sessionStartEvent` is passed into the factory.

### Agent run: start notify vs settled

| Signal | Surface | Meaning |
| --- | --- | --- |
| `preflightResult(success)` | `PromptOptions` on `session.prompt` | Prompt accepted/queued (`true`) or rejected before acceptance (`false`). Fires before `prompt()` resolves. Used by RPC to ACK the `prompt` command. |
| `before_agent_start` | Extension | After acceptance, before the agent loop; can adjust system prompt / inject messages |
| `agent_start` / `agent_end` | Extension + public stream | Low-level agent loop; may still retry, compact, or drain follow-ups |
| `agent_settled` | Extension + `session.subscribe` | No automatic retry, compaction retry, or queued continuation remains |
| `waitForIdle()` / `isIdle` | Session + extension `ctx` | Promise/API for the same idle boundary as `agent_settled` |

```mermaid
sequenceDiagram
  participant Host
  participant Session as AgentSession
  participant Ext as Extensions
  participant Agent as Agent loop

  Host->>Session: prompt(text, { preflightResult })
  Session-->>Host: preflightResult(true)
  Session->>Ext: before_agent_start
  Session->>Agent: run (+ retries / follow-ups)
  Agent-->>Session: agent_end (may willRetry)
  Note over Session: continue while retry / queued follow-up
  Session->>Ext: agent_settled
  Session-->>Host: subscribe({ type: "agent_settled" })
  Session-->>Host: waitForIdle resolves
  Session-->>Host: prompt() resolves
```

Rules verified by tests:

- One `agent_settled` after a successful auto-retry sequence (not one per failed attempt)
- Follow-ups queued from `agent_end` handlers run **before** settle
- `ctx.waitForIdle()` waits for session-level settlement, not merely the first `agent_end`
- Switching sessions mid-tool aborts the turn first so the outgoing session gets a tool result, not a dangling call

Public `AgentSessionEvent` types include agent stream events plus session-only:

- `agent_settled`
- `queue_update` (`steering`, `followUp`)
- `compaction_start` / `compaction_end`
- `auto_retry_start` / `auto_retry_end`
- `entry_appended`, `session_info_changed`, `thinking_level_changed`, `bash_execution_update`
- `agent_end` with `willRetry: boolean`

## Event bus

`createEventBus()` is a small channel bus for **inter-extension** messaging (`pi.events`), not a substitute for `session.subscribe`.

```typescript
// EventBus
emit(channel: string, data: unknown): void
on(channel: string, handler: (data: unknown) => void): () => void  // unsubscribe
// EventBusController also: clear()
```

- Shared per resource loader / extension load; handlers log errors and continue
- Subscriptions are tracked and cleared on extension unload
- Example: `examples/extensions/event-bus.ts` uses `pi.events.on` / `pi.events.emit`

```typescript
pi.events.on("my:notification", (data) => { /* ... */ });
pi.events.emit("my:notification", { message: "Session started", from: "my-ext" });
```

## Embedding without the TUI

| Need | API |
| --- | --- |
| Single session | `createAgentSession` or services + `createAgentSessionFromServices` |
| Replace sessions | `createAgentSessionRuntime` + factory |
| Stream output | `session.subscribe` |
| Idle / completion | `await session.prompt(...)` and/or `await session.waitForIdle()` / `agent_settled` |
| Extension commands + UI dialogs | `session.bindExtensions({ mode, uiContext?, commandContextActions })` |
| Shutdown | `await runtime.dispose()` |

Provider and auth stay BYOK: pass a custom `ModelRuntime`, model, and credentials; no hosted connector is required.

`SessionManager.create(cwd)` for disk-backed JSONL, or `SessionManager.inMemory(cwd)` for ephemeral runs. Runtime `newSession` preserves that choice.

## Diagnostics

```typescript
interface AgentSessionRuntimeDiagnostic {
  type: "info" | "warning" | "error";
  message: string;
}
```

Returned on services and on the runtime after each create/replace. The app layer decides whether to print, fail startup, or ignore. Runtime creation throws on hard failures (missing import path, invalid fork entry, missing session cwd); the caller owns UX.

| Error | Cause |
| --- | --- |
| `SessionImportFileNotFoundError` | `importFromJsonl` path missing |
| `MissingSessionCwdError` / assert | Imported or resumed session cwd unusable without override |
| `Invalid entry ID for forking` | Fork target not found / not a user message when `position: "before"` |
| Unsaved session fork | Persisted session file not flushed yet — wait for first assistant response before fork/clone |

## Constraints

- **Do not** call session-replace methods on `AgentSession`; use `AgentSessionRuntime`.
- **Do** re-`subscribe` and re-`bindExtensions` after every successful replace.
- **Do not** keep using a captured extension `ctx` after replace/reload.
- Prompt during compaction throws until compaction finishes.
- Streaming `prompt` without `streamingBehavior: "steer" | "followUp"` throws.
- `preflightResult(false)` only covers pre-acceptance failures; later stream errors use normal events/messages.

## Verification

<Steps>
  <Step title="Construct a runtime">
    Use the factory pattern from `examples/sdk/13-session-runtime.ts`. Confirm `runtime.session` and `runtime.services.cwd`.
  </Step>
  <Step title="Subscribe and prompt">
    `session.subscribe` for `message_update` / `agent_settled`. `await session.prompt("...")` should resolve only after settle.
  </Step>
  <Step title="Replace and rebind">
    `await runtime.newSession()` then rebind. Confirm event order `session_before_switch` → `session_shutdown` → `session_start` if extensions listen.
  </Step>
  <Step title="Idle wait">
    Mid-tool, `waitForIdle` must not resolve until tools and follow-ups finish; one `agent_settled` after retries.
  </Step>
  <Step title="Dispose">
    `await runtime.dispose()` emits `session_shutdown` with reason `quit`.
  </Step>
</Steps>

## Related pages

<CardGroup cols={2}>
  <Card title="Agent sessions" href="/agent-sessions">
    Prompt queue, concurrent behavior, and turn ownership on AgentSession.
  </Card>
  <Card title="SDK" href="/sdk">
    Package main export, createAgentSession, and embed options.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including session runtime example 13.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    Process integration that streams AgentSessionEvent and ACKs via preflightResult.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK entry points.
  </Card>
  <Card title="Extensions" href="/extensions">
    Extension handlers, bindExtensions, and shutdown cleanup contracts.
  </Card>
  <Card title="Branching and session trees" href="/branching-and-tree">
    Fork/clone semantics and tree navigation.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Settlement issues, SIGTERM cleanup, and related failure modes.
  </Card>
</CardGroup>

---

## 19. RPC mode

> Process integration via rpc-entry: command IDs, prompt-during-compaction constraints, JSON stream behavior, and unknown-command handling.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/19-rpc-mode.md
- Generated: 2026-08-09T03:48:04.437Z

### Source Files

- `packages/coding-agent/package.json`
- `packages/coding-agent/src/cli/args.ts`
- `packages/coding-agent/test/suite/regressions/5868-rpc-unknown-command-id.test.ts`
- `packages/coding-agent/test/suite/regressions/7150-rpc-prompt-during-compaction.test.ts`
- `packages/coding-agent/test/suite/regressions/7290-json-stream-linear.test.ts`
- `packages/coding-agent/src/core/agent-session-runtime.ts`

---
title: "RPC mode"
description: "Process integration via rpc-entry: command IDs, prompt-during-compaction constraints, JSON stream behavior, and unknown-command handling."
---

RPC mode runs `@earendil-works/pi-coding-agent` as a headless JSONL process over stdin/stdout. Start it with `pi --mode rpc` or the `./rpc-entry` package export; the process title becomes `pi-rpc`, stdin is reserved for protocol frames, and `runRpcMode` owns command dispatch, event streaming, and process lifecycle.

## When to use RPC

| Integration | Entry | Use when |
|-------------|-------|----------|
| Subprocess protocol | `pi --mode rpc` or `@earendil-works/pi-coding-agent/rpc-entry` | IDE host, another language runtime, or any process that needs JSON control of the agent |
| Typed Node client | `RpcClient` (`src/modes/rpc/rpc-client.ts`) | TypeScript host that wants request IDs and event listeners without hand-rolling framing |
| In-process embed | package main / SDK (`AgentSession`, `AgentSessionRuntime`) | Same Node process; prefer this over spawning RPC |

RPC is one of four run modes (`interactive`, `print`/`text`, `json`, `rpc`). Print/JSON modes also emit structured events; RPC adds a full command surface (prompt, model, session tree, bash, compaction, extension UI) and keeps the process alive until stdin ends or a signal arrives.

## Start the process

<CodeGroup>

```bash title="CLI"
pi --mode rpc \
  --provider anthropic \
  --model claude-sonnet-4-20250514 \
  --name my-session
```

```js title="rpc-entry (package export)"
// package.json exports: "./rpc-entry" → dist/rpc-entry.js
// Equivalent to: main(["--mode", "rpc", ...argv])
import "@earendil-works/pi-coding-agent/rpc-entry";
```

```ts title="RpcClient spawn"
import { RpcClient } from "./src/modes/rpc/rpc-client.ts";

const client = new RpcClient({
  cliPath: "dist/cli.js",
  provider: "anthropic",
  model: "claude-sonnet-4-20250514",
  cwd: process.cwd(),
});
await client.start();
```

</CodeGroup>

### Startup constraints

| Constraint | Behavior |
|------------|----------|
| `@file` arguments | Rejected: `@file arguments are not supported in RPC mode` |
| Piped stdin content | Not read as prompt text; stdin is the JSONL command stream |
| Model required | Process exits if no model is available (non-interactive modes require a model) |
| Catalog refresh | Background model-catalog refresh starts with a 15s abort timeout |
| Session options | `--provider`, `--model`, `--name`/`-n`, `--no-session`, `--session-dir`, tools/extensions flags apply as for other modes |

Process signals: `SIGTERM` exits with code `143`; on non-Windows, `SIGHUP` exits with `129`. stdin `end` triggers orderly dispose and exit.

## Wire protocol

### Framing

- One JSON object per record.
- Record delimiter is LF (`\n`) only.
- Trailing CR on CRLF input is stripped; payload strings may contain `U+2028` / `U+2029`.
- Do **not** use Node `readline` (it splits on Unicode separators that are valid inside JSON strings).
- Implementation: `serializeJsonLine` / `attachJsonlLineReader` in `src/modes/rpc/jsonl.ts`.

### Message kinds on stdout

| Kind | Discriminator | Role |
|------|---------------|------|
| Response | `type: "response"` | Correlated command result (`success` true/false) |
| Session event | `type: "agent_start"`, `message_update`, … | Live agent/session stream (`toJsonEvent`) |
| Extension UI | `type: "extension_ui_request"` | Host must answer interactive extension dialogs |
| Extension error | `type: "extension_error"` | Extension handler threw |

Commands arrive on stdin as JSON objects with a required `type` and optional `id`. Extension UI replies use `type: "extension_ui_response"` and are **not** routed through the command switch.

### Command IDs

Every `RpcCommand` may include `id?: string`. On success or failure, the matching `response` echoes that `id`.

- Unknown commands still echo the request `id`.
- Parse failures use `command: "parse"` and omit `id` when the line never produced a command object.
- Direct `bash` commands also attach `id` to streamed `bash_execution_update` events so hosts can multiplex concurrent shell output.

```json
{"id":"req-1","type":"prompt","message":"List open TODOs"}
{"id":"req-1","type":"response","command":"prompt","success":true}
```

### Prompt response semantics

For `prompt` only:

1. Preflight runs (extension commands, compaction gate, streamingBehavior, auth).
2. On preflight success, RPC emits `{ type: "response", command: "prompt", success: true }` **before** the agent run finishes.
3. On preflight failure, RPC emits `{ success: false, error: "..." }` and does not start the agent.
4. Failures after acceptance appear in the event stream (`agent_*`, messages), not as a second `response` for the same id.

`steer`, `follow_up`, and most other commands return their response after the handler completes.

## Command catalog

Commands are defined in `RpcCommand` / `RpcResponse` (`src/modes/rpc/rpc-types.ts`) and dispatched in `handleCommand` (`src/modes/rpc/rpc-mode.ts`).

### Prompting

| `type` | Required fields | Notes |
|--------|-----------------|-------|
| `prompt` | `message` | Optional `images`, `streamingBehavior: "steer" \| "followUp"`. Async response after preflight |
| `steer` | `message` | Queue while running; no extension-command form |
| `follow_up` | `message` | Queue until idle |
| `abort` | — | Abort current agent operation |
| `new_session` | — | Optional `parentSession`; may return `data.cancelled` |

While streaming, bare `prompt` without `streamingBehavior` fails with:

`Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.`

Slash messages starting with `/` that match an extension command run immediately (even while streaming). Skill (`/skill:…`) and prompt-template expansions run before queue/send.

### State, model, thinking, queues

| `type` | Purpose |
|--------|---------|
| `get_state` | Snapshot: model, thinkingLevel, isStreaming, **isCompacting**, steering/follow-up modes, sessionFile/id/name, autoCompactionEnabled, messageCount, pendingMessageCount |
| `get_messages` | Full `AgentMessage[]` |
| `set_model` | `provider` + `modelId` (error if not found) |
| `cycle_model` | Next scoped model; `data` may be `null` |
| `get_available_models` | `{ models }` |
| `set_thinking_level` | `level`: `off` \| `minimal` \| `low` \| `medium` \| `high` \| `xhigh` \| `max` |
| `cycle_thinking_level` | Next supported level; `data` may be `null` |
| `get_available_thinking_levels` | Levels for current model |
| `set_steering_mode` | `"all"` \| `"one-at-a-time"` |
| `set_follow_up_mode` | `"all"` \| `"one-at-a-time"` |

### Compaction and retry

| `type` | Purpose |
|--------|---------|
| `compact` | Manual compaction; optional `customInstructions`; returns `CompactionResult` |
| `set_auto_compaction` | `enabled: boolean` |
| `set_auto_retry` | `enabled: boolean` |
| `abort_retry` | Cancel in-flight retry delay |

### Bash

| `type` | Purpose |
|--------|---------|
| `bash` | Run shell; optional `excludeFromContext`; streams `bash_execution_update` with command `id`; final `BashResult` in response |
| `abort_bash` | Abort running bash |

Bash output is recorded into session state and reaches the LLM on the **next** `prompt`, not immediately.

### Session tree and export

| `type` | Purpose |
|--------|---------|
| `get_session_stats` | Tokens, cost, contextUsage |
| `export_html` | Optional `outputPath` |
| `switch_session` | `sessionPath`; may cancel via extension |
| `fork` | `entryId`; returns selected text + cancelled |
| `clone` | Fork at current leaf (`position: "at"`); errors if no leaf |
| `get_fork_messages` | User messages available for fork |
| `get_entries` | Optional `since` entry id cursor; unknown `since` → error |
| `get_tree` | Full tree + `leafId` |
| `get_last_assistant_text` | `{ text: string \| null }` |
| `set_session_name` | Non-empty trimmed `name` |

### Slash command discovery

`get_commands` returns extension commands, prompt templates, and skills as `RpcSlashCommand[]`:

```ts
{
  name: string;           // invoke via prompt as /name
  description?: string;
  source: "extension" | "prompt" | "skill";
  sourceInfo: SourceInfo; // path, source, scope, origin, baseDir?
}
```

Skill names are returned as `skill:<name>`. Built-in TUI-only commands (`/settings`, `/hotkeys`, …) are not listed and would not run if sent as a prompt.

## Event stream

Session events are forwarded with `toJsonEvent` after `session.subscribe`.

| Event | Meaning |
|-------|---------|
| `agent_start` / `agent_end` | Low-level agent run boundaries; `agent_end` may include `willRetry` |
| `agent_settled` | No automatic retry, compaction retry, or queued continuation remains |
| `turn_start` / `turn_end` | Assistant turn + tool results |
| `message_start` / `message_update` / `message_end` | Message lifecycle |
| `bash_execution_update` | Direct RPC bash chunk (`id` + `delta`) |
| `tool_execution_start` / `_update` / `_end` | Tool lifecycle |
| `queue_update` | Steering / follow-up queues changed |
| `compaction_start` / `compaction_end` | Manual or auto compaction |
| `auto_retry_*` | Transient provider retry loop |
| `summarization_retry_*` | Compaction/branch-summary retry loop |
| `extension_error` | Extension throw |

### Linear JSON streaming (`message_update`)

Wire `message_update` events are **delta-only**:

- No cumulative `message` snapshot on the event.
- No `assistantMessageEvent.partial` field.
- Clients assemble text/tool calls from `message_start` + deltas (`contentIndex`), then treat `message_end.message` as authoritative.

This keeps stream size linear in generated content (not quadratic). Regression coverage: issue `#7290` / `7290-json-stream-linear.test.ts`.

```json
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello"}}
```

## Compaction interaction

Manual and automatic compaction set an in-progress gate on `AgentSession`.

If an RPC `prompt` arrives while compaction is running:

1. Preflight reports failure (`preflightResult(false)`).
2. Response error text includes `compaction is in progress`.
3. The probe text is **not** added to agent messages or the session file.
4. No `agent_start` / `agent_settled` for that rejected prompt.

```json
{"id":"p1","type":"response","command":"prompt","success":false,
 "error":"Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry."}
```

Use `get_state.isCompacting` and/or `compaction_*` events to wait before prompting. Related: [Context compaction](/compaction).

## Unknown command handling

Unrecognized `type` values do not crash the process. The dispatcher returns:

```json
{
  "id": "test",
  "type": "response",
  "command": "foobar",
  "success": false,
  "error": "Unknown command: foobar"
}
```

The request `id` is preserved (regression `#5868`). Malformed JSON lines produce:

```json
{"type":"response","command":"parse","success":false,"error":"Failed to parse command: ..."}
```

## Extension UI over RPC

When extensions call UI APIs, RPC emits `extension_ui_request` and waits for `extension_ui_response` (except fire-and-forget methods).

| Method | Host action |
|--------|-------------|
| `select` / `input` / `editor` | Reply with `{ type, id, value }` or `{ cancelled: true }` |
| `confirm` | Reply with `{ confirmed: boolean }` or cancelled |
| `notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text` | Fire-and-forget on the wire |

Unsupported in RPC UI context (no-op or fixed error): raw terminal input, working-indicator/loader APIs, custom editor components, autocomplete composition, theme switching, tool expansion toggles.

## Minimal host loop

```text
Host                              pi --mode rpc
  |  {"id":"1","type":"get_state"}     |
  | ---------------------------------> |
  |  {"id":"1","type":"response",...}  |
  | <--------------------------------- |
  |  {"id":"2","type":"prompt",...}    |
  | ---------------------------------> |
  |  {"id":"2","type":"response",      |
  |   "command":"prompt","success":true}|
  | <--------------------------------- |
  |  {"type":"agent_start"} ...        |
  |  {"type":"message_update",...}     |
  |  {"type":"agent_settled"}          |
  | <--------------------------------- |
```

<Steps>
  <Step title="Spawn">
    Start `pi --mode rpc` (or `rpc-entry` / `RpcClient`) with provider/model auth available.
  </Step>
  <Step title="Frame I/O">
    Write one JSON object + `\n` per command; read stdout by splitting on `\n` only.
  </Step>
  <Step title="Correlate">
    Put unique `id` values on commands; match `type: "response"` lines by `id` and `command`.
  </Step>
  <Step title="Drive turns">
    Send `prompt`; treat `success: true` as acceptance; wait for `agent_settled` before assuming the turn is fully idle.
  </Step>
  <Step title="Handle gates">
    If `isCompacting` or compaction events are active, do not prompt until compaction finishes. Supply `streamingBehavior` when the agent is already streaming.
  </Step>
</Steps>

## Error and verification matrix

| Situation | Signal |
|-----------|--------|
| Unknown `type` | `success: false`, `error: "Unknown command: …"`, same `id` |
| Invalid JSON line | `command: "parse"`, `success: false` |
| Prompt while compacting | `success: false`, compaction-in-progress message; no persistence |
| Prompt while streaming without `streamingBehavior` | Preflight error on `prompt` |
| Missing model/auth | Preflight error before acceptance |
| Unknown `get_entries.since` | `Entry not found: …` |
| Empty `set_session_name` | `Session name cannot be empty` |
| Clone with no leaf | `Cannot clone session: no current entry selected` |

## Architecture sketch

```mermaid
flowchart LR
  subgraph Host
    Client[Host / RpcClient]
  end
  subgraph Process["pi process"]
    Entry["rpc-entry / cli --mode rpc"]
    Main[main → AgentSessionRuntime]
    RPC[runRpcMode]
    Sess[AgentSession]
  end
  Client -->|"JSONL commands stdin"| RPC
  Entry --> Main --> RPC
  RPC -->|"handleCommand"| Sess
  Sess -->|"session.subscribe → toJsonEvent"| RPC
  RPC -->|"JSONL responses + events stdout"| Client
```

## Related pages

<CardGroup>
  <Card title="Run modes" href="/run-modes">
    How RPC sits next to interactive, print, and JSON modes.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    AgentSessionRuntime lifecycle, rebind, and non-TUI embedding.
  </Card>
  <Card title="Context compaction" href="/compaction">
    Manual/auto compaction triggers and why prompts are blocked mid-compact.
  </Card>
  <Card title="Package exports" href="/package-exports">
    npm surface including `./rpc-entry` and main SDK export.
  </Card>
  <Card title="SDK" href="/sdk">
    In-process alternative when a subprocess is not required.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Retry, SIGTERM cleanup, and event-settlement issues.
  </Card>
</CardGroup>

---

## 20. CLI reference

> pi binary flags and argument parsing, auth subcommands, experimental auth entry points, and package bin wiring.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/20-cli-reference.md
- Generated: 2026-08-09T03:45:41.021Z

### Source Files

- `packages/coding-agent/src/cli/args.ts`
- `packages/coding-agent/test/args.test.ts`
- `packages/coding-agent/src/cli/auth-command.ts`
- `packages/coding-agent/src/cli/experimental/auth.ts`
- `packages/coding-agent/package.json`
- `packages/coding-agent/README.md`

---
title: "CLI reference"
description: "pi binary flags and argument parsing, auth subcommands, experimental auth entry points, and package bin wiring."
---

The `@earendil-works/pi-coding-agent` package installs the `pi` binary at `dist/cli.js`. That entry sets `process.title`, marks `PI_CODING_AGENT=true` / `AI_AGENT=pi`, configures the HTTP dispatcher, then hands `process.argv.slice(2)` to `main()` in `src/main.ts`. `main()` dispatches auth, package, and config subcommands first; everything else goes through `parseArgs()` into interactive, print/JSON, or RPC run modes.

## Package bin wiring

| Surface | Value |
|---------|--------|
| npm package | `@earendil-works/pi-coding-agent` |
| Binary name | `pi` |
| Bin target | `dist/cli.js` |
| Node entry source | `src/cli.ts` (`#!/usr/bin/env node`) |
| Bun compile entry | `src/bun/cli.ts` → compiled `dist/pi` via `build:binary` |
| Config dir key | `package.json` → `piConfig.configDir` = `.pi` |
| Engines | Node `>=22.19.0` |

```json
// package.json (bin + config)
{
  "bin": { "pi": "dist/cli.js" },
  "piConfig": { "configDir": ".pi" }
}
```

Build makes the CLI executable: `chmod +x dist/cli.js dist/rpc-entry.js`. Related public exports (`main`, `./rpc-entry`, `./client`) are documented on [Package exports](/package-exports).

### App identity derived at runtime

| Constant | Default |
|----------|---------|
| `APP_NAME` | `pi` (overridable via package rename / piConfig) |
| Config directory | `~/.pi/agent` |
| Agent dir env | `PI_CODING_AGENT_DIR` |
| Session dir env | `PI_CODING_AGENT_SESSION_DIR` |

## Invocation shape

```bash
pi [options] [@files...] [messages...]
pi <subcommand> [subcommand-options]
```

### Dispatch order

`main(args)` handles requests in this order:

1. **Offline bootstrap** — if `--offline` is present or `PI_OFFLINE` is truthy (`1` / `true` / `yes`), set `PI_OFFLINE=1` and `PI_SKIP_VERSION_CHECK=1`.
2. **`pi auth …`** — early exit via `runAuthCommand`.
3. **Package commands** — `install` / `remove` / `uninstall` / `update` / `list`.
4. **`pi config`** — resource enable/disable TUI.
5. **`parseArgs(args)`** — general options and messages.
6. **One-shot exits** — `--version`, `--export`, then runtime setup; `--help` and `--list-models` run after extensions load so help can list extension flags.

Unknown short options (single `-` that is not a known shorthand) are hard errors. Unknown long options are collected as extension flags (`unknownFlags`).

## Subcommands

### Package management

| Command | Usage | Notes |
|---------|-------|-------|
| `install` | `pi install <source> [-l] [--approve\|--no-approve]` | Sources: `npm:…`, `git:…`, HTTPS/SSH git URLs, local paths |
| `remove` | `pi remove <source> [-l] …` | Alias: `uninstall` |
| `update` | `pi update [source\|self\|pi] [--self\|--extensions\|--models\|--all] [--extension <source>] [--force] …` | Default with no target: self-update pi only |
| `list` | `pi list [--approve\|--no-approve]` | Lists packages from user and project settings |
| `config` | `pi config [-l] [--approve\|--no-approve]` | TUI to enable/disable package resources; Tab switches global vs project |

`-l` / `--local` writes project settings (`.pi/settings.json`) for install/remove/config. Trust overrides: `-a` / `--approve`, `-na` / `--no-approve`.

### Auth subcommands

Handled before general argument parsing. Require at least one of `--provider` or `--model`. Only those two options are accepted with the auth verb (plus the flags listed below).

| Command | Usage |
|---------|-------|
| Print API key | `pi auth print-api-key [--provider <provider>] [--model <model>]` |
| Print bearer token | `pi auth print-bearer-token [--provider <provider>] [--model <model>] [--min-expiry <duration>]` |
| Check readiness | `pi auth check [--provider <provider>] [--model <model>] [--json] [--credentials] [--no-refresh]` |
| Help | `pi auth`, `pi auth help`, or any auth form with `-h` / `--help` |

**Print commands**

- Write a single credential line to stdout (trailing newline).
- OAuth refresh goes through `ModelRuntime.getAuth()` (15s abort timeout).
- `--min-expiry` (bearer only): duration `N` + `ms|s|m|h` (for example `30m`, `1h`). Default minimum validity when printing bearer tokens is **30 minutes**.
- Errors if the provider uses the wrong credential type (API key vs OAuth), if none is configured, or if multiple providers match without `--provider`.

**`auth check`**

| Flag | Effect |
|------|--------|
| `--json` | Emit full result object as JSON |
| `--credentials` | Include the resolved credential (text mode: credential only; JSON: `credentials` field). Only when status is `ready` |
| `--no-refresh` | Use `ReadOnlyAuthStorage`; do not refresh expired OAuth |

Exit codes for check:

| Status | Exit code | Meaning |
|--------|-----------|---------|
| `ready` | `0` | Provider has usable auth |
| `not_ready` | `1` | Provider missing / credentials missing / credential unavailable |
| `invalid` | `2` | Invalid state or unexpected failure |

Plain text output without `--credentials` is the status string (`ready`, `not_ready`, `invalid`). Reasons include `provider_not_found`, `credentials_not_configured`, `credential_not_available`, `invalid_state`.

<RequestExample>
```bash
# API key for external tooling
pi auth print-api-key --provider openai

# OAuth bearer (refreshes when near expiry)
pi auth print-bearer-token --provider openai-codex --min-expiry 1h

# Machine-readable readiness
pi auth check --provider anthropic --json
```
</RequestExample>

## Global options

Parsed by `parseArgs()` into the `Args` object. Help text is produced by `printHelp()` (and can append extension-registered flags).

### Modes and output

| Flag | Type | Default / notes |
|------|------|-----------------|
| `--mode <mode>` | `text` \| `json` \| `rpc` | Only these three values are accepted |
| `-p`, `--print` | boolean | Non-interactive: process prompt and exit. Next non-flag / non-`@` token (including `---` YAML frontmatter) is consumed as a message |
| `--export <file>` | path | Export session JSONL to HTML and exit; optional output path is the first positional message |

**App mode resolution** (`resolveAppMode`):

| Condition | Mode |
|-----------|------|
| `--mode rpc` | `rpc` |
| `--mode json` | `json` |
| `--print`, or stdin not a TTY, or stdout not a TTY | `print` |
| Otherwise | `interactive` |

Piped stdin content is merged into the initial prompt except in RPC mode (stdin is the RPC channel). If stdin has data and the mode would be interactive, mode becomes print.

### Model and provider

| Flag | Notes |
|------|-------|
| `--provider <name>` | Provider id (help default text: `google`) |
| `--model <pattern>` | Pattern or id; supports `provider/id` and optional `:<thinking>` (for example `sonnet:high`) |
| `--api-key <key>` | Runtime API key for the selected model; requires `--model` / provider+model / scoped models |
| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Invalid values are warnings |
| `--models <patterns>` | Comma-separated patterns for Ctrl+P cycling (globs / fuzzy) |
| `--list-models [search]` | List models and exit; optional fuzzy search (not a flag or `@file`) |

### Session

| Flag | Notes |
|------|-------|
| `-c`, `--continue` | Continue most recent session |
| `-r`, `--resume` | Interactive session picker |
| `--session <path\|id>` | Session file or partial UUID |
| `--session-id <id>` | Exact project session id; creates if missing |
| `--fork <path\|id>` | Fork into a new session |
| `--session-dir <dir>` | Storage/lookup directory (overrides settings and env) |
| `--no-session` | Ephemeral (in-memory) session |
| `-n`, `--name <name>` | Display name at startup; empty/missing value errors |

**Conflicts**

- `--fork` cannot combine with `--session`, `--continue`, `--resume`, or `--no-session`.
- `--session-id` cannot combine with `--session`, `--continue`, or `--resume`.
- RPC mode rejects `@file` arguments.

### Tools

| Flag | Effect |
|------|--------|
| `-t`, `--tools <list>` | Comma-separated allowlist (built-in, extension, custom) |
| `-xt`, `--exclude-tools <list>` | Comma-separated denylist |
| `-nt`, `--no-tools` | Disable all tools by default |
| `-nbt`, `--no-builtin-tools` | Disable built-ins only; keep extension/custom tools |

Built-in names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` (`grep` / `find` / `ls` are off by default in the product surface).

### Resources and prompts

| Flag | Effect |
|------|--------|
| `-e`, `--extension <path>` | Load extension (repeatable) |
| `-ne`, `--no-extensions` | Skip discovery; explicit `-e` still applies |
| `--skill <path>` | Load skill file/dir (repeatable) |
| `-ns`, `--no-skills` | Disable skill discovery |
| `--prompt-template <path>` | Load template file/dir (repeatable) |
| `-np`, `--no-prompt-templates` | Disable template discovery |
| `--theme <path>` | Load theme file/dir (repeatable) |
| `--no-themes` | Disable theme discovery |
| `-nc`, `--no-context-files` | Disable `AGENTS.md` / `CLAUDE.md` discovery |
| `--system-prompt <text>` | Replace default system prompt |
| `--append-system-prompt <text>` | Append text or file contents (repeatable) |

### Trust, TUI, misc

| Flag | Effect |
|------|--------|
| `-a`, `--approve` | Trust project-local files for this run |
| `-na`, `--no-approve` | Ignore project-local files for this run |
| `--tui-mode <mode>` | `regular` (default) or `fullscreen` (experimental UI) |
| `--verbose` | Force verbose startup |
| `--offline` | Disable startup network ops (same as `PI_OFFLINE=1`) |
| `-h`, `--help` | Show help (after extension load when possible) |
| `-v`, `--version` | Print package version and exit |

### Positional inputs

| Form | Behavior |
|------|----------|
| `@path` | File attachment (`fileArgs`); `@` prefix stripped |
| bare text | Message string (`messages`) |
| `--flag=value` | Unknown long flag with equals value |
| `--flag value` | Unknown long flag with next non-flag token as value |
| `--flag` | Unknown boolean long flag (`true`) |

Unknown long flags become `extensionFlagValues` for extension CLI registration (for example plan-mode’s `--plan`).

## Environment variables (CLI-relevant)

| Variable | Role |
|----------|------|
| Provider API key vars | See help / [Authentication](/authentication) (many `*_API_KEY` names) |
| `PI_CODING_AGENT_DIR` | Config directory (default `~/.pi/agent`) |
| `PI_CODING_AGENT_SESSION_DIR` | Session storage (overridden by `--session-dir`) |
| `PI_PACKAGE_DIR` | Override package directory (Nix/Guix-style layouts) |
| `PI_OFFLINE` | `1` / `true` / `yes` disables startup network ops |
| `PI_TELEMETRY` | Override install telemetry (`1`/`true`/`yes` or `0`/`false`/`no`) |
| `PI_SHARE_VIEWER_URL` | Base URL for `/share` (default `https://pi.dev/session/`) |
| `PI_EXPERIMENTAL` | Must be exactly `1` for experimental first-time setup |
| `PI_STARTUP_BENCHMARK` | Interactive-only startup benchmark path |

## Experimental auth and remote CLI scaffolding

Under `src/cli/experimental/` there is a composable command tree (`experimentalCli`) used by unit tests. It is **not** currently invoked from `main()`; the production binary still uses the classic `parseArgs` path for normal sessions.

| Command form | Options | Purpose in the tree |
|--------------|---------|---------------------|
| Default (`pi`) | `--listen`, `--auth-token`, `--auth-token-file`, plus legacy `parseArgs` remainder | Host-style parse result with optional listen addresses and auth input |
| `server` | `--listen`, `--auth-token`, `--auth-token-file` | Server invocation; rejects leftover classic CLI options |
| `client` | `--connect`, `--auth-token`, `--auth-token-file` | Client invocation; rejects leftover classic CLI options |

### Experimental auth input

```ts
type AuthInput =
  | { type: "token"; token: string }
  | { type: "file"; path: string };
```

| Option | Behavior |
|--------|----------|
| `--auth-token <token>` | Inline token |
| `--auth-token-file <path>` | Token file path |
| Both | Error: mutually exclusive |
| Duplicate same option | Error: may only be specified once |

### Transport addresses

Only Unix sockets: `unix:///absolute/path` (no authority, query, or fragment). Used with `--listen` / `--connect`.

When experimental features are enabled (`PI_EXPERIMENTAL=1`), interactive first-time setup can prompt for theme and analytics opt-in before runtime services start.

## Argument parsing edge cases

| Case | Behavior |
|------|----------|
| `-p` followed by `--provider` | Does not treat the flag as a prompt; continues option parsing |
| `-p` followed by `---…` text | Consumed as prompt (YAML frontmatter-safe) |
| Invalid `--thinking` | Warning diagnostic; run continues without that level |
| Invalid `--tui-mode` / missing value | Error diagnostic; process exits 1 if any error diagnostics exist |
| Missing `--name` value | Error: `--name requires a value` |
| Extension load failure in runtime | Reported; hint `pi -ne` |
| `--api-key` without model | Error diagnostic at runtime construction |

## Examples

```bash
# Interactive
pi
pi "List all .ts files in src/"

# Files + prompt
pi @prompt.md @image.png "What color is the sky?"

# Print / JSON / RPC
pi -p "Summarize package.json"
pi --mode json -p "List tools"
pi --mode rpc

# Session
pi --continue "What did we discuss?"
pi --resume
pi --session-id my-run --name "Refactor auth"
pi --fork 1234abcd
pi --no-session -p "Ephemeral ask"

# Model and tools
pi --model openai/gpt-4o "Help me refactor this"
pi --models "anthropic/*,*sonnet*"
pi --tools read,grep,find,ls -p "Review src/"
pi --exclude-tools bash
pi --no-builtin-tools -e ./my-ext.ts

# Resources
pi --no-extensions -e ./only-this.ts
pi --no-skills --skill ./one-skill
pi --export ~/.pi/agent/sessions/.../session.jsonl
pi --list-models sonnet
```

## Verification

```bash
pi --version
pi --help
pi auth check --provider openai --json
pi --list-models
```

Expected signals: version string matches package version; help lists options and extension flags when extensions load; auth check exits 0/1/2 with status text or JSON.

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    npm and installer paths, bin entry, and verification for @earendil-works/pi-coding-agent.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK invocation and when to use each.
  </Card>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth login, credential storage, and refresh behavior.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    Process integration via rpc-entry and the JSON command stream.
  </Card>
  <Card title="Package exports" href="/package-exports">
    Public npm surface: main, rpc-entry, client, bin name, piConfig.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default tools, extension tools, and --tools / --exclude-tools filters.
  </Card>
</CardGroup>

---

## 21. Package exports

> Public npm surface for @earendil-works/pi-coding-agent: main, rpc-entry, client exports, bin name, and piConfig.configDir.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/21-package-exports.md
- Generated: 2026-08-09T03:44:15.422Z

### Source Files

- `packages/coding-agent/package.json`
- `packages/coding-agent/README.md`
- `packages/coding-agent/examples/sdk/README.md`
- `packages/coding-agent/CHANGELOG.md`

---
title: "Package exports"
description: "Public npm surface for @earendil-works/pi-coding-agent: main, rpc-entry, client exports, bin name, and piConfig.configDir."
---

`@earendil-works/pi-coding-agent` is an ESM package (`"type": "module"`) that publishes one CLI binary and three import surfaces: the package root (SDK), `./rpc-entry` (process integration), and `./client` (experimental remote-session controller). Package metadata also declares `piConfig.configDir` as `.pi`, which aligns with the default agent config path used by the SDK.

## Package identity

| Field | Value |
|-------|--------|
| Name | `@earendil-works/pi-coding-agent` |
| Version (documented snapshot) | `0.84.1` |
| Description | Coding agent CLI with read, bash, edit, write tools and session management |
| Module type | `module` (ESM) |
| License | MIT |
| Node engines | `>=22.19.0` |
| Repository | `git+https://github.com/earendil-works/pi.git` (`packages/coding-agent`) |

Install (global CLI):

```bash
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
```

`--ignore-scripts` disables dependency lifecycle scripts. Pi does not require install scripts for normal npm installs. An installer alternative is `curl -fsSL https://pi.dev/install.sh | sh`.

## Export map

The public `exports` map is the supported import surface. Consumers should use these subpaths, not deep paths under `dist/`.

| Export subpath | Import specifier | Types | Runtime entry |
|----------------|------------------|-------|---------------|
| `.` (main) | `@earendil-works/pi-coding-agent` | `./dist/index.d.ts` | `./dist/index.js` |
| `./rpc-entry` | `@earendil-works/pi-coding-agent/rpc-entry` | *(not declared in package.json)* | `./dist/rpc-entry.js` |
| `./client` | `@earendil-works/pi-coding-agent/client` | `./dist/client/index.d.ts` | `./dist/client/index.js` |

Top-level `main` and `types` point at the same root as `exports["."]`:

- `main`: `./dist/index.js`
- `types`: `./dist/index.d.ts`

<Note>
`./rpc-entry` declares only an `import` condition. Root and `./client` declare both `types` and `import`. All three use ESM import conditions only—no `require` condition is published.
</Note>

## Binary entry

| Field | Value |
|-------|--------|
| Bin name | `pi` |
| Target | `dist/cli.js` |

The build script marks both CLI and RPC entry scripts executable:

```text
chmod +x dist/cli.js dist/rpc-entry.js
```

After a global install, the `pi` command starts the interactive agent. Authentication is typically an API key environment variable or interactive `/login`.

```bash
export ANTHROPIC_API_KEY=sk-ant-...
pi
```

Pi runs in four modes: interactive, print or JSON, RPC for process integration, and SDK embedding. Binary flags and auth subcommands are covered on the CLI and run-modes pages.

## Main export (SDK)

Import the package root to embed pi in application code:

```ts
import {
  createAgentSession,
  DefaultResourceLoader,
  ModelRuntime,
  SessionManager,
  SettingsManager,
} from "@earendil-works/pi-coding-agent";
```

SDK examples also document `createAgentSessionRuntime()` for runtime-backed session replacement when the active session cwd changes.

### Documented constructors and helpers

| Symbol | Role |
|--------|------|
| `createAgentSession(options)` | Construct an agent session |
| `createAgentSessionRuntime()` | Manage runtime-backed session replacement |
| `ModelRuntime.create(...)` | Canonical model and authentication runtime |
| `DefaultResourceLoader` | Load extensions, skills, prompts, themes, context files |
| `SessionManager.create(cwd)` / `SessionManager.inMemory()` | Session persistence or in-memory sessions |
| `SettingsManager.create(cwd, agentDir)` / `SettingsManager.inMemory()` | Settings load and overrides |

### `createAgentSession` options (from SDK examples)

| Option | Default | Description |
|--------|---------|-------------|
| `modelRuntime` | Runtime using `agentDir/auth.json` and `models.json` | Canonical model and authentication runtime |
| `cwd` | `process.cwd()` | Working directory |
| `agentDir` | `~/.pi/agent` | Config directory |
| `model` | From settings / first available | Model to use |
| `thinkingLevel` | From settings / `"off"` | `off`, `low`, `medium`, `high` |
| `tools` | `["read", "bash", "edit", "write"]` built-ins | Allowlist across built-in, extension, and custom tools |
| `customTools` | `[]` | Additional tool definitions |
| `resourceLoader` | `DefaultResourceLoader` | Resources for extensions, skills, prompts, themes, context files |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
| `settingsManager` | `SettingsManager.create(cwd, agentDir)` | Settings overrides |

### Minimal embed

```ts
import { createAgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({ modelRuntime });

session.subscribe((event) => {
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
    process.stdout.write(event.assistantMessageEvent.delta);
  }
});

await session.prompt("Hello");
```

### Full-control embed

```ts
import {
  createAgentSession,
  DefaultResourceLoader,
  ModelRuntime,
  SessionManager,
  SettingsManager,
} from "@earendil-works/pi-coding-agent";
import { getModel } from "@earendil-works/pi-ai";

const model = getModel("anthropic", "claude-opus-4-5");
const customRuntime = await ModelRuntime.create({
  authPath: "/my/app/auth.json",
  modelsPath: "/my/app/models.json",
});
await customRuntime.setRuntimeApiKey("anthropic", process.env.MY_KEY!);

const resourceLoader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are helpful.",
  extensionFactories: [myExtension],
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
  agentsFilesOverride: () => ({ agentsFiles: [] }),
  promptsOverride: () => ({ prompts: [], diagnostics: [] }),
});
await resourceLoader.reload();

const { session } = await createAgentSession({
  model,
  modelRuntime: customRuntime,
  resourceLoader,
  tools: ["read", "bash", "my_tool"],
  customTools: [myTool],
  sessionManager: SessionManager.inMemory(),
  settingsManager: SettingsManager.inMemory(),
});
```

Run packaged examples from the package tree:

```bash
cd packages/coding-agent
npx tsx examples/sdk/01-minimal.ts
```

## `rpc-entry` export

| Item | Value |
|------|--------|
| Specifier | `@earendil-works/pi-coding-agent/rpc-entry` |
| File | `./dist/rpc-entry.js` |
| Conditions | `import` only |
| Build | Made executable alongside `dist/cli.js` |

Use this subpath for RPC process integration (JSON stream, command protocol). Command IDs, compaction constraints, and stream event shapes belong on the RPC mode page.

<Info>
Changelog 0.84.0 notes that JSON and RPC `message_update` events emit only `assistantMessageEvent` deltas. Clients assemble partial messages between `message_start` and `message_end`; `message_end` remains authoritative.
</Info>

## `client` export

| Item | Value |
|------|--------|
| Specifier | `@earendil-works/pi-coding-agent/client` |
| Types | `./dist/client/index.d.ts` |
| Runtime | `./dist/client/index.js` |

Added as experimental remote-session client APIs in 0.84.0. The documented controller is `RemoteSession`, with transcript reducers, alongside transport-neutral `PiClient`, a CBOR protocol, and Unix-socket transport.

Session list metadata uses durable `SessionMetadata`. `RemoteSession.sessions` does not expose runtime phase, model, thinking, attachment, or lock state; those remain available from acquired `SessionSnapshot` values.

Related runtime packages (same major line as this package snapshot):

| Dependency | Role in stack |
|------------|----------------|
| `@earendil-works/pi-client` | Client transport stack |
| `@earendil-works/pi-protocol` | Remote protocol |
| `@earendil-works/pi-agent-core` | Agent/session core |
| `@earendil-works/pi-ai` | Models and providers (for example `getModel`) |
| `@earendil-works/pi-tui` | Terminal UI |

## `piConfig.configDir`

```json
"piConfig": {
  "configDir": ".pi"
}
```

`configDir` is the package-declared Pi config directory name (`.pi`). SDK defaults place agent config under `~/.pi/agent` (`agentDir`), including paths such as:

| Path | Typical use |
|------|-------------|
| `~/.pi/agent` | Default `agentDir` |
| `agentDir/auth.json` | Auth credentials for `ModelRuntime` |
| `agentDir/models.json` (or `~/.pi/agent/models.json`) | Custom providers and models |
| `~/.pi/agent/keybindings.json` | Keybinding customization |
| `~/.pi/agent/sessions/` | Auto-saved JSONL sessions |

`ModelRuntime.create({ authPath, modelsPath })` can override credential and model catalog locations for embedded apps.

## Published package contents

`files` lists what is included on npm:

```text
dist/
docs/
examples/
containerization.md
CHANGELOG.md
npm-shrinkwrap.json
```

Build and publish scripts of note:

| Script | Behavior |
|--------|----------|
| `build` | Compile TypeScript, `chmod +x` on `dist/cli.js` and `dist/rpc-entry.js`, copy interactive theme/assets and export-html assets into `dist/` |
| `build:binary` | Build workspace deps, compile a Bun standalone binary to `dist/pi` with `--no-compile-autoload-bunfig`, copy binary assets |
| `prepublishOnly` | `clean` → `build` → `shrinkwrap` |

Standalone binary build notes: Bun compilation uses `--no-compile-autoload-bunfig` so binaries do not crash when the cwd contains a `bunfig.toml` with `preload`.

## Runtime requirements

| Requirement | Constraint |
|-------------|------------|
| Node.js | `>=22.19.0` |
| Module format | ESM only (`import`) |
| Optional dependency | `@mariozechner/clipboard` (clipboard support) |

Peer-style internal packages resolve at `^0.84.1` for this package version.

## Import quick reference

<Tabs>
  <Tab title="CLI">
```bash
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
pi
```
  </Tab>
  <Tab title="SDK (main)">
```ts
import {
  createAgentSession,
  ModelRuntime,
} from "@earendil-works/pi-coding-agent";
```
  </Tab>
  <Tab title="RPC entry">
```ts
import "@earendil-works/pi-coding-agent/rpc-entry";
// Or resolve the package export as a process entry for RPC integration
```
  </Tab>
  <Tab title="Client">
```ts
import /* RemoteSession controller */ from "@earendil-works/pi-coding-agent/client";
```
  </Tab>
</Tabs>

## Verification

<Steps>
  <Step title="Confirm package install">
    Install `@earendil-works/pi-coding-agent` and ensure Node is `>=22.19.0`.
  </Step>
  <Step title="Verify the binary">
    Run `pi` (or the path to `dist/cli.js`) and confirm the interactive harness starts.
  </Step>
  <Step title="Verify the main export">
    Import `createAgentSession` and `ModelRuntime` from `@earendil-works/pi-coding-agent`, create a session, and call `session.prompt(...)`.
  </Step>
  <Step title="Verify subpath exports">
    Resolve `@earendil-works/pi-coding-agent/rpc-entry` and `@earendil-works/pi-coding-agent/client` through the package `exports` map (for example via Node’s package resolution), not by hard-coding `node_modules` file paths.
  </Step>
</Steps>

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    npm and installer paths, bin entry, and verification for this package.
  </Card>
  <Card title="Run modes" href="/run-modes">
    Interactive, print/JSON, RPC, and SDK invocation shapes.
  </Card>
  <Card title="SDK" href="/sdk">
    Embed with the main export: sessions, models, tools, settings.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes for minimal, custom, and full-control setups.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    Process integration via `rpc-entry`, commands, and JSON streams.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    `pi` binary flags, auth subcommands, and bin wiring.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    Runtime services for embedding without the interactive TUI.
  </Card>
  <Card title="Overview" href="/overview">
    What pi is, four run modes, and first docs routes.
  </Card>
</CardGroup>

---

## 22. SDK examples

> Copy-paste SDK recipes: minimal agent, custom model, skills, tools, extensions, sessions, settings, and full-control setups.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/22-sdk-examples.md
- Generated: 2026-08-09T03:44:18.866Z

### Source Files

- `packages/coding-agent/examples/sdk/README.md`
- `packages/coding-agent/examples/sdk/01-minimal.ts`
- `packages/coding-agent/examples/sdk/11-sessions.ts`
- `packages/coding-agent/examples/sdk/05-tools.ts`
- `packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts`
- `packages/coding-agent/examples/sdk/12-full-control.ts`

---
title: "SDK examples"
description: "Copy-paste SDK recipes: minimal agent, custom model, skills, tools, extensions, sessions, settings, and full-control setups."
---

`createAgentSession()` is the primary embed surface for `@earendil-works/pi-coding-agent`. The recipes under `packages/coding-agent/examples/sdk/` construct a session, optionally wire `ModelRuntime`, `SessionManager`, `SettingsManager`, and a `ResourceLoader`, then run prompts through `session.subscribe()` / `session.prompt()` and tear down with `session.dispose()`. A companion path, `createAgentSessionRuntime()`, appears in the runtime example for recreating cwd-bound services when the active session cwd changes.

## Example catalog

| File | Recipe |
|------|--------|
| `01-minimal.ts` | All defaults: discovery from `cwd` and `~/.pi/agent`, model from settings or first available |
| `02-custom-model.ts` | Select model and thinking level |
| `03-custom-prompt.ts` | Replace or modify system prompt |
| `04-skills.ts` | Discover, filter, or replace skills |
| `05-tools.ts` | Built-in tool allowlists and custom `cwd` |
| `06-extensions.ts` | Logging, blocking, result modification; custom tools via `pi.registerTool()` |
| `07-context-files.ts` | `AGENTS.md` context files |
| `08-slash-commands.ts` | File-based slash commands |
| `09-api-keys-and-oauth.ts` | API key resolution and OAuth via `ModelRuntime` |
| `10-settings.ts` | Override compaction, retry, terminal settings |
| `11-sessions.ts` | In-memory, new file, continue recent, list/open |
| `12-full-control.ts` | Explicit configuration, no discovery |
| `13-session-runtime.ts` | Runtime-backed session replacement |

## Run an example

```bash
cd packages/coding-agent
npx tsx examples/sdk/01-minimal.ts
```

Replace the filename with any entry in the catalog.

## Minimal agent

Uses package defaults: discovers skills, extensions, tools, and context files from `cwd` and `~/.pi/agent`; model comes from settings or the first available model.

```typescript
import { createAgentSession } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession();

try {
  session.subscribe((event) => {
    if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
      process.stdout.write(event.assistantMessageEvent.delta);
    }
  });

  await session.prompt("What files are in the current directory?");
  session.state.messages.forEach((msg) => {
    console.log(msg);
  });
} finally {
  session.dispose();
}
```

With an explicit runtime (same defaults otherwise):

```typescript
import { createAgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({ modelRuntime });
```

## Custom model and thinking level

```typescript
import { getModel } from "@earendil-works/pi-ai";
import { createAgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const model = getModel("anthropic", "claude-opus-4-5");
const { session } = await createAgentSession({
  model,
  thinkingLevel: "high",
  modelRuntime,
});
```

`thinkingLevel` values: `off`, `low`, `medium`, `high`. Default is from settings, otherwise `"off"`.

In the full-control recipe, model resolution uses `@earendil-works/pi-ai/compat`:

```typescript
import { getModel } from "@earendil-works/pi-ai/compat";

const model = getModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Model not found");
```

## System prompt override

`DefaultResourceLoader` accepts `systemPromptOverride`. Call `reload()` before passing the loader into `createAgentSession`.

```typescript
import {
  createAgentSession,
  DefaultResourceLoader,
  ModelRuntime,
} from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const loader = new DefaultResourceLoader({
  systemPromptOverride: (base) => `${base}\n\nBe concise.`,
});
await loader.reload();
const { session } = await createAgentSession({ resourceLoader: loader, modelRuntime });
```

Replace the entire prompt by returning a fixed string from the override:

```typescript
systemPromptOverride: () => "You are helpful.",
```

## Skills

Disable discovery and supply an empty skill set via `skillsOverride` on `DefaultResourceLoader`:

```typescript
const resourceLoader = new DefaultResourceLoader({
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
});
await resourceLoader.reload();
```

The same loader pattern supports related resource overrides used in full-control setups: `agentsFilesOverride`, `promptsOverride`, and `extensionFactories`.

## Tools

Tool names are an allowlist matched against built-in, extension, and custom tools. Default built-ins when unset: `["read", "bash", "edit", "write"]`.

If you pass a custom `cwd`, `createAgentSession()` applies that cwd when it builds built-in tools. Custom tools are not registered through the `tools` array alone; register them through the extensions system with `pi.registerTool()` (see `06-extensions.ts`).

```typescript
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

// Read-only (no edit/write)
const { session: readOnlySession } = await createAgentSession({
  tools: ["read", "grep", "find", "ls"],
  sessionManager: SessionManager.inMemory(),
});
readOnlySession.dispose();

// Subset of tools
const { session: customToolsSession } = await createAgentSession({
  tools: ["read", "bash", "grep"],
  sessionManager: SessionManager.inMemory(),
});
customToolsSession.dispose();

// Custom cwd + full built-in write surface
const customCwd = "/path/to/project";
const { session: customCwdSession } = await createAgentSession({
  cwd: customCwd,
  tools: ["read", "bash", "edit", "write"],
  sessionManager: SessionManager.inMemory(customCwd),
});
customCwdSession.dispose();
```

Quick-reference allowlist with a named custom tool:

```typescript
const { session } = await createAgentSession({
  tools: ["read", "bash", "my_tool"],
  customTools: [myTool],
  modelRuntime,
});
```

`customTools` defaults to `[]`.

## Extensions and resource factories

Wire extension factories and resource overrides through `DefaultResourceLoader`:

```typescript
const resourceLoader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are helpful.",
  extensionFactories: [myExtension],
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
  agentsFilesOverride: () => ({ agentsFiles: [] }),
  promptsOverride: () => ({ prompts: [], diagnostics: [] }),
});
await resourceLoader.reload();
```

`06-extensions.ts` covers logging, blocking, and result modification patterns. Always `reload()` after constructing a loader that uses overrides or factories.

## Authentication and ModelRuntime

Configure provider auth through `ModelRuntime`. Default runtime paths resolve under `agentDir` (`auth.json`, `models.json`).

```typescript
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";

// Default auth locations
const modelRuntime = await ModelRuntime.create();
const { session: defaultAuthSession } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime,
});
defaultAuthSession.dispose();

// Custom auth and models paths
const customRuntime = await ModelRuntime.create({
  authPath: "/tmp/my-app/auth.json",
  modelsPath: "/tmp/my-app/models.json",
});
const { session: customAuthSession } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime: customRuntime,
});
customAuthSession.dispose();

// Runtime API key override (not persisted as the sole source of truth for disk auth)
await modelRuntime.setRuntimeApiKey("anthropic", "sk-my-temp-key");
const { session: runtimeKeySession } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime,
});
runtimeKeySession.dispose();
```

Full-control setups often gate keys on env vars:

```typescript
if (process.env.MY_ANTHROPIC_KEY) {
  await modelRuntime.setRuntimeApiKey("anthropic", process.env.MY_ANTHROPIC_KEY);
}
```

## Sessions

`SessionManager` controls persistence. `createAgentSession` returns `{ session }` and may also return `modelFallbackMessage` when continuing a session with a changed model.

```typescript
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

// In-memory (no persistence)
const { session: inMemory } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
});
console.log("In-memory session:", inMemory.sessionFile ?? "(none)");
inMemory.dispose();

// New persistent session under cwd
const { session: newSession } = await createAgentSession({
  sessionManager: SessionManager.create(process.cwd()),
});
console.log("New session file:", newSession.sessionFile);
newSession.dispose();

// Continue most recent (or create new if none)
const { session: continued, modelFallbackMessage } = await createAgentSession({
  sessionManager: SessionManager.continueRecent(process.cwd()),
});
if (modelFallbackMessage) console.log("Note:", modelFallbackMessage);
console.log("Continued session:", continued.sessionFile);
continued.dispose();

// List and open a specific session
const sessions = await SessionManager.list(process.cwd());
if (sessions.length > 0) {
  const { session: opened } = await createAgentSession({
    sessionManager: SessionManager.open(sessions[0].path),
  });
  console.log("Opened:", opened.sessionId);
  opened.dispose();
}
```

Optional custom session directory (no cwd encoding), shown commented in the example:

```typescript
// const customDir = "/path/to/my-sessions";
// SessionManager.create(process.cwd(), customDir);
// SessionManager.list(process.cwd(), customDir);
// SessionManager.continueRecent(process.cwd(), customDir);
```

Default when `sessionManager` is omitted: `SessionManager.create(cwd)`.

## Settings

Use `SettingsManager.inMemory()` for process-local overrides without disk settings:

```typescript
import { SettingsManager } from "@earendil-works/pi-coding-agent";

const settingsManager = SettingsManager.inMemory({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 2 },
});
```

Default when omitted: `SettingsManager.create(cwd, agentDir)`. Example `10-settings.ts` targets compaction, retry, and terminal settings overrides.

## Full control

Replace discovery with an explicit `ResourceLoader`, fixed `agentDir`, selected tools, in-memory session and settings, and a dedicated `ModelRuntime`.

```typescript
import { getModel } from "@earendil-works/pi-ai/compat";
import {
  createAgentSession,
  createExtensionRuntime,
  ModelRuntime,
  type ResourceLoader,
  SessionManager,
  SettingsManager,
} from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create({
  authPath: "/tmp/my-agent/auth.json",
  modelsPath: "/tmp/my-agent/models.json",
});
if (process.env.MY_ANTHROPIC_KEY) {
  await modelRuntime.setRuntimeApiKey("anthropic", process.env.MY_ANTHROPIC_KEY);
}

const model = getModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Model not found");

const settingsManager = SettingsManager.inMemory({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 2 },
});

const cwd = process.cwd();

const resourceLoader: ResourceLoader = {
  getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
  getSkills: () => ({ skills: [], diagnostics: [] }),
  getPrompts: () => ({ prompts: [], diagnostics: [] }),
  getThemes: () => ({ themes: [], diagnostics: [] }),
  getAgentsFiles: () => ({ agentsFiles: [] }),
  getSystemPrompt: () => `You are a minimal assistant.
Available: read, bash. Be concise.`,
  getSystemPromptSource: () => undefined,
  getAppendSystemPrompt: () => [],
  getAppendSystemPromptSources: () => [],
  extendResources: () => {},
  reload: async () => {},
};

const { session } = await createAgentSession({
  cwd,
  agentDir: "/tmp/my-agent",
  model,
  thinkingLevel: "off",
  modelRuntime,
  resourceLoader,
  tools: ["read", "bash"],
  sessionManager: SessionManager.inMemory(cwd),
  settingsManager,
});

try {
  session.subscribe((event) => {
    if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
      process.stdout.write(event.assistantMessageEvent.delta);
    }
  });
  await session.prompt("List files in the current directory.");
} finally {
  session.dispose();
}
```

Equivalent composition with `DefaultResourceLoader` + overrides (from the examples README quick reference):

```typescript
const customRuntime = await ModelRuntime.create({
  authPath: "/my/app/auth.json",
  modelsPath: "/my/app/models.json",
});
await customRuntime.setRuntimeApiKey("anthropic", process.env.MY_KEY!);

const resourceLoader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are helpful.",
  extensionFactories: [myExtension],
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
  agentsFilesOverride: () => ({ agentsFiles: [] }),
  promptsOverride: () => ({ prompts: [], diagnostics: [] }),
});
await resourceLoader.reload();

const { session } = await createAgentSession({
  model,
  modelRuntime: customRuntime,
  resourceLoader,
  tools: ["read", "bash", "my_tool"],
  customTools: [myTool],
  sessionManager: SessionManager.inMemory(),
  settingsManager: SettingsManager.inMemory(),
});
```

## Session runtime recreation

`13-session-runtime.ts` shows `createAgentSessionRuntime()` with a recreate function that closes over process-global fixed inputs and rebuilds cwd-bound services and sessions when the active session cwd changes. Use that path when the host process must swap working directories without restarting the agent process.

## Events

Subscribe before `prompt()`. Common event types used in the examples:

| `event.type` | Use |
|--------------|-----|
| `message_update` | Stream text when `assistantMessageEvent.type === "text_delta"`; write `assistantMessageEvent.delta` |
| `tool_execution_start` | Log `event.toolName` |
| `tool_execution_end` | Log `event.result` |
| `agent_end` | Turn complete |

```typescript
session.subscribe((event) => {
  switch (event.type) {
    case "message_update":
      if (event.assistantMessageEvent.type === "text_delta") {
        process.stdout.write(event.assistantMessageEvent.delta);
      }
      break;
    case "tool_execution_start":
      console.log(`Tool: ${event.toolName}`);
      break;
    case "tool_execution_end":
      console.log(`Result: ${event.result}`);
      break;
    case "agent_end":
      console.log("Done");
      break;
  }
});
await session.prompt("Hello");
```

After a turn, inspect history with `session.state.messages`. Always call `session.dispose()` in a `finally` block.

## `createAgentSession` options

| Option | Default | Description |
|--------|---------|-------------|
| `modelRuntime` | Runtime using `agentDir/auth.json` and `models.json` | Canonical model and authentication runtime |
| `cwd` | `process.cwd()` | Working directory |
| `agentDir` | `~/.pi/agent` | Config directory |
| `model` | From settings / first available | Model to use |
| `thinkingLevel` | From settings / `"off"` | `off`, `low`, `medium`, `high` |
| `tools` | `["read", "bash", "edit", "write"]` built-ins | Allowlist across built-in, extension, and custom tools |
| `customTools` | `[]` | Additional tool definitions |
| `resourceLoader` | `DefaultResourceLoader` | Extensions, skills, prompts, themes, context files |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
| `settingsManager` | `SettingsManager.create(cwd, agentDir)` | Settings overrides |

## Lifecycle checklist

```text
ModelRuntime.create(...)     optional auth / models paths, setRuntimeApiKey
DefaultResourceLoader / ResourceLoader
  └─ reload()                required after override/factory construction
SettingsManager.inMemory|create
SessionManager.inMemory|create|continueRecent|open
createAgentSession({ ... })
  ├─ session.subscribe(...)
  ├─ await session.prompt(...)
  ├─ session.state.messages
  └─ session.dispose()
```

## Related pages

<CardGroup>
  <Card title="SDK" href="/sdk">
    Package main export: minimal construction, hooks, custom models, tools, and settings.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    AgentSessionRuntime services, lifecycle events, and embedding without the TUI.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default tools, extension tools, allowlists, and blocked-tool behavior.
  </Card>
  <Card title="Skills" href="/skills">
    SKILL.md rules, naming, collisions, and invocation controls.
  </Card>
  <Card title="Extensions" href="/extensions">
    TypeScript extension registration, active tools, and shutdown contracts.
  </Card>
  <Card title="Agent sessions" href="/agent-sessions">
    Session lifecycle, prompt queue, and runtime ownership of a turn.
  </Card>
  <Card title="Settings" href="/settings">
    Settings load, reload, and merge rules.
  </Card>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth, credential storage, and auth failure modes.
  </Card>
  <Card title="Package exports" href="/package-exports">
    Public npm surface for @earendil-works/pi-coding-agent.
  </Card>
  <Card title="Extension examples" href="/extension-examples">
    Reference extension packages: subagents, plan-mode, and related samples.
  </Card>
</CardGroup>

---

## 23. Extension examples

> Reference extension packages: subagent agents, plan-mode, doom-overlay, dynamic resources skill, and auto-commit-on-exit.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/23-extension-examples.md
- Generated: 2026-08-09T04:03:44.920Z

### Source Files

- `packages/coding-agent/examples/extensions/README.md`
- `packages/coding-agent/examples/extensions/subagent/README.md`
- `packages/coding-agent/examples/extensions/subagent/agents.ts`
- `packages/coding-agent/examples/extensions/plan-mode/README.md`
- `packages/coding-agent/examples/extensions/doom-overlay/README.md`
- `packages/coding-agent/examples/extensions/auto-commit-on-exit.ts`

---
title: "Extension examples"
description: "Reference extension packages: subagent agents, plan-mode, doom-overlay, dynamic resources skill, and auto-commit-on-exit."
---

The `examples/extensions/` tree contains loadable `ExtensionAPI` factories that register tools, commands, shortcuts, interactive components, resource discovery hooks, and session lifecycle handlers. These examples range from single TypeScript files to directories with an `index.ts` entry point and supporting assets.

## Load an example

From `packages/coding-agent/`, load an example for the current process with `--extension` or `-e`:

```bash
pi -e ./examples/extensions/subagent
pi -e ./examples/extensions/plan-mode
pi -e ./examples/extensions/doom-overlay
pi -e ./examples/extensions/dynamic-resources
pi -e ./examples/extensions/auto-commit-on-exit.ts
```

For auto-discovery, place single-file extensions in `~/.pi/agent/extensions/` or `.pi/extensions/`. Preserve the complete directory for multi-file extensions:

```text
~/.pi/agent/extensions/
├── auto-commit-on-exit.ts
├── subagent/
│   ├── index.ts
│   └── agents.ts
└── doom-overlay/
    ├── index.ts
    ├── doom-component.ts
    ├── doom-engine.ts
    ├── doom-keys.ts
    ├── wad-finder.ts
    └── doom/
```

Project-local `.pi/extensions/` entries load only after the project is trusted.

<Warning>
Extensions execute with the same system permissions as `pi`. Review extension code, agent definitions, shell commands, downloaded assets, and Git hooks before installing them.
</Warning>

## Example inventory

| Example | Registered surface | State and side effects | Primary constraint |
|---|---|---|---|
| `subagent/` | `subagent` tool | Starts child `pi` processes; no child sessions | Project agent definitions are executable repository-controlled prompts |
| `plan-mode/` | `--plan`, `/plan`, `/todos`, `Ctrl+Alt+P` | Persists plan state in session entries | Its read-only policy is a tool filter and Bash regex, not a sandbox |
| `doom-overlay/` | `/doom-overlay` | Keeps the engine in process memory and may download a WAD | TUI mode and adjacent WASM assets are required |
| `dynamic-resources/` | `resources_discover` handler | Adds a skill, prompt template, and theme | Resources must remain valid and reachable beside the extension |
| `auto-commit-on-exit.ts` | `session_shutdown` handler | Stages and commits the entire Git worktree | Runs for more shutdown reasons than process exit |

## Subagent orchestration

`examples/extensions/subagent/index.ts` registers a `subagent` tool that delegates work to separate `pi` processes. Each child runs in JSON print mode with `--no-session`, receives its agent definition through model, tool, and appended-system-prompt arguments, and streams `message_end` and `tool_result_end` events back to the parent tool call.

### Install the bundled definitions

Loading the extension alone does not install its sample agents or workflow prompt templates. From the repository root:

```bash
mkdir -p ~/.pi/agent/extensions/subagent
ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/index.ts" \
  ~/.pi/agent/extensions/subagent/index.ts
ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/agents.ts" \
  ~/.pi/agent/extensions/subagent/agents.ts

mkdir -p ~/.pi/agent/agents
for file in packages/coding-agent/examples/extensions/subagent/agents/*.md; do
  ln -sf "$(pwd)/$file" ~/.pi/agent/agents/"$(basename "$file")"
done

mkdir -p ~/.pi/agent/prompts
for file in packages/coding-agent/examples/extensions/subagent/prompts/*.md; do
  ln -sf "$(pwd)/$file" ~/.pi/agent/prompts/"$(basename "$file")"
done
```

This installs four sample definitions:

| Agent | Default role | Declared tools |
|---|---|---|
| `scout` | Codebase reconnaissance | `read`, `grep`, `find`, `ls`, `bash` |
| `planner` | Read-only implementation planning | `read`, `grep`, `find`, `ls` |
| `reviewer` | Code and security review | `read`, `grep`, `find`, `ls`, `bash` |
| `worker` | General implementation | No restriction; child defaults apply |

The bundled definitions specify model names. The extension mechanism itself is provider-neutral: `model` is an optional string forwarded to child `pi --model`. Omit it to use the child process’s configured default, or replace it with another configured model.

### Agent definition format

Definitions are Markdown files with YAML frontmatter:

```markdown
---
name: scout
description: Fast codebase recon
tools: read, grep, find, ls, bash
model: configured-model-name
---

System instructions for the child agent.
```

`name` and `description` are required. `tools` is an optional comma-separated allowlist, `model` is optional, and the Markdown body becomes the appended system prompt. Unreadable files and definitions missing required fields are skipped.

Agent discovery runs for every tool invocation:

- User agents: `~/.pi/agent/agents/*.md`
- Project agents: the nearest ancestor `.pi/agents/*.md`
- Default `agentScope`: `user`
- With `agentScope: "both"`, project definitions replace same-named user definitions

### Tool request modes

Exactly one execution mode must be provided.

| Mode | Input | Behavior |
|---|---|---|
| Single | `agent`, `task`, optional `cwd` | Runs one child and returns its final assistant text |
| Parallel | `tasks: [{ agent, task, cwd? }]` | Accepts at most eight tasks and runs up to four concurrently |
| Chain | `chain: [{ agent, task, cwd? }]` | Runs sequentially and replaces every `{previous}` token with the preceding final output |

A chain request has this shape:

```json
{
  "chain": [
    {
      "agent": "scout",
      "task": "Locate the authentication implementation"
    },
    {
      "agent": "planner",
      "task": "Create a change plan from these findings:\n\n{previous}"
    }
  ],
  "agentScope": "user"
}
```

The installed prompt templates provide common chains:

```text
/implement add caching to session lookup
/scout-and-plan separate provider selection from authentication
/implement-and-review validate configuration input
```

These are prompt templates from `prompts/*.md`, not commands registered by the extension.

### Trust and failure behavior

<Warning>
Project agents are repository-controlled prompts that can request shell commands and file modifications. Interactive use asks for confirmation by default when `agentScope` includes `project`. Headless contexts have no confirmation UI, so selecting `project` or `both` is itself the trust decision.
</Warning>

Single and chain failures are detected from a nonzero exit code or a child stop reason of `error` or `aborted`. Chains stop at the first failed step. Parallel execution reports each failure in its aggregate summary but does not mark the aggregate tool result as an error.

Model-visible parallel output is capped at 50 KiB per task; complete results remain in tool details. Cancellation sends `SIGTERM` to running children. Treat child-process cancellation as cooperative cleanup, not process containment.

## Plan mode

`examples/extensions/plan-mode/index.ts` adds a planning state that changes the active tool set, injects planning instructions, extracts numbered steps, and tracks `[DONE:n]` markers.

Start in plan mode:

```bash
pi -e ./examples/extensions/plan-mode --plan
```

Or toggle it during an interactive session:

| Control | Behavior |
|---|---|
| `/plan` | Enter or leave plan mode |
| `Ctrl+Alt+P` | Keyboard equivalent of `/plan` |
| `/todos` | Display the extracted steps and completion state |
| `--plan` | Enter plan mode during session startup |

### State transitions

| State | Active behavior |
|---|---|
| Normal | Uses the current active-tool configuration |
| Planning | Removes `edit` and `write`, preserves other active tools, and adds `read`, `bash`, `grep`, `find`, `ls`, and `questionnaire` |
| Selection | After a valid plan, the TUI offers execute, stay in plan mode, or refine |
| Execution | Restores the pre-plan tool snapshot and injects remaining steps before each agent run |
| Complete | Clears execution state after every item has a matching `[DONE:n]` marker |

A recognized plan must contain a `Plan:` header followed by numbered, single-line items:

```text
Plan:
1. Inspect the current session loader
2. Add a focused regression test
3. Update the implementation
```

Step text is normalized and truncated to 50 display characters. During execution, assistant responses mark progress with case-insensitive tags such as `[DONE:1]`. The extension persists `enabled`, `executing`, the todo list, and the pre-plan tool snapshot in custom `plan-mode` session entries. Resume reconstructs completion state from assistant messages after the most recent execution marker.

The execute/refine selection requires `ctx.hasUI`. In a headless mode, the extension can restrict tools and inject planning context, but it does not display the post-plan selection dialog.

<Warning>
Plan mode is not a security boundary. It preserves custom active tools, including custom tools that may mutate state. Bash filtering uses regular expressions rather than shell parsing; commands such as `curl -o ...` or `git branch <name>` can pass the current rules. Use an OS sandbox or a stricter tool policy when mutation prevention is required.
</Warning>

## DOOM overlay

`examples/extensions/doom-overlay/index.ts` registers `/doom-overlay` and renders the bundled WebAssembly build through a custom TUI overlay.

```bash
pi -e ./examples/extensions/doom-overlay
```

Then run either:

```text
/doom-overlay
/doom-overlay /absolute/path/to/doom1.wad
```

The command rejects non-TUI modes. Without an explicit path, WAD discovery checks:

1. `doom1.wad` beside the extension.
2. `./doom1.wad` and `./DOOM1.WAD`.
3. `~/doom1.wad` and `~/.doom/doom1.wad`.
4. The downloadable shareware WAD.

The downloaded gzip is decompressed, checked for an `IWAD` header, and written beside the extension. A read-only extension installation therefore prevents first-run download. An explicit command argument bypasses discovery and header validation; file and engine errors are caught by the command and shown as a load failure.

The generated `doom/build/doom.js` and `doom/build/doom.wasm` assets must remain adjacent to the extension. A missing JavaScript build reports that `doom/build.sh` must be run.

### Runtime behavior

- The game loop targets 35 ticks per second.
- Each terminal cell uses a 24-bit-color `▀` to represent two framebuffer pixels.
- Render height preserves the effective 3.2:1 terminal-cell aspect ratio, with a minimum height of ten rows and one footer row.
- Current overlay options are `width: "75%"`, `maxHeight: "95%"`, `anchor: "center"`, and a one-row top margin.
- `Q` sends the pause key, stops the render timer, and closes the overlay.
- Reopening the same WAD within the same extension instance resumes the cached engine.
- The native adapter does not provide sound.

| Action | Input |
|---|---|
| Move or strafe | WASD or arrow keys |
| Run | Shift with WASD |
| Fire | `F` or Ctrl combinations other than `Ctrl+C` |
| Use/open | Space |
| Menu and map | Escape and Tab |
| Weapon/input digits | `0`–`9` are forwarded; the footer documents weapons `1`–`7` |
| Pause and close overlay | `Q` |

## Dynamic skills, prompts, and themes

`examples/extensions/dynamic-resources/index.ts` contributes resources through `resources_discover`:

```typescript
pi.on("resources_discover", () => ({
  skillPaths: [join(baseDir, "SKILL.md")],
  promptPaths: [join(baseDir, "dynamic.md")],
  themePaths: [join(baseDir, "dynamic.json")],
}));
```

The paths are based on `import.meta.url`, so discovery does not depend on the session working directory. The event runs after `session_start` during startup and reload. Pi extends the resource loader with the returned paths and rebuilds the session system prompt when necessary.

| File | Discovered interface | Behavior |
|---|---|---|
| `SKILL.md` | `/skill:dynamic-resources` | Loads the `dynamic-resources` skill instructions |
| `dynamic.md` | `/dynamic` | Requests a repository-structure summary with build and test commands |
| `dynamic.json` | Theme named `dynamic-resources` | Adds the theme to the selector; it is not selected automatically |

Use the resources after loading the extension:

```text
/skill:dynamic-resources
/dynamic
/theme
```

<Info>
Resource discovery is portable and provider-neutral. The extension contributes local file paths and does not depend on a hosted skill catalog, connector, or specific model provider. The same pattern can source resources from files, repository packages, or another catalog adapter as long as the handler returns valid local paths.
</Info>

## Auto-commit on shutdown

`examples/extensions/auto-commit-on-exit.ts` registers one `session_shutdown` handler. It exposes no command, tool, flag, or confirmation UI.

Its shutdown sequence is:

1. Run `git status --porcelain`.
2. Return silently outside a Git repository or when the worktree is clean.
3. Find the most recent assistant message and join its text blocks.
4. Build `[pi] <first line>`, limiting the assistant fragment to 50 characters and appending `...` when truncated.
5. Fall back to `[pi] Work in progress` when no assistant text exists.
6. Run `git add -A`.
7. Run `git commit -m <message>`.
8. Notify the user only when the commit succeeds and a UI is available.

<Warning>
The example stages every tracked, untracked, modified, and deleted path visible to `git add -A`, including unrelated work and previously staged changes. It has no branch check, path allowlist, confirmation, or rollback.
</Warning>

Despite its filename, the handler is not limited to process exit. `session_shutdown` runs with reasons `quit`, `reload`, `new`, `resume`, and `fork`. The example ignores the reason, so any of those transitions can create a commit.

It also ignores the result of `git add` and does not report commit failures. Hooks, identity configuration, repository policy, or an empty index can prevent the commit without a UI error. A production adaptation should normally:

- Require `event.reason === "quit"` if commits must occur only at exit.
- Stage explicit paths instead of using `git add -A`.
- Check and report both command results.
- Obtain confirmation before committing.
- Preserve repository-specific commit-message and signing rules.

Verify the result with:

```bash
git status --short
git log -1 --pretty=%s
```

## Troubleshooting

| Symptom | Check |
|---|---|
| `Unknown agent` | Confirm the definition has `name` and `description`, is under the selected user/project agents directory, and matches `agentScope` |
| Project-agent confirmation never appears | Confirmation is TUI-only; headless execution does not display it |
| Plan mode still permits mutation | Inspect preserved custom tools and the regex allowlist; the example only removes `edit` and `write` directly |
| No plan selection appears | The response must contain a `Plan:` header, valid numbered items, and an interactive UI |
| DOOM reports interactive mode required | Run regular interactive `pi`, not print or JSON mode |
| DOOM cannot load its WAD | Check the supplied path, extension-directory write permission, network access, and adjacent WASM build |
| `/dynamic` or the skill is missing | Reload after installing the extension and validate the sibling Markdown frontmatter |
| Auto-commit produces no commit | Check Git status, hooks, identity configuration, and commit errors manually; the example reports success only |

## Related pages

<CardGroup>
  <Card title="Extensions" href="/extensions">
    Extension registration, lifecycle events, resource hooks, UI contracts, and cleanup.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Built-in and extension tools, active-tool filtering, and blocked-tool behavior.
  </Card>
  <Card title="Skills" href="/skills">
    Skill discovery, frontmatter validation, invocation names, and collision rules.
  </Card>
  <Card title="Themes and Pi packages" href="/themes-and-packages">
    Theme requirements and packaging extensions with related resources.
  </Card>
</CardGroup>

---

## 24. Troubleshooting

> Retry and network failures, credential refresh hangs, SIGTERM cleanup, bash output truncation, and session event settlement issues.

- Page Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/pages/24-troubleshooting.md
- Generated: 2026-08-09T03:46:07.179Z

### Source Files

- `packages/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts`
- `packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts`
- `packages/coding-agent/test/suite/regressions/7027-credential-refresh-hang.test.ts`
- `packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts`
- `packages/coding-agent/test/suite/regressions/5303-bash-output-truncation.test.ts`
- `packages/coding-agent/src/core/auth-guidance.ts`

---
title: "Troubleshooting"
description: "Retry and network failures, credential refresh hangs, SIGTERM cleanup, bash output truncation, and session event settlement issues."
---

Transient provider failures, post-login catalog refresh, process signal teardown, and bash child-process I/O are handled inside `packages/coding-agent` with explicit retry settings, session events, and regression-backed cleanup contracts. This page records the failure modes those paths cover and the signals that confirm recovery.

## Auto-retry for transient provider errors

When session settings enable retry, a failed assistant turn with a recognized transient error is retried instead of ending the prompt as a hard failure. Recovery is observable as session events and a second model call that can produce normal assistant text.

### Retry settings shape

Harnesses enable auto-retry with:

```ts
settings: {
  retry: {
    enabled: true,
    maxRetries: 3,
    baseDelayMs: 1,
  },
}
```

| Key | Type | Role in recovery |
| --- | --- | --- |
| `retry.enabled` | `boolean` | Turns auto-retry on for the session |
| `retry.maxRetries` | `number` | Upper bound on retry attempts (tests use `3`) |
| `retry.baseDelayMs` | `number` | Base delay between attempts (tests use `1` ms) |

Without `retry.enabled: true`, the regression cases that expect a second model call do not apply.

### Errors that retry

| Pattern | Example / form | Issue coverage |
| --- | --- | --- |
| Network drop | Exact message `Network connection lost.` with `stopReason: "error"` | #3317 |
| OpenAI explicit retry guidance | Long prose that tells the client to retry and include a request ID | #6019 |
| Bedrock explicit retry guidance | JSON body: `{"message":"The system encountered an unexpected error during processing. Try your request again."}` | #6019 |

OpenAI message used in regression coverage:

```text
An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message.
```

### Session events for retry settlement

Successful auto-retry emits a start event, then an end event after the recovered turn:

| Event type | Fields asserted in tests | Meaning |
| --- | --- | --- |
| `auto_retry_start` | `errorMessage` | Retry cycle started for the failing error text |
| `auto_retry_end` | `success: true` | Retry cycle settled successfully |

On success, the faux model call count is `2` (initial failure + recovered response), and assistant text includes the recovered message (for example `recovered after reconnect` or `recovered`).

```mermaid
stateDiagram-v2
  [*] --> Prompt
  Prompt --> ErrorTurn: stopReason error
  ErrorTurn --> AutoRetryStart: retry.enabled
  AutoRetryStart --> RecoveredTurn: second model call
  RecoveredTurn --> AutoRetryEndSuccess: success true
  AutoRetryEndSuccess --> [*]
```

<Note>
If `auto_retry_start` fires but `auto_retry_end` never arrives with `success: true`, treat the turn as unsettled: the first error matched retry guidance, but recovery did not complete within the observed event stream.
</Note>

### Quick checks

1. Confirm `retry.enabled` is true for the session under test or embed.
2. Confirm the provider error text matches a known transient pattern (exact network string or provider-supplied “try again” guidance).
3. Expect `auto_retry_start` with that `errorMessage`, then `auto_retry_end` with `success: true`, and a second model invocation.

---

## Credential refresh hang after login

Login must not stall behind a slow or infinite model-catalog network refresh. Issues **#7027** and **#7113** cover two related contracts: concurrent `ModelRuntime` login during a stalled refresh, and interactive post-login refresh with a hard timeout.

### Login is not blocked by a stalled catalog refresh

`ModelRuntime` can run a network `refresh({ allowNetwork: true, providers: [...] })` while `login` for the same provider still completes.

| Operation | Behavior under stall |
| --- | --- |
| `refreshModels({ allowNetwork: true })` hanging | Does not prevent `runtime.login(providerId, "api_key", …)` from resolving |
| Login result | Resolves to the stored credential (e.g. `{ type: "api_key", key: "secret" }`) |
| Available models | Snapshot still includes models from the non-network registration path |
| Credentials | `AuthStorage` retains the written API key for the provider |
| Stalled refresh completion | Can still resolve later with `{ aborted: false }` without having blocked login |

```ts
// Pattern under test: offline register, start hanging network refresh, then login
await runtime.refresh({ allowNetwork: false, providers: [provider.id] });
const stalledRefresh = runtime.refresh({ allowNetwork: true, providers: [provider.id] });
// login resolves while refreshModels is still awaiting forever
await runtime.login(provider.id, "api_key", { prompt, notify });
```

### Interactive login finishes before bounded background refresh

After interactive API-key auth, `InteractiveMode.completeProviderAuthentication` kicks a scoped catalog refresh with an `AbortSignal`:

```ts
runtime.refresh({
  providers: [providerId],
  signal: /* AbortSignal */,
});
```

| Timing | UI behavior |
| --- | --- |
| Immediately after login completes | No timeout warning yet |
| After **15_000** ms if refresh has not finished | Warning via `showWarning` |

Exact warning string:

```text
Saved API key for Stalled Login, but its model catalog refresh timed out; using cached models.
```

(The provider display name is the authenticated provider’s name; the test uses `"Stalled Login"`.)

<Warning>
If login appears to hang, distinguish “credential write stuck” from “catalog refresh still running.” Interactive mode is expected to finish authentication first and only warn later if the background catalog refresh times out.
</Warning>

### Symptom matrix

| Symptom | Likely cause | Expected recovery |
| --- | --- | --- |
| `/login` never returns while models refresh | Older hang pattern (login gated on network catalog) | Login must complete independently of stalled `refreshModels` |
| Login succeeds but models look stale | Post-login refresh aborted after 15s | Warning about timeout; cached models remain in use |
| Credentials missing after “successful” UI | Auth write failed (not covered as hang) | Re-check `AuthStorage` / re-run login |

---

## SIGTERM and signal-exit cleanup

Interactive process exit interacts with `proper-lockfile` → `signal-exit`. That library can re-send `SIGTERM`/`SIGHUP` when it sees no other process listeners during the same signal dispatch. `InteractiveMode` therefore must keep its own signal handlers registered until async terminal cleanup finishes (issue **#5724**).

### Signal-triggered shutdown order

When `shutdown({ fromSignal: true })` runs:

| Phase | Call | Handlers |
| --- | --- | --- |
| 1 | `runtimeHost.dispose()` starts | Signal handlers still registered (`unregisterSignalHandlers` not called yet) |
| 2 | After `dispose` resolves | `ui.terminal.drainInput(ms)` then `stop()` |
| — | While dispose is pending | Order is only `["dispose"]`; unregister has not run |

```mermaid
sequenceDiagram
  participant SIG as Process signal
  participant IM as InteractiveMode.shutdown
  participant RH as runtimeHost.dispose
  participant UI as terminal.drainInput
  participant Stop as stop

  SIG->>IM: fromSignal true
  IM->>RH: dispose() async
  Note over IM: signal handlers still registered
  RH-->>IM: resolved
  IM->>UI: drainInput
  IM->>Stop: stop()
```

<Warning>
Unregistering signal handlers before `runtimeHost.dispose()` completes can let `signal-exit` re-fire `SIGTERM`/`SIGHUP` and interrupt cleanup. Keep handlers installed for the full async dispose window on signal-triggered shutdown.
</Warning>

### Failure mode

| Failure | Observable effect |
| --- | --- |
| Handlers removed too early during SIGTERM path | Cleanup interrupted; process may exit before dispose/drain complete |
| Correct path | `dispose` → `drainInput` → `stop` after dispose resolves; unregister not called mid-dispose |

---

## Bash output truncation after process exit

`waitForChildProcess` waits for child exit and remaining stdio. A historical bug armed a **fixed 100 ms** timer on `exit` and destroyed streams when it fired. If a short-lived detached descendant held stdout open, `close` never fired; any writes more than 100 ms after `exit` were dropped. That showed up as truncated bash tool output (for example mid lint-staged / listr2 output after `git commit`), which models often read as a hang (issue **#5303**).

### Current contract

| Situation | Behavior |
| --- | --- |
| Child emits `exit`, stdout still open, more data keeps arriving | Grace timer **re-arms on each chunk**; wait stays open while the pipe is active |
| Child emits `exit`, stdout held open but quiet | Wait resolves after one full grace interval (~100 ms) with no further data |
| Data every 50 ms for several ticks after exit | All chunks (including late ones) remain readable before resolve |

```text
exit(0)
  │
  ├─ write chunk  → re-arm grace (100ms)
  ├─ write chunk  → re-arm grace
  ├─ …quiet…
  └─ grace elapses → waitForChildProcess resolves (exit code)
```

### Symptom vs fix

| Before fix | After fix |
| --- | --- |
| Fixed 100 ms from `exit` → destroy streams | Grace restarts on every stdout/stderr chunk |
| Late hook output truncated mid-stream | Active writers keep being read |
| Model sees partial hook output / “hang” | Full post-exit burst captured until idle grace |

If bash results still look truncated with a quiet held-open pipe, that is expected after the idle grace: the wait intentionally releases when no further data arrives.

---

## Auth and model availability messages

When credentials or model selection are missing, user-facing strings from `auth-guidance` point operators at `/login` and local provider/model docs under the docs path from `getDocsPath()`.

| Helper | Message shape |
| --- | --- |
| `formatNoModelsAvailableMessage()` | `No models available.` + login help |
| `formatNoModelSelectedMessage()` | `No model selected.` + login help + “Then use /model to select a model.” |
| `formatNoApiKeyFoundMessage(provider)` | `No API key found for <provider or "the selected model">.` + login help |

Login help body:

```text
Use /login to log into a provider via OAuth or API key. See:
  <docsPath>/providers.md
  <docsPath>/models.md
```

`<ParamField body="provider" type="string">`
When the provider id is the sentinel `"unknown"`, `formatNoApiKeyFoundMessage` displays **the selected model** instead of that id.
</ParamField>

### Recovery steps

<Steps>
  <Step title="Authenticate">
    Run `/login` (OAuth or API key) for the provider that owns the selected model.
  </Step>
  <Step title="Select a model">
    If the message is “No model selected,” use `/model` after credentials exist.
  </Step>
  <Step title="If login succeeded but catalog timed out">
    Expect the 15s warning about timed-out catalog refresh and cached models; retry provider/model refresh later or check network access for `refreshModels`.
  </Step>
</Steps>

---

## Issue index

| Issue | Surface | Contract |
| --- | --- | --- |
| #3317 | Session auto-retry | Retries `Network connection lost.` when `retry.enabled` |
| #5303 | `waitForChildProcess` / bash | Re-arm 100 ms grace on post-exit I/O chunks |
| #5724 | `InteractiveMode.shutdown` | Keep signal handlers until signal-path dispose finishes |
| #6019 | Session auto-retry | Retries OpenAI and Bedrock explicit “try again” error text |
| #7027 / #7113 | `ModelRuntime` + interactive auth | Login not blocked by stalled refresh; 15s post-login refresh timeout warning |

---

## Related pages

<CardGroup>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth login, credential storage, and refresh hang failure modes.
  </Card>
  <Card title="Providers and models" href="/providers-and-models">
    Provider registration, catalog refresh, and provider-retry message behavior.
  </Card>
  <Card title="Settings" href="/settings">
    Session settings load/reload, including `retry` merge and related options.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    Runtime services, lifecycle events, and non-TUI embedding of sessions.
  </Card>
  <Card title="Agent sessions" href="/agent-sessions">
    Prompt loop, concurrent behavior, and turn ownership around recovery.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default bash tool surface and how tool output reaches the model.
  </Card>
</CardGroup>

---
