{"owner":"earendil-works","repo":"pi","hasSkills":true,"totalSkillsCount":79,"totalTokensCount":209910,"categories":["anthropic-skill","root-instruction","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".pi/skills/add-llm-provider.md",".pi/skills/github-work.md",".pi/skills/interactive-testing.md",".pi/skills/release.md","AGENTS.md","packages/agent/README.md","packages/ai/README.md","packages/chord/README.md","packages/chord/docs/guide.md","packages/chord/docs/spec.md","packages/chord/docs/tutorial.md","packages/chord/src/delta/README.md","packages/codemode/README.md","packages/codemode/examples/cloudflare/README.md","packages/coding-agent/README.md","packages/coding-agent/docs/cli-integration.md","packages/coding-agent/docs/cli.md","packages/coding-agent/docs/codemode.md","packages/coding-agent/docs/compaction.md","packages/coding-agent/docs/configuration.md","packages/coding-agent/docs/containerization.md","packages/coding-agent/docs/custom-provider.md","packages/coding-agent/docs/docs.json","packages/coding-agent/docs/environment-variables.md","packages/coding-agent/docs/extensions.md","packages/coding-agent/docs/how-pi-works.md","packages/coding-agent/docs/index.md","packages/coding-agent/docs/json.md","packages/coding-agent/docs/keybindings.md","packages/coding-agent/docs/llama-cpp.md","packages/coding-agent/docs/mcp.md","packages/coding-agent/docs/message-types.md","packages/coding-agent/docs/models-configuration.md","packages/coding-agent/docs/models.md","packages/coding-agent/docs/nvidia-rtx-llama-cpp.md","packages/coding-agent/docs/packages.md","packages/coding-agent/docs/prompt-templates.md","packages/coding-agent/docs/providers.md","packages/coding-agent/docs/quickstart.md","packages/coding-agent/docs/rpc-commands.md","packages/coding-agent/docs/rpc-extension-ui.md","packages/coding-agent/docs/rpc.md","packages/coding-agent/docs/sdk.md","packages/coding-agent/docs/security.md","packages/coding-agent/docs/session-format.md","packages/coding-agent/docs/sessions.md","packages/coding-agent/docs/settings.md","packages/coding-agent/docs/shell-aliases.md","packages/coding-agent/docs/skills.md","packages/coding-agent/docs/slash-commands.md","packages/coding-agent/docs/terminal-setup.md","packages/coding-agent/docs/termux.md","packages/coding-agent/docs/themes.md","packages/coding-agent/docs/tmux.md","packages/coding-agent/docs/tui.md","packages/coding-agent/docs/usage.md","packages/coding-agent/docs/virtual-models.md","packages/coding-agent/docs/windows.md","packages/coding-agent/examples/README.md","packages/coding-agent/examples/extensions/README.md","packages/coding-agent/examples/extensions/doom-overlay/README.md","packages/coding-agent/examples/extensions/dynamic-resources/SKILL.md","packages/coding-agent/examples/extensions/plan-mode/README.md","packages/coding-agent/examples/extensions/subagent/README.md","packages/coding-agent/examples/sdk/README.md","packages/coding-agent/test/mcp-conformance/README.md","packages/coding-agent/test/suite/README.md","packages/durable/README.md","packages/durable/docs/spec.md","packages/env/README.md","packages/env/docs/protocol.md","packages/env/docs/semantics.md","packages/evals/README.md","packages/mcp/README.md","packages/telemetry/README.md","packages/tui/README.md","packages/tui/native/darwin/README.md","packages/tui/native/linux/README.md","packages/tui/native/win32/README.md"],"skills":{".pi/skills/add-llm-provider.md":"---\nname: add-llm-provider\ndescription: Checklist for adding a new LLM provider to packages/ai. Covers core types, provider implementation, lazy registration, model generation, the full test matrix, coding-agent wiring, and docs.\n---\n\n# Adding a New LLM Provider (packages/ai)\n\nA new provider touches multiple files. Work through these steps in order.\n\n## 1. Core Types (`packages/ai/src/types.ts`)\n\n- Add API identifier to `Api` type union (e.g. `\"bedrock-converse-stream\"`).\n- Create options interface extending `StreamOptions`.\n- Add mapping to `ApiOptionsMap`.\n- Add provider name to `KnownProvider` type union.\n\n## 2. Provider Implementation (`packages/ai/src/providers/`)\n\nCreate a provider file exporting:\n\n- `stream<Provider>()` returning `AssistantMessageEventStream`.\n- `streamSimple<Provider>()` for `SimpleStreamOptions` mapping.\n- Provider-specific options interface.\n- Message/tool conversion functions.\n- Response parsing that emits standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`).\n\n## 3. Provider Exports and Lazy Registration\n\n- Add a package subpath export in `packages/ai/package.json` pointing at `./dist/providers/<provider>.js`.\n- Add `export type` re-exports in `packages/ai/src/index.ts` for provider option types that should remain available from the root entry.\n- Register the provider in `packages/ai/src/providers/register-builtins.ts` via lazy loader wrappers; do not statically import provider implementation modules there.\n- Add credential detection in `packages/ai/src/env-api-keys.ts`.\n\n## 4. Model Generation (`packages/ai/scripts/generate-models.ts`)\n\n- Add logic to fetch/parse models from the provider source.\n- Map to the standardized `Model` interface.\n\n## 5. Tests (`packages/ai/test/`)\n\n- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API impl such as `openai-completions`.\n- Add the provider to the broader matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.\n- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (e.g. GPT and Claude), add at least one pair per family.\n- For non-standard auth, create a utility (e.g. `bedrock-utils.ts`) with credential detection.\n\n## 6. Coding Agent (`packages/coding-agent/`)\n\n- `src/core/model-resolver.ts`: add default model ID to `defaultModelPerProvider`.\n- `src/core/provider-display-names.ts`: add API-key login display name so `/login` and related UI show the provider for built-in API-key auth.\n- `src/cli/args.ts`: add env var documentation.\n- `README.md`: add provider setup instructions.\n- `docs/providers.md`: add setup instructions, env var, and `auth.json` key.\n\n## 7. Documentation\n\n- `packages/ai/README.md`: add to providers table, document options/auth, add env vars.\n- `packages/ai/CHANGELOG.md`: add entry under `## [Unreleased]`.\n",".pi/skills/github-work.md":"---\nname: github-work\ndescription: Work on GitHub issues and pull requests of this repo. Use when asked to investigate, analyze, fix, or review an issue or PR, and when asked to wrap up such work (changelog, comment, commit, push, close).\n---\n\n# Working on GitHub issues and PRs\n\nThe repo's prompt templates in `.pi/prompts/` (two directories above this skill) define the workflow. Read the matching template in full and follow it. Where a template says `$ARGUMENTS` or `$@`, substitute the issue or PR number or URL from the request, and any extra instructions the user gave.\n\n| Request | Template |\n|---|---|\n| Investigate, analyze, or fix an issue | `.pi/prompts/is.md` |\n| Review a pull request | `.pi/prompts/pr.md` |\n| Wrap up issue or PR work: changelog, final comment, commit, push, close | `.pi/prompts/wr.md` |\n\nRules:\n\n- `is.md` analyzes and proposes only. Implement only when the user asks for it, then keep following AGENTS.md (checks, tests, changelog rules).\n- `pr.md` never checks out or switches to the PR branch.\n- Run `wr.md` only when the user asks to wrap up, commit, or push. Post at most one final GitHub comment per session, ending with the exact disclaimer line it specifies.\n- When a request spans several steps, for example \"fix issue X and wrap it up\", read every template involved before starting.\n",".pi/skills/interactive-testing.md":"---\nname: interactive-testing\ndescription: Test and debug pi's interactive mode in a controlled tmux terminal. Use for TUI behavior checks and interactive release smoke tests.\n---\n\n# Testing pi Interactive Mode with tmux\n\nRun the TUI in a controlled terminal (from the repo root, two directories above this skill):\n\n```bash\ntmux new-session -d -s pi-test -x 80 -y 24\ntmux send-keys -t pi-test \"./pi-test.sh\" Enter\nsleep 3 && tmux capture-pane -t pi-test -p     # capture after startup\ntmux send-keys -t pi-test \"your prompt here\" Enter\ntmux send-keys -t pi-test Escape               # special keys (also C-o for ctrl+o, etc.)\ntmux kill-session -t pi-test\n```\n\nFor release smoke tests, start the tmux session with `-c /tmp` and replace `./pi-test.sh` with the absolute path to the release binary. Test both Node and Bun binaries separately, submit a prompt, and wait for the model reply; startup alone is not a passing smoke test.\n",".pi/skills/release.md":"---\nname: release\ndescription: Prepare, publish, verify, and recover pi releases. Use for release preparation, local release smoke tests, publishing, and failed release CI or announcements.\n---\n\n# Releasing pi\n\nRun repository commands from the repo root (two directories above this skill), unless instructed otherwise.\n\n**Lockstep versioning**: all packages share one version; every release updates all together. `patch` = fixes + additions, `minor` = breaking changes. No major releases.\n\n1. **Update CHANGELOGs**: ask the user whether they ran the `/cl` prompt on the latest commit on `main`. If not, they must run `/cl` first to audit and update each package's `[Unreleased]` section before releasing.\n\n2. **Local smoke test**: build an unpublished release and smoke test from outside the repo (so it can't resolve workspace files):\n   ```bash\n   npm run release:local -- --out /tmp/pi-local-release --force\n   cd /tmp\n\n   # Node package install smoke tests\n   /tmp/pi-local-release/node/pi --help\n   /tmp/pi-local-release/node/pi --version\n   /tmp/pi-local-release/node/pi --list-models\n   /tmp/pi-local-release/node/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/node/pi\n\n   # Bun binary smoke tests\n   /tmp/pi-local-release/bun/pi --help\n   /tmp/pi-local-release/bun/pi --version\n   /tmp/pi-local-release/bun/pi --list-models\n   /tmp/pi-local-release/bun/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/bun/pi\n   ```\n   Verify both Node and Bun startup, model/account listing, interactive startup, and at least one real prompt with the intended default provider. The bare commands `/tmp/pi-local-release/node/pi` and `/tmp/pi-local-release/bun/pi` start interactive mode; run each in tmux, submit a prompt, and wait for the model reply before considering the interactive smoke test passed. Failures are release blockers unless the user explicitly accepts the risk.\n\n   Load and follow [interactive-testing.md](interactive-testing.md) for the tmux workflow. Start each release binary from `/tmp`, not the repo root.\n\n3. **Run the release script**:\n   ```bash\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:patch    # fixes + additions\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:minor    # breaking changes\n   ```\n   If the test suite already passed in `npm run release:local` and only fails from machine load, `PI_RELEASE_SKIP_TESTS=1` skips `./test.sh` in the release script; checks and the packed install check still run. Use it only with the user's approval.\n\n   Use `npm_config_min_release_age=0` only for the release command. The repo's normal npm age gate can otherwise block the release lockfile refresh when the current workspace package version was published recently. Review any lockfile or install lock diffs the release creates before push.\n\n   The release script pins the newest published model catalog for Nix (`nix/model-catalog.json`) and builds the checkout with it as the Nix build does (it fails if that catalog predates main's model changes; publish one first), bumps all package versions, updates changelogs, regenerates release artifacts, runs `npm run check`, commits `Release vX.Y.Z`, tags `vX.Y.Z`, adds fresh `## [Unreleased]` changelog sections, commits `Add [Unreleased] section for next cycle`, then pushes `main` and the tag. Do not rerun the release script after a tag was pushed.\n\n4. **CI verifies and announces the npm release**: pushing the `vX.Y.Z` tag triggers `.github/workflows/build-binaries.yml`. The `publish-npm` job uses npm trusted publishing through GitHub Actions OIDC with environment `npm-publish`; no local `npm publish`, `npm whoami`, OTP, or WebAuthn flow is required. After publishing, `announce-pi-dev-release` verifies every public workspace package resolves at the exact release version and that its npm tarball is available, then writes the verified release marker to R2. `pi.dev/api/latest-version` reads that marker; it must never announce a release from npm before this job succeeds.\n\n5. **CI builds the Nix release**: the tag push also triggers `.github/workflows/nix.yml`, which builds the tagged flake on Linux and macOS and then fast-forwards the `stable` branch to the release commit. `nix run github:earendil-works/pi/stable` runs the latest release. If the Nix build fails, `stable` stays on the previous release; the tag cannot be fixed, so fix `main` and ship the fix in the next release.\n\n6. **If CI publish or announcement fails**: inspect the failed job. The publish helper is idempotent and skips package versions already present on npm; the announcement job rechecks availability before updating the R2 marker. Rerun the failed job or workflow after fixing CI or transient npm issues. Do not rerun `npm run release:patch` or `npm run release:minor` for the same version.\n","AGENTS.md":"# Development Rules\n\n## Conversational Style\n\n- Keep answers short and concise\n- No emojis in commits, issues, PR comments, or code\n- No fluff or cheerful filler text (e.g., \"Thanks @user\" not \"Thanks so much @user!\")\n- Technical prose only, be direct\n- Use concise, clear, simple language. Define unavoidable jargon before using it.\n- Explain non-trivial designs and problems as: problem, concrete example or short trace, then solution. State why the solution is necessary and distinguish it from optional complexity.\n- Prefer concrete behavior and small illustrations over abstract summaries, dense terminology, or unexplained lists of changes.\n- When the user asks a question, answer it first before making edits or running implementation commands.\n- When responding to user feedback or an analysis, explicitly say whether you agree or disagree before saying what you changed.\n\n## Code Quality\n\n- Read files in full before wide-ranging changes, before editing files you have not fully inspected, and when asked to investigate or audit. Do not rely on search snippets for broad changes.\n- No `any` unless absolutely necessary.\n- Inline single-line helpers that have only one call site.\n- Check node_modules for external API types; don't guess.\n- **No inline imports** (`await import()`, `import(\"pkg\").Type`, dynamic type imports). Top-level imports only.\n- In `packages/coding-agent`, resolve package assets through helpers in `src/config.ts`. Do not use `__dirname` directly; the helpers account for source checkouts, npm installations, and standalone binaries.\n- Never remove or downgrade code to fix type errors from outdated deps; upgrade the dep instead.\n- Use only erasable TypeScript syntax (Node strip-only mode) in code checked by the root config (`packages/*/src`, `packages/*/test`, `packages/coding-agent/examples`): no parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other constructs needing JS emit. Use explicit fields with constructor assignments.\n- Always ask before removing functionality or code that appears intentional.\n- Do not preserve backward compatibility unless the user asks for it.\n- Never hardcode key checks (e.g. `matchesKey(keyData, \"ctrl+x\")`). Add defaults to `DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS` so they stay configurable.\n- Never modify `packages/ai/src/models.generated.ts` directly; update `packages/ai/scripts/generate-models.ts` instead, then regenerate. Including the resulting `models.generated.ts` diff is always OK, even if regeneration includes unrelated upstream model metadata changes. The generated model data in `packages/ai/src/providers/data/` is gitignored: never stage it, and don't look for it in git history.\n\n## Commands\n\n- After code changes (not docs): `npm run check` (full output, no tail). Fix all errors, warnings, and infos before committing. Does not run tests.\n- Never run `npm run build` or `npm test` unless requested by the user.\n- Never run the full vitest suite directly: it includes e2e tests that activate when endpoint/auth env vars are present. For all non-e2e tests, run `./test.sh` from the repo root. Otherwise run specific tests from the package root:\n  - Vitest: `node \"$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js\" --run test/specific.test.ts`\n  - `packages/tui` (`node:test`): `node --test test/specific.test.ts`\n- If you create or modify a test file, run it and iterate on test or implementation until it passes.\n- For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` + the faux provider. No real provider APIs, keys, or paid tokens.\n- When regressions tests for fixing a github issue, add a comment with the github issue number next to the test.\n- For ad-hoc scripts, `write` them to a temp file (e.g. `/tmp`), run, edit if needed, remove when done. Don't embed multi-line scripts in `bash` commands.\n- Never commit unless the user asks.\n\n## Dependency and Install Security\n\n- Treat npm dep and lockfile changes as reviewed code. Direct registry deps, including internal workspace deps, stay pinned to exact versions.\n- When updating `undici`, you MUST read its changelog/release notes for the target version and evaluate whether any changes may affect functionality before applying the update.\n- Hydrate/update locally with `npm install --ignore-scripts`; clean/CI-style with `npm ci --ignore-scripts`. Don't run lifecycle scripts unless the user asks.\n- If dep metadata changes, refresh `package-lock.json` with `npm install --package-lock-only --ignore-scripts`.\n- If `packages/coding-agent/install-lock/` needs regen, run `node scripts/generate-coding-agent-install-lock.mjs` (verify with `--check` or `npm run check`). New deps with lifecycle scripts require review and an explicit allowlist entry in that script; never add one silently.\n- Pre-commit blocks lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1`. Don't bypass unless the user wants the lockfile change committed.\n\n## Git\n\nMultiple pi sessions may be running in this cwd at the same time, each modifying different files. Git operations that touch unstaged, staged, or untracked files outside your own changes will stomp on other sessions' work. Follow these rules:\n\nCommitting:\n\n- Only commit files YOU changed in THIS session.\n- Stage explicit paths (`git add <path1> <path2>`); never `git add -A` / `git add .`.\n- Before committing, run `git status` and verify you are only staging your files.\n- `packages/ai/src/models.generated.ts` may always be included alongside your files.\n- Message format: `{feat,fix,docs}[(ai,tui,agent,coding-agent)]: <commit message> (optionally multiple lines)`. Message is informative and concise.\n\nNever run (destroys other agents' work or bypasses checks):\n\n- `git reset --hard`, `git checkout .`, `git clean -fd`, `git stash`, `git add -A`, `git add .`, `git commit --no-verify`.\n\nIf rebase conflicts occur:\n\n- Resolve conflicts only in files you modified.\n- If a conflict is in a file you did not modify, abort and ask the user.\n- Never force push.\n\n## Issues and PRs\n\nSee `CONTRIBUTING.md` for the contributor gate (auto-close workflows, `lgtm`/`lgtmi`, quality bar).\n\nWhen reviewing PRs:\n\n- Do not run `gh pr checkout`, `git switch`, or otherwise move the worktree to the PR branch unless the user explicitly asks.\n- Use `gh pr view`, `gh pr diff`, `gh api`, and local `git show`/`git diff` against fetched refs to inspect PR metadata, commits, and patches without changing branches.\n- If you need PR file contents, fetch/read them into temporary files or use `git show <ref>:<path>` without switching branches.\n\nWhen creating issues:\n\n- Add `pkg:*` labels for affected packages (`pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui`); use all that apply.\n\nWhen posting issue/PR comments:\n\n- Write the comment to a temp file and post with `gh issue/pr comment --body-file` (never multi-line markdown via `--body`).\n- Keep comments concise, technical, in the user's tone.\n- End every AI-posted comment with the AI-generated disclaimer line specified by the originating prompt (e.g. `This comment is AI-generated by `/wr``).\n\nWhen closing issues via commit:\n\n- Include `fixes #<number>` or `closes #<number>` in the message so merging auto-closes the issue. For multiple issues, repeat the keyword per issue (`closes #1, closes #2`); a shared keyword (`closes #1, #2`) only closes the first.\n\n## Testing pi Interactive Mode with tmux\n\nFor testing pi's interactive mode, load and follow [.pi/skills/interactive-testing.md](.pi/skills/interactive-testing.md).\n\n## Changelog\n\nLocation: `packages/*/CHANGELOG.md` (one per package).\n\nSections under `## [Unreleased]`: `### Breaking Changes` (API changes requiring migration), `### Added`, `### Changed`, `### Fixed`, `### Removed`.\n\nRules:\n\n- All new entries go under `## [Unreleased]`. Read the full section first and append to existing subsections; never duplicate them.\n- Released version sections (e.g. `## [0.12.2]`) are immutable; never modify them.\n- Do not create changelog entries when working on a branch other than `main` or pull request\n\nAttribution:\n\n- Internal (from issues): `Fixed foo bar ([#123](https://github.com/earendil-works/pi/issues/123))`\n- External contributions: `Added feature X ([#456](https://github.com/earendil-works/pi/pull/456) by [@username](https://github.com/username))`\n\n## Releasing\n\nFor release preparation, publishing, verification, or recovery, load and follow [.pi/skills/release.md](.pi/skills/release.md).\n\n## User Override\n\nIf the user's instructions conflict with any rule in this document, ask for explicit confirmation before overriding. Only then execute their instructions.\n","packages/agent/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fagent%2FREADME.md -->","packages/ai/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fai%2FREADME.md -->","packages/chord/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fchord%2FREADME.md -->","packages/chord/docs/guide.md":"# Chord usage guide\n\n> Describes the runtime in `src`. The [specification](spec.md) defines its semantics and lists known limitations.\n\nChord builds applications out of **objects** that live in several processes (a server, workers, browser tabs) and can be **hot reloaded** while they run.\n\n- An **object type** is declared by a contract: a name, an input (the object's address) and members. A plugin's\n  **facet** (its code for one host: server, worker, browser) serves object types.\n- An **object** is one instance of an object type: state plus code at an address. It is created on demand and lives\n  while someone references it.\n- Everyone gets a **reference**, never the object. References work the same locally and across processes, and survive reloads.\n- Every object knows its **principal**: an owner and a guest get different objects, with different methods.\n- **State** is observable JSON. Remote viewers get a snapshot followed by deltas.\n- **Hot reload** swaps code while it runs. Work in flight finishes on the code it started with, state is kept, and incompatible changes migrate at a safe point.\n\n## Contents\n\n- [Quick start](#quick-start)\n- [Contracts](#contracts)\n- [Serving objects](#serving-objects)\n- [Declarations](#declarations)\n- [Activation](#activation)\n- [Principals and variants](#principals-and-variants)\n- [References](#references)\n- [State](#state)\n- [Calls, ctx and roots](#calls-ctx-and-roots)\n- [Lifetime](#lifetime)\n- [Registries](#registries)\n- [Plugins](#plugins)\n- [Hot reload](#hot-reload)\n- [Hosts and links](#hosts-and-links)\n- [Durable](#durable)\n- [Errors](#errors)\n- [Rules of thumb](#rules-of-thumb)\n\n## Quick start\n\n```ts\nimport { Type } from \"typebox\";\nimport { createHost, method, remote } from \"@earendil-works/chord\";\n\n// contract.ts: shared by provider and consumers\nexport const Counter = remote(\"demo.counter\", {\n\tversion: 1,\n\tinput: Type.Object({ name: Type.String() }),\n\tvariants: {\n\t\tdefault: {\n\t\t\tstate: { count: Type.Number() },\n\t\t\tmethods: { increment: method(Type.Object({ by: Type.Number() })) },\n\t\t},\n\t},\n});\n\nconst host = createHost({ name: \"server\", self: { kind: \"server\" } });\n\nhost.serve(Counter, ({ name }, _principal, self) => {\n\tconst count = self.state(\"count\", Type.Number(), 0);\n\treturn {\n\t\tvariant: \"default\",\n\t\tcount,\n\t\tincrement: async ({ by }, ctx) => {\n\t\t\tcount.change(ctx, (n) => n + by);\n\t\t},\n\t};\n});\n\nawait host.root(async (ctx) => {\n\tconst clicks = await ctx.get(Counter, { name: \"clicks\" });  // ready, with its state; released when this root ends\n\tclicks.count.subscribe((n) => console.log(\"count\", n));\n\tawait clicks.increment({ by: 2 }, ctx);  // logs 0, then 2\n});\n```\n\n## Contracts\n\nA contract is a runtime value: a name, a version number, and a description of the members. Shipped bundles have no TypeScript, so everything the runtime checks must be in the contract. TypeScript types are inferred from it.\n\n### Remote types\n\nRemote types can be reached from other hosts. Everything that crosses the wire has a schema, and the provider validates it.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),  // lives on `session:S1` for `{ session: \"S1\", … }`\n\tvariants: {\n\t\tviewer: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t},\n\t\tparticipant: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t\tmethods: {\n\t\t\t\tsend: method(SendInputSchema, Type.Object({ submissionId: Type.Number() })),\n\t\t\t\tstop: method(),  // no arguments, no result: `chat.stop(ctx)`\n\t\t\t},\n\t\t},\n\t},\n});\n```\n\n**Where a remote type lives** is part of its contract, so no caller names a host:\n\n- `host: hostNamed(\"server\")`: every object lives on the host named `server`.\n- `host: hostFamily(\"session:\", \"session\")`: an object lives on `session:` + its input's `session` field, a required\n  string. TypeScript checks the field name against the input.\n\nA `get` normalizes the input (defaults count), resolves the host, and acquires the object here if that is this host,\notherwise over the link to it. `serve` refuses a type whose placement does not include the serving host, so a misplaced\ntype fails at startup or at the reload that adds it. The host that gets a hold checks that its own contract places the\ninput on itself, and a relay checks the hold's target against its contract token: a client never chooses where an\nobject lives. The placement is part of the description and the fingerprint: moving a type is a breaking change, and\nreferences to it get `upgrade_required`.\n\n### Local types\n\nLocal types are reachable only inside their host. Their members may be anything: functions, callbacks, native objects. Non-JSON members get opaque or function descriptors.\n\n```ts\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(HitSchema)),\n\t\tonChange: method(Type.Function([ChangeSchema], Type.Void()), UnsubscribeSchema),\n\t\tharness: opaque(\"pi.durable-harness@1\"),\n\t},\n});\n```\n\n### Inputs identify objects\n\n`{ session: \"S1\", conversation: 1 }` always means the same object. Inputs are normalized (defaults applied, unknown fields removed), validated, and serialized canonically. Fields equal to their default are left out, so adding a new optional field with a default doesn't change existing addresses. Changing a default, or how inputs are normalized, needs a new type name.\n\n### Versions and compatibility\n\nEvery bundle carries its own copy of each contract it was built against. When a consumer binds to a provider, the two copies are compared:\n\n> A consumer built against version M binds to a provider at version N if **N = M and the provider's description covers the consumer's**. Otherwise the provider must offer an adapter in `accepts[M]`, or binding fails with `contract_mismatch`.\n\n\"Covers\" means the provider still offers everything the consumer was built against:\n- every member exists with the same kind;\n- methods accept everything the consumer can send (new arguments must be optional) and return at least what it expects (extra result fields are fine);\n- state values fit the consumer's schema;\n- opaque members have the same name and version.\n\n| Change | Version | Result |\n| --- | --- | --- |\n| Add a method, an optional argument, a result field, a state field | keep | binds |\n| Remove or rename a member; make an argument required; change a type | **bump** | old consumers need `accepts`, or must be reloaded too |\n| Same shape, different meaning (`query` is now a regular expression) | **bump** | as above; undetectable, so it's on you |\n| Forgot to bump on a breaking shape change | keep | refused: \"consumer needs member `search`\" |\n\n```ts\n// version 2 changed what `search` means; old consumers keep working through an adapter\nhost.serve(Index, indexFactory, {\n\taccepts: {\n\t\t1: (current) => ({ search: ({ query }, ctx) => current.search({ query: escapeRegExp(query) }, ctx) }),\n\t},\n});\n```\n\n## Serving objects\n\n```ts\nhost.serve(Type, (input, principal, self) => implementation, options);\n```\n\n- The **factory** runs synchronously. It declares what the object needs and returns its implementation: methods and state members, plus `variant` for remote types. Returning a promise throws.\n- There is one **object** per `(type, input, principal)`. It holds the object's state, resources and references, and survives reloads. Only the implementation is replaced.\n- **Options:**\n  - `accepts`: adapters for consumers built against older contract versions;\n  - `lifetime`: `\"referenced\"` (default) creates the object at its first `get` and disposes it after the last reference\n    is released; `\"host\"` creates it when the plugin loads and keeps it until the host closes (see [Lifetime](#lifetime));\n  - `linger`: how long to keep a referenced object after the last reference is released.\n\n## Declarations\n\nA reload runs the factory again over the same object. Declarations that keep something across a reload have a **name**: the new code's `status` cell is the old code's `status` cell, with its value. Derivations and tasks hold nothing across a reload and have no name.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 });\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async ({ signal }) => openSqlite(statusPath(session), { signal }),\n\t\tclose: async (db) => db.close(),\n\t});\n\tself.on(\"catalogChanges\", catalog.rows, (rows, ctx) => errors.replace(ctx, checkRows(rows)));\n\tself.task(async (ctx) => {\n\t\twhile (!ctx.signal.aborted) {\n\t\t\tawait sleep(10_000, ctx.signal);\n\t\t\tawait db.value.run(\"INSERT INTO history VALUES (?)\", [JSON.stringify(status.value)]);\n\t\t}\n\t});\n\tconst summary = self.derive([status, errors], (s, e) => ({ ...s, failing: e.length > 0 }));\n\treturn { summary, report: reportMethod(status) };  // exposed state is what the implementation returns\n});\n```\n\n| Declaration | What it is | On reload |\n| --- | --- | --- |\n| `self.state(name, schema, initial \\| { init }, { version?, migrate? })` | an observable JSON cell; the schema is required | same cell and value (`init` does not run); migrated when its version rises |\n| `self.get(Type, input, { at? })` | a reference to another object; its identity is the address | kept if declared again, otherwise released |\n| `self.resource(name, { version, deps?, exclusive?, open, close? })` | a connection, watch, socket, port | kept; reopened only if `version`, `deps` or `exclusive` change |\n| `self.on(name, source, handler)` or `self.on(name, source, select, handler)` | reacts to state or to an event source | the same subscription, with the new handler, as long as the source and `select` are unchanged |\n| `self.task(async (ctx) => ...)` | long-running background work | the old run is aborted through `ctx.signal`; the new code starts once it finished (at most `taskStopDeadline`, default 5 s) |\n| `self.derive(sources, fn)` | live projection of state; `fn` is synchronous | a new derivation, computed when the reload commits |\n| `self.register(Registry, { key: value })` | an entry in a registry | replaced at commit |\n| `self.expose(resource)` | a member backed by a resource (local types) | follows the resource |\n\nNames are strings: declaring one twice in a run fails (`invalid/duplicate`). Each kind has its own names, so a cell and a resource may share one.\n\nA listener keeps its subscription across reloads when it is declared on the same resource (or source) with the same `select`: the same function, or one with the same source text. `select` is not called again, so a source that accepts a single subscriber (a Durable watch) survives reloads, and `select` may wrap the value in a new adapter object. A `select` whose result depends on captured values that change between reloads is not noticed; give such a listener a new name.\n\n**A listener on state reconciles.** A listener on state (a cell, a derivation, a reference's state member) gets the current value when it starts, then every new value. When a reload replaces its source (a derivation is new at every reload), it subscribes again and gets the current value again. Write its handler to bring things in line with the value it gets (set, compare, start or stop what the value says), not to count or append per call. An event source (a resource's events) delivers no current value.\n\nA name you no longer declare is released, closed or stopped, after the old code has finished what it was doing.\n\n**State that must survive a reload lives in cells,** never in closure variables. A closure variable lasts as long as one version of the code.\n\n```ts\n// wrong: reset on every reload\nhost.serve(Visits, (_, _p, self) => {\n\tlet visits = 0;\n\treturn { hit: async (ctx) => ++visits };\n});\n\n// right\nhost.serve(Visits, (_, _p, self) => {\n\tconst visits = self.state(\"visits\", Type.Number(), 0);\n\treturn {\n\t\thit: async (ctx) => {\n\t\t\tvisits.change(ctx, (n) => n + 1);\n\t\t\treturn visits.value;\n\t\t},\n\t};\n});\n```\n\n**Methods** are declared with `method(args?, result?)` in the contract. Without `args` a method takes none: callers write `ref.hit(ctx)` (or `ref.hit()`), and its implementation is `(ctx) => …`. Without `result` it returns nothing: the implementation returns nothing, and callers get `undefined`. `method(undefined, Type.Number())` takes no arguments and returns a number.\n\n## Activation\n\nThe factory is synchronous, but activation isn't. `self.get` and `self.resources` only *declare*; the slow work happens afterwards:\n\n```text\nget(Index, { session: \"S1\" })\n 1. run the factory               → declarations\n 2. open declared resources        → e.g. connect to SQLite\n 3. wait for declared refs        → dependencies activate first\n 4. run the cells' init           → e.g. read the rows into a cell\n 5. live                           → refs become \"live\"; listeners and tasks start\n```\n\n```ts\nhost.serve(Index, ({ session }, _host, self) => {\n\tconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(indexPath(session)), close: (db) => db.close() });\n\treturn {\n\t\tsearch: async ({ query }, ctx) => db.value.all(\"SELECT entryId, text FROM fts WHERE fts MATCH ?\", [query]),\n\t};\n});\n\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tconst index = self.get(Index, { session });  // a placeholder until Index is live\n\treturn {\n\t\tvariant: \"participant\",\n\t\tfind: async ({ query }, ctx) => index.search({ query }, ctx),  // only ever runs once Chat (and Index) are live\n\t\t// ...\n\t};\n});\n```\n\n**Rules:**\n- Inside the factory, never use what you declared: `db.value` and `index.someState.value` throw `not_ready`, and calling a reference throws. Use them in methods, listeners, tasks and `init`.\n- **The object goes live only with its references ready**: live, and (for a remote reference) every state member of its variant hydrated. The same holds for state it forwards or derives from a remote reference it holds some other way: it goes live once that state has its value. A reference that is not ready after the host's `readyTimeout` (default 60 s) fails the activation with `unavailable/ready_timeout`; a reference cycle across hosts ends that way instead of hanging.\n- **Concurrent `get`s** of one address share one activation, so the database opens once.\n- **Failure propagates:** if `openSqlite` throws, `Index` fails, `Chat` fails with that cause, and every waiting `get` rejects. The next `get` tries again.\n- **Abandoned activations:** if everyone releases before activation finishes, the result is disposed.\n\n### Startup readiness and parent admission\n\n`await host.load(facets)` and `await host.install(facet)` wait for the `lifetime: \"host\"` objects they install to\nbecome live: resources are open, declared references are live, and registrations are published. All facets in a load\nare installed before activation is awaited, so a host object can use a provider in a later facet. Referenced objects\nare created only when something references them. Factory, dependency and resource errors reject startup with the\nreference's activation error; host object failures also reach `onError`. Installation is not rolled back on failure: close the host\nwhen abandoning startup. `install` registers synchronously, so setup/registration errors still throw synchronously.\n\n`await host.ready()` waits for the host objects present when called, including direct\n`host.serve(..., { lifetime: \"host\" })` and previously deferred loads. It is an activation barrier, not a continuous health check, a wait for tasks to\nfinish. Later installations require another readiness wait.\n\nA worker may need to open its local Harness and announce control readiness **before** the parent accepts its link.\nA host object reporting to the server's SessionStatus cannot become live before that acceptance. Awaiting the\nwhole load before announcing control readiness would deadlock. Defer the whole barrier explicitly:\n\n```ts\n// Connect the upstream link first; its outgoing references can queue until the parent accepts it.\nawait worker.load(workerFacets, { awaitHostObjects: false });  // also supported by install\nconst harness = await worker.get(Harness, {});        // local bootstrap only; no upstream dependency\nannounceControlReady();                               // parent can now accept the worker link\nawait worker.ready();                                 // every host object live, including upstream references\nharness.release();\n```\n\n`awaitHostObjects: false` still starts every host object; it skips waiting, not activation. There is no\nautomatic classification of upstream dependencies. Choose a local bootstrap reference with no upstream dependency,\nconnect before loading upstream consumers, and let the parent admit the link before the full barrier. Control\nreadiness means admission is possible, not that every host object is live. Keep worker bootstrap counted as busy\nuntil the full barrier settles, and report a later failure through the worker's fatal/startup-error path. Never await a\nreference whose object needs the parent (a remote one, or a local one that holds one) before the parent admits the link.\n\nTo hand out a native object a resource opens (a local type's `opaque` member), expose the resource. References read its value once the object is live, and `undefined` before:\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", { version: 1, open: () => DurableHarness.open(storage), close: (h) => h.close() });\n\treturn { harness: self.expose(durable) };  // contract member: harness: opaque<DurableHarness>(\"pi.durable-harness@1\")\n});\n\nconst harness = await ctx.get(Harness, {});\nharness.harness.resume();  // the DurableHarness itself\n```\n\n## Principals and variants\n\nThe application defines its principals and how they're keyed. Chord invents none: the app gives every principal a\nrequest can act as.\n\n```ts\ntype Principal =\n\t| { kind: \"owner\"; access: \"local\" | \"relay\" }\n\t| { kind: \"guest\"; id: string; name: string; session: string; mode: \"view\" | \"full\"; revision: number }\n\t| { kind: \"worker\"; session: string; generation: number }\n\t| { kind: \"server\" }   // the parent's own requests, as a worker sees them\n\t| { kind: \"host\" };    // a host's own code\n\nconst host = createHost<Principal>({\n\tname: \"server\",\n\tself: { kind: \"host\" },\n\tprincipalKey: (p) =>\n\t\tp.kind === \"owner\" ? `owner:${p.access}` :\n\t\tp.kind === \"guest\" ? `guest:${p.id}:r${p.revision}` :\n\t\tp.kind === \"worker\" ? `worker:${p.session}:${p.generation}` :\n\t\tp.kind,\n});\n```\n\n- `createHost<Principal>` types every factory's `principal`, `ctx.principal`, routes, `accept` and\n  `invalidatePrincipal` patterns, so code narrows on `kind` without casts. Facet modules say it too:\n  `defineFacet<Principal>((api) => …)` (an app usually exports its own `defineFacet` and `Ctx` aliases). Without a type\n  argument, principals are `unknown`.\n- There is one object per principal key: two tabs of the owner share an object, and a guest gets its own.\n- Code inside a host acts as `self`, so internal objects are shared. A principal with the same key as `self` is the\n  host's own, whichever object carries it.\n- A link stamps the principal of whatever connects to it. Callers can never supply their own.\n- A child host names what its parent's own requests act as: `connect(link, { principal })` (or\n  `upstream: { link, principal }`). Requests the parent relays act as the caller it forwards.\n- A route names what the hosts it resolves act as when they call back: `route(prefix, { …, principal: (name) => … })`.\n\n**A remote factory picks a variant:**\n\n```ts\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tif (!canView(principal, session)) throw forbidden();\n\tconst c = self.get(Conversation, { session, conversation });  // as host: one shared instance\n\tif (!canSend(principal)) return { variant: \"viewer\", view: c.view };\n\tconst author = authorOf(principal);\n\treturn {\n\t\tvariant: \"participant\",\n\t\tview: c.view,\n\t\tsend: async (input, ctx) => c.submit(input, author, ctx),\n\t\tstop: async (ctx) => {\n\t\t\tawait c.stop(author, ctx);\n\t\t},\n\t};\n});\n```\n\n```ts\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // on session:S1\nif (chat.variant === \"participant\") showComposer(chat.send);  // narrows: `send` exists only there\nelse showReadOnly();\n```\n\n**What a variant doesn't do:**\n- **It isn't complete authorization.** Methods still check their arguments: that a submission belongs to this conversation, or a session ID to this principal.\n- **It doesn't cover flags that change while you hold the object.** Those are state, for example `you: { canDraw: boolean }`.\n- **Revocation** is `host.invalidatePrincipal(match)`. It force-disposes matching objects, here and over links, and their references become `gone`. A new `get` runs the factory with the new grants.\n\nLocal types have no variants. Their only callers are inside the host, acting as its `self`. If a local object needs to know who's acting, pass that explicitly, like `author` above.\n\n## References\n\n| Where | API | Released |\n| --- | --- | --- |\n| a factory | `self.get(Type, input)`: synchronous; the object goes live after it is ready | with the object (or its replaced declaration) |\n| a call | `await ctx.get(Type, input, { state? })` | when the call settles |\n| outside Chord | `await host.get(Type, input, { signal?, state? })` | `ref.release()` |\n| a UI runtime | `host.hold(Type, input)`: synchronous, possibly `loading` | `ref.release()` |\n\n```ts\nconst chat = await ctx.get(Chat, input);  // ready: live, every state member of its variant hydrated\nchat.status;  // \"loading\" | \"live\" | \"released\" | \"gone\"\nchat.variant;  // the current variant; a multi-variant reference narrows on it\nchat.shape;  // { variant, members, contract }\nconst send = chat.send;  // a stable function: always dispatches to the current code\nchat.view.value;  // the current value, typed T\nchat.view.subscribe((v) => render(v));\nchat.onStatus((status, error) => { if (status === \"gone\") reconnect(error); });\nchat.release();\n```\n\n- **A reference you hold is usable:** an awaited `get` resolves once the object is live and every state member of its variant has its first snapshot, local or remote. It rejects with the activation error (`forbidden`, unreachable, …), with `aborted` (a `ctx.get` whose ctx is aborted or whose call settles first; `host.get`'s `signal`), or with `unavailable/ready_timeout` after the host's `readyTimeout`. Only that reference is released then.\n- **A held reference stays subscribed** to the state of its variant, with or without listeners, until it is released. After a link loss the last values stay readable while it re-holds (status `loading`).\n- **Reading state before the first hydration throws `not_ready`** (a `hold` that is still loading, a member read inside a factory). A member outside the current variant throws `not_in_variant`; an ended reference throws its end (`released`, or the `gone` error). A listener added before the hydration gets its snapshot then.\n- **`state: false`** gives a method-only reference: it receives no state (unless another reference to the same object wants it) and is ready once its object is live.\n- **Calls on a `loading` reference wait** until it's live, respecting `ctx` cancellation and a deadline.\n- A member missing from the current variant rejects with `not_in_variant`.\n- After `release()`, every member rejects with `released`.\n- **`gone`** means the object was forced away while referenced: revocation, archive, its host dying. Call `get` again, or show \"unavailable\". `onStatus` reports every status change (`live`, `loading` while a link reconnects, `released`, `gone` with the error), so a UI needn't poll.\n- **References to one remote object share one binding** per host and key (type, normalized input, principal key, contract fingerprint): one hold over the link, one stream per state member, calls over the same hold. Each reference keeps its own status, observers and `release`. With `createHost({ linger: { types: { [typeName]: ms }, cap } })`, a binding whose last reference is released stays held (and hydrated) for `ms`, at most `cap` (default 4) at a time, so taking the reference again costs nothing on the link.\n- **Return values are values.** Anything long-lived you want to share is an object you reach with `get`.\n\n## State\n\n```ts\nconst todos = self.state(\"todos\", Type.Array(TodoSchema), []);\n\ntodos.change(ctx, (draft) => {\n\tdraft.push({ id: \"t1\", text: \"Write docs\", done: false });\n});\ntodos.change(ctx, (draft) => {\n\tdraft[0].done = true;\n});\ntodos.replace(ctx, []);\n\ntodos.value;  // immutable\ntodos.subscribe((value) => render(value));\n\n// mirroring an external immutable store: subscribers get a diff, not the whole value\nview.replace(ctx, conversation.view);                    // diffs when the new value reuses old containers\nview.replace(ctx, rebuilt, { diff: true });              // always diff\n```\n\n- A `change` is atomic: one revision, or nothing if the callback throws.\n- `change` only touches the paths you mutate, so fields your code doesn't know about survive. Use `replace` only where your code owns the whole value.\n- Values are immutable. Never mutate what you read or receive.\n- Remote references get a snapshot of each state member of their variant, then deltas. After a reconnect or a reload they get a `reset` snapshot, and nothing is replayed.\n- `replace` sends a structural diff when the new value shares containers with the old one (a new revision of the same state), and a single replacement otherwise. `{ diff: true | false }` forces either. The value is the same either way.\n- An implementation may expose another object's state member as its own: `return { variant: \"viewer\", view: conversation.view }`. The object goes live only once that member has its value, local or remote.\n\n**A cell whose first value comes from a source** declares `{ init }` instead of an initial value:\n\n```ts\nconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(path), close: (db) => db.close() });\nconst notes = self.state(\"notes\", Type.Array(NoteSchema), { init: () => db.value.all(\"SELECT * FROM notes\") });\n```\n\n`init(ctx)` runs once the resources are open and the references live, before the object goes live, so no method,\nlistener, task or subscriber sees a value that did not come from the source. A task that loads the value instead\nstarts only after the object is live: a method called in between answers from the placeholder (a note that exists\nreads as unknown), and an async load can overwrite a write made during its `await`. `init` may be async and gets a\n`ctx` to call other objects. The `init`s of one object run in parallel, so one must not read another's cell. A throw or\nan invalid value fails the activation. A reload keeps the cell and its value and does not run `init` again; a cell the\nreload adds runs its `init` while the reload prepares, and a failure refuses the reload.\n\n`init` reads a source the object has anyway. It saves nothing: what must outlive the process is written to that source\n(a database, a file, a Durable document) by the code that changes it.\n\n```ts\nconst visible = self.derive([catalog.rows], (rows) => rows.filter((r) => visibleTo(principal, r)));\nconst openCount = self.derive([todos], (t) => t.filter((x) => !x.done).length);\n```\n\nDerivation functions are synchronous. A derivation has no name: every reload makes a new one, computed when the\nreload commits.\n\n## Calls, ctx and roots\n\nEvery method gets `(args, ctx)`, or `(ctx)` when it takes no arguments. Pass `ctx` on to everything you call:\n\n```ts\nsend: async (input, ctx) => {\n\tctx.signal.throwIfAborted();\n\tconst uploads = await ctx.get(Uploads, {});  // a reference owned by this call; released when it settles\n\tconst files = await uploads.resolve({ ids: input.uploads }, ctx);\n\treturn c.submit({ ...input, files }, author, ctx);\n},\n```\n\nA **root** is a short tree of calls, started at ingress:\n- by a link, for a request that arrives without one;\n- by `host.root(fn)`, for timers and scripts;\n- by each event delivered to a listener (`self.on`);\n- by each Chord call made from a `task` or a Durable phase.\n\nA root runs on the code that was current when it started (its **code version**), across hosts, until it ends. Keep roots short. Long-running work is a `task`, and long waits are state.\n\n```ts\n// wrong: holds a root (and its version of the code) open for minutes\napprove: async (ctx) => waitForHuman(ctx),\n\n// right: record the request as state; the answer is a separate call\nrequest: async (req, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\td.push(req);\n\t});\n},\nanswer: async ({ id, value }, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\tsettle(d, id, value);\n\t});\n},\n```\n\n## Lifetime\n\nAn object lives while it's **referenced**, and every reference has an owner:\n\n| Owner | Acquired with | Released when |\n| --- | --- | --- |\n| another object | `self.get` | that object is disposed, or no longer declares it |\n| a call | `ctx.get` | the call settles |\n| a connection | a remote host's `get` | release, disconnect, revocation |\n| the host | `lifetime: \"host\"` | host shutdown (or a reload removing its facet) |\n| UI | `useObject` | unmount |\n\n- **Disposal** runs after the last reference is released, plus `linger`. Tasks are aborted, listeners removed, resources closed in reverse order (every close runs, and errors are collected), then the object's own references are released, which can dispose further objects.\n- **A resource's `close` may still use the objects its object references.** That holds for host shutdown and revocation too: `host.close()` and `invalidatePrincipal` dispose referrers before what they reference, and close links last. Bound remote calls in `close` with a signal: a call over a link that is already down waits for a reconnect.\n- **Forced disposal** (revocation, archive) makes references `gone` at once and aborts in-flight calls.\n- **References must not form cycles.** Inside one host this is checked at `get`.\n\n```ts\nhost.serve(Conversation, conversationFactory, { linger: 30_000 });  // switching back quickly doesn't reopen its watch\nhost.serve(Index, indexFactory, { lifetime: \"host\" });              // one object from load to host shutdown\n```\n\n**`lifetime: \"host\"`** is for local types with an empty input (or only defaults): the type has exactly one object,\ncreated for the host principal when its facet loads. Every `get` reaches that object, whoever asks; a `get` with\nanother input is invalid. References others hold do not change its lifetime, and it cannot `linger`. Only such\nobjects may register into registries. TypeScript refuses `lifetime: \"host\"` for a remote type, for an input with a\nrequired field and together with `linger`; `serve` checks the same at runtime, for callers without types.\n\n## Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\n\n// contribute (only from `lifetime: \"host\"` objects)\nhost.serve(Downloads, (_, _host, self) => {\n\tconst blobs = self.get(Blobs, {});\n\tself.register(Routes, {\n\t\t\"GET /download\": async (request, who, ctx) => {\n\t\t\tconst file = await blobs.find({ id: request.query.id }, ctx);\n\t\t\tif (!canView(who, file.session)) return forbidden();\n\t\t\treturn stream(file, ctx);\n\t\t},\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n\n// consume: a request sees the table of its own code version\nserver.on(\"request\", (req, res) =>\n\thost.root(async (ctx) => {\n\t\tconst route = host.registry(Routes).get(`${req.method} ${pathOf(req)}`, ctx);\n\t\treturn route ? route(req, principalOf(req), ctx) : notFound(res);\n\t}),\n);\n```\n\n- Each code version has one complete table. A reload never leaves a moment where an entry is missing.\n- A request that started before a reload keeps using the old table.\n- Order is first-registration order and survives replacement.\n- `conflict: \"error\"` makes a duplicate key fail the registration (or the reload).\n\n## Plugins\n\nA plugin is a feature with one **facet** per host kind:\n\n```text\nplugins/chat/\n  contract.ts    Chat, Conversation, Drafts\n  server.ts      facet for the server\n  worker.ts      facet for session workers\n  web.tsx        facet for browsers\n```\n\n```ts\n// plugins/chat/worker.ts\nimport { defineFacet } from \"@earendil-works/chord\";\nimport { Chat, Conversation } from \"./contract.ts\";\n\nexport default defineFacet((host) => {\n\thost.serve(Conversation, conversationFactory, { linger: 30_000 });\n\thost.serve(Chat, chatFactory);\n});\n```\n\n```ts\n// a host loads the plugins' facets for its kind\nawait host.load([\n\tnew URL(\"../plugins/chat/worker.ts\", import.meta.url),\n\tnew URL(\"../plugins/status/worker.ts\", import.meta.url),\n]);\n```\n\nBuilt-in plugins are trusted code from this repository and load directly from source.\n\nThe facet's `setup` argument is its host, scoped to what a plugin needs: `serve`, `name`, `root(fn)`, `registry(R)` and `signal`, which aborts once this run of the facet is no longer installed (a reload replaced or removed it, or the reload that loaded it aborted). Code that is not a Chord method, listener or task (Durable tools and hooks, callbacks of native libraries) keeps it to start roots and read registries, instead of reaching for a process-global host:\n\n```ts\nexport default defineFacet((host) => {\n\thost.serve(Sessions, sessionsFactory);\n\ttools.register(\"rename_session\", (args) =>\n\t\thost.root(async (ctx) => (await ctx.get(Sessions, {})).rename(args, ctx)),\n\t);\n});\n```\n\n## Hot reload\n\n```ts\nawait host.reload([new URL(\"../plugins/chat/worker.ts\", import.meta.url)]);  // or reload every changed plugin\n```\n\n### What happens\n\n1. **Load** the new modules.\n2. **Prepare:** run the new factories over the existing objects, without side effects. Cell writes throw, listeners and tasks don't start, registrations are collected, and changed resources are opened next to the old ones. Contracts are checked against every live consumer. Any failure leaves everything as it was.\n3. **Gate:** only if a cell's version was raised or an exclusive resource changes.\n4. **Commit:** in one synchronous step, install the new code, swap registry tables, restart tasks, rebind listeners, and send `reset` and shape updates to subscribers.\n5. **Retire:** old code keeps running the roots it started. Once they end, it's dropped, along with whatever it alone declared. If they are still running when the next reload starts, that reload waits up to `retireDeadline`, then aborts them (their `ctx.signal`) before it closes what only the old code used.\n\n### Compatible changes: no waiting\n\nBug fixes, new methods, new cells, new resources and registration changes all switch immediately. A root that started on the old code finishes on it, and new roots get the new code.\n\n### Changing what state looks like or means: bump the cell's version\n\n```ts\n// v1: { running, tasks } where tasks counted phases\n// v2: tasks counts tasks, and usage is new\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusV2, { running: false, tasks: 0, usage: emptyUsage() }, {\n\t\tversion: 2,\n\t\tmigrate: (old, from) => (from < 2 ? { running: old.running, tasks: 0, usage: emptyUsage() } : old),\n\t});\n\treturn {\n\t\tstatus,\n\t\treport: async (s, ctx) => {\n\t\t\tstatus.replace(ctx, s);\n\t\t},\n\t};\n});\n```\n\nAt the gate, for each live `StatusStore`:\n1. **Roots already inside finish.** Their re-entrant calls are let in.\n2. **Old roots that haven't touched it yet get `upgrade_conflict`** instead of old code over migrated state. They retry as a new root.\n3. **`migrate` runs** on a private copy, and the result is validated against the new schema.\n4. **Commit.** New roots use the new code and the migrated state.\n\nIf the roots inside don't finish within the deadline (5 s), the reload is **aborted** and nothing changes. The error names the blocking roots.\n\nThe version belongs to the cell (default 1); other cells of the object are not migrated. The runtime compares each\ncell's schema and version between old and new code:\n- unchanged: compatible;\n- version raised: migrate (without `migrate`, the value is kept and must fit the new schema);\n- schema changed **without** a raised version, or a lowered version: the reload is refused.\n\nA change of meaning with an identical schema needs a bump too. Nothing can detect that one for you.\n\n### Resources\n\n```ts\nconst db = self.resource(\"db\", { version: 2, open: () => openSqlite(path, { wal: true }), close: (db) => db.close() });  // 1 → 2: reopen\nconst port = self.resource(\"port\", { version: 1, exclusive: true, open: () => listen(8787), close: (s) => s.close() });\n```\n\n- A reload keeps a resource's open value unless its `version`, `deps` or `exclusive` change: `open` doesn't run\n  again. The version is required: a native value has no schema to detect a change. The kept resource still takes the\n  new code's declaration at the commit, so a later `close` is the new one.\n- A changed resource opens next to the old one during prepare. The old one is closed with its own `close` once the\n  old code retires.\n- An `exclusive` resource can't exist twice, so it's swapped at the gate: close the old one, open the new one, and reopen the old one if that fails.\n\n### Contract changes\n\n| Change | What happens |\n| --- | --- |\n| Additive, same version | reloads; consumers keep working |\n| Breaking, version bumped, with `accepts[old]` | reloads; old consumers go through the adapter |\n| Breaking, without an adapter | refused, unless the consumers reload in the same batch |\n| Breaking, version not bumped | refused: \"consumer needs member …\" |\n\nThe host's own **reference to a `lifetime: \"host\"` object** is a lifecycle pin, not an old-version consumer. When a reload replaces\nits provider, that reference adopts the new contract at commit; it does not require an `accepts` adapter for itself.\nThe object, cells and unchanged resources stay alive without reactivation. A refused reload keeps the original\nreference and contract. Other references (`self.get`, `ctx.get`, `host.get`, peers) still obey the compatibility rules above;\nstate-version bumps and migrations are still required when stored state changes.\n\n### Several plugins at once\n\nReload them together: `host.reload([a, b])`. Prepare sees all new code at once, so a changed contract and its updated consumer check against each other.\n\nOn the server, `host.reload(serverFacets, { remote: { \"session:\": workerFacets } })` reloads every connected worker too. Its report covers the server; `report.children[\"session:S1\"]` says what each worker changed, prepared and gated.\n\n## Hosts and links\n\n```ts\n// server\nconst server = createHost<Principal>({ name: \"server\", self: { kind: \"host\" }, principalKey });\nserver.accept(webSocketLink(wss, { principal: (req) => authenticate(req) }));  // stamps owner or guest from cookies\nserver.route(\"session:\", {\n\tauthorize: (principal, host) => canReach(principal, host),  // before resolving or spawning\n\tresolve: (host) => workers.ensure(host),  // a link to the worker, spawned if needed\n\tprincipal: (host) => workers.principal(host),  // what the worker acts as when it calls the server\n});\n\n// worker\nconst worker = createHost<Principal>({\n\tname: `session:${id}`,\n\tself: { kind: \"host\" },\n\tprincipalKey,\n\tupstream: { link: ipcLink(process), principal: { kind: \"server\" } },  // the server's own requests act as this\n});\n\n// anywhere\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // browser → server → worker session:S1\nconst sessions = self.get(Sessions, {});  // `hostNamed(\"server\")`: worker → server, acting as worker{S1}\n```\n\n- **The server relays** between its children: worker to worker, and browser to worker.\n- **Disconnects:** when a browser disconnects, exactly the references it had in workers are released.\n- **Principals across links:**\n  - a forwarded principal is accepted only from the parent link;\n  - calls an object makes itself act as its host;\n  - links ping, and a dead link releases everything held through it.\n- **Client origins:** a hold that comes from a browser carries an opaque id of its connection (`origin`), forwarded by the server when it relays the hold, so a worker can tell its clients apart. A client cannot set it. A disconnect releases that client's holds at once.\n- **Slow peers:** a peer acknowledges what it processed. While a window (`window`, default 256 KiB) of frames is unacknowledged, state waits and is coalesced per member's stream; calls, results and the first snapshots of a new reference are never held back, so a `get` does not wait behind other references' state. Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams are replaced by their current value. So under load a result can arrive before the state change it caused: wait for the state you need instead of assuming it is there when the call returns.\n- **Reloads across hosts:** the server coordinates prepare and commit for every host.\n- **Workers that start behind** (spawned after a reload) are caught up to the server's code version before anything is routed to them. Calls their roots make to the server meanwhile run in the server's current code version instead of failing `superseded`.\n- **Protocol version:** both ends announce the link protocol version in their hello. A peer of another version is not\n  talked to: the link closes, and `onProtocolMismatch` (a `LinkOptions` field of `connect` and `accept`) tells the\n  side that has to load new code. A browser reloads its page instead of reconnecting:\n\n  ```ts\n  host.connect(webSocketLink(socket), { onProtocolMismatch: () => location.reload() });\n  ```\n- **Browsers** don't take part in commits. They learn about new code through shape and `reset` messages, and an incompatible contract makes their calls fail with `upgrade_required`.\n\n## Durable\n\nA worker's host provides the Durable Harness as a local object of lifetime `\"host\"`. A Durable phase isn't a root: it's pinned to its registry snapshot by Durable itself. Each Chord call a hook or tool makes starts a fresh short root.\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", {\n\t\tversion: 1,\n\t\topen: () => DurableHarness.open(storage, { models, registry: durableRegistry(host, Extensions) }),  // a reader over the code version tables\n\t\tclose: (h) => h.close(),\n\t});\n\treturn { conversation: (id: number) => durable.value.conversation(id) };\n}, { lifetime: \"host\" });\n\n// a plugin contributes a Durable extension like any registration\nhost.serve(Guard, (_, _host, self) => {\n\tself.register(Extensions, {\n\t\tguard: defineExtension({\n\t\t\tname: \"guard\",\n\t\t\thooks: [hook(ToolTask, { beforeTool: (call) => (isDangerous(call) ? { block: \"Needs approval\" } : undefined) })],\n\t\t}),\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n```\n\nA reload changing the guard: Durable phases already running keep the old snapshot, the next phase uses the new one, and there's never a phase without a guard.\n\n## Errors\n\n| Code | Meaning |\n| --- | --- |\n| `forbidden` | The factory or a method refused this principal |\n| `invalid` | Input, arguments or a result failed validation |\n| `not_ready` | A declared resource or reference was used inside a factory, or state read before its reference's first hydration |\n| `not_in_variant` | The member doesn't exist in the variant you have |\n| `released` | The reference was released |\n| `gone` | The object was forced away while referenced. Call `get` again. |\n| `contract_mismatch` | The provider's contract doesn't cover the consumer's, and no adapter exists |\n| `upgrade_conflict` | An old root reached an object migrated meanwhile. Retry as a new root. |\n| `superseded` | The root's code version is no longer available on this host. Retry. |\n| `upgrade_required` | A browser's contract is outdated. Reload its web facets. |\n| `unavailable` | The target host can't be reached or started; reason `ready_timeout`: a reference was not ready within `readyTimeout` |\n| `aborted` | The call's `ctx` was cancelled |\n\n## Rules of thumb\n\n| Do | Not |\n| --- | --- |\n| Keep reloadable state in cells | Closure variables (lost on reload) |\n| Bump the cell's version, with `migrate`, when a cell's shape or meaning changes | Changing a schema without a bump (refused) or a meaning without a bump (silent) |\n| Bump the contract version on breaking changes; add `accepts` for old consumers | Renaming or removing members at the same version (refused) |\n| Do slow setup in a resource's `open` | Awaiting inside the factory (it's synchronous) |\n| Do side effects in resources, listeners, tasks and registrations | Effects in the factory body (they run during prepare) |\n| Use `ctx.get` inside methods | `self.get` inside methods (a new reference per call) |\n| Keep roots short; long waits are state | Awaiting a human inside a method |\n| Check arguments in methods | Treating the variant as all of authorization |\n| Pass `ctx` everywhere | Starting background work with the caller's `ctx` (use a task) |\n| Treat state values as immutable | Mutating what you read |\n","packages/chord/docs/spec.md":"# Chord specification\n\nThis document specifies the runtime in `src`. Where it and the code disagree, the disagreement is a bug. [Known limitations and decisions](#12-known-limitations-and-decisions) lists what the runtime does not do yet and the choices the sections below leave open.\n\nHard constraints:\n- Chord's runtime depends only on its own `delta` module (tracker, applyImmutable, encoder/decoder, diff) and Node/browser built-ins plus `typebox` for schemas. No other Pi packages.\n- Built-in plugins are trusted, loaded from source in-process. The plugin loader uses dynamic `import()` with a cache-busting query (owner-approved exception, `TODO(loader)`: replace with an isolating, evicting loader; old module copies leak; TUI plugins need isolation).\n- Hot reload is required. Host restart only for Chord/runtime code changes or host-level crash recovery.\n\n## 1. Concepts\n\n| Term | Meaning |\n| --- | --- |\n| facet | A plugin's code for one host (surface): it serves object types (`defineFacet`). |\n| object type | Name + contract. `remote` (addressable from other hosts; schemas for input, variant state members, method args/results) or `local` (host-only; members may be anything, including functions and native objects, but must be listed in the contract: `method`, `opaque` or a state schema). |\n| factory | `(input, principal, self) => implementation`. Synchronous. Registered with `host.serve(type, factory, options)` from a plugin facet. |\n| object | One instance of an object type. Stable identity per `(type, canonicalInput, principalKey)`. Owns cells, resources, references, listeners, tasks, registrations. Survives reloads. |\n| implementation | The object's code: what a factory run returns (methods + state members; `variant` for remote types). Immutable, per code version. |\n| reference | What `get` returns: a late-bound claim on an object, never the object or its implementation. An object lives while referenced. |\n| principal | App-defined. Stamped by links. In-host code acts as the `host` principal. |\n| root | One short Chord call tree, admitted at trusted ingress (link request without root, UI event, timer, `host.root`). Carries `{ rootId, codeVersion }`. |\n| code version | One committed set of implementations/registrations per host (server-issued id). |\n| code holder | Long-lived execution that is NOT a root: Durable phases/tool runs, browsers, `self.task` loops. Every Chord call they make starts a fresh short root. |\n\n## 2. Contracts\n\nContracts are runtime values for every type (shipped bundles have no TypeScript; third-party plugins are built separately). TS types are inferred from them.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,                                   // contract version: a plain number, bumped only on breaking changes\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),       // where its objects live: `session:` + input.session\n\tvariants: {\n\t\tviewer: { state: { view: ViewSchema } },\n\t\tparticipant: {\n\t\t\tstate: { view: ViewSchema },\n\t\t\tmethods: { send: method(SendSchema, Type.Object({ submissionId: Type.Number() })) },\n\t\t},\n\t},\n});\n\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(Hit)),\n\t\tonChange: method(Type.Function([Change], Type.Void()), Unsubscribe),   // non-JSON allowed for locals\n\t\tharness: opaque(\"pi.durable-harness@1\"),                               // native object, by name+version\n\t},\n});\n```\n\n- Input identity: normalize (apply defaults, strip unknown), validate, then canonical JSON with fields equal to their schema default omitted. Changing defaults or normalization requires a new type name.\n- Remote calls validate args/results and state values at the provider boundary. Local calls are not validated per call.\n- Local contracts list every member (`members: { search: method(...), harness: opaque(...), status: Schema }`). A reference that is still `loading` must know whether `h.x` is a method (a caller that waits until live), a state member (a readable) or an opaque value, before any implementation exists. Members missing from the contract are found only once the reference is live.\n- Every bundle embeds the contract tokens it was built against (its own copy). Fingerprint = hash of the canonical description.\n- Placement (remote types, required): `host: hostNamed(name)` (one host) or `hostFamily(prefix, key)` (the host `prefix` + the input's required string field `key`). Data only, in the description (and so the fingerprint and the `hold` token); there is no `at` option. A `get` normalizes the input with the caller's contract, resolves the host and acquires locally if that is this host, else remotely. `serve` (and a reload that loads a facet) refuses a type whose placement excludes the host's name. A host refuses a hold whose input its own contract places elsewhere (`invalid/placement`), and a relay refuses a hold whose target disagrees with its token's placement and input. Coverage requires equal placements, and an `accepts` adapter never bridges two placements: a moved type is a breaking change. A remote reference's address is the type, the normalized input and the principal: no host name.\n\n### Compatibility rule\n\nA consumer built against version M binds to a provider at version N iff N = M and the provider's description covers the consumer's; otherwise only through `accepts[M]` adapters declared by the provider (`serve(type, factory, { accepts: { 1: (current) => v1View } })`); else `contract_mismatch`.\n\nCovers: every consumer member exists with the same kind; methods: both take arguments or neither does, the provider accepts every argument the consumer can send (new fields optional, nothing newly required or retyped), and the provider's result fits the consumer's expected result (extra fields fine; a void consumer accepts any); state: provider value fits consumer schema (extra fields fine); opaque: same name@version.\n\nAdditive changes need no bump. Breaking changes (remove/rename/retype, newly required, or a change of meaning with the same shape) bump the number. Same version + non-covering description = forgotten bump, refused. Meaning changes without a bump are undetectable (author's responsibility, as with Durable document versions).\n\nChecked at: `get` binding, reload prepare (new provider token vs every surviving consumer holding the type and every loaded plugin importing it), remote references (consumer token in the `hold` frame).\n\nThe exact host-owned reference to a `lifetime: \"host\"` object is a lifecycle pin, not a surviving old-contract consumer. When its\nprovider is replaced, commit replaces that reference with one bound to the new provider contract, acquiring it before\nreleasing the old one so the same object, state and unchanged resources survive. It needs no `accepts` adapter\nfor its own previous contract. Prepare/abort do not change that reference. All other consumers retain their own tokens\nand the normal coverage/adapter checks; state-version and migration checks are unchanged.\n\n## 3. Factory and declarations\n\nFactories are synchronous; returning a promise throws. A declaration has a name only if something of it survives a reload and must be found again: state (its value) and resources (the open value), plus listeners (a kept subscription). Names are strings, unique per object per declaration kind (a repeated name is `invalid/duplicate`); kinds have separate names. Derivations and tasks have no names: a reload makes new ones. References are identified by their address.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 },  // schema required, initial value\n\t\t{ version: 2, migrate: (old, from) => upgrade(old, from) });                     // per cell; default version 1\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});                         // reference; identity = canonical address (+ at)\n\tconst watch = self.resource(\"watch\", {\n\t\tversion: 1, deps: [catalog], exclusive: false,\n\t\topen: async ({ signal }) => openWatch(session, signal),\n\t\tclose: async (w) => w.close(),\n\t});\n\tself.on(\"changes\", watch, (w) => w.events, (event, ctx) => status.change(ctx, (d) => { d.tasks = event.tasks; }));\n\tself.task(async (ctx) => { /* loop until ctx.signal aborts */ });\n\tconst visible = self.derive([status], (s) => project(s));      // synchronous\n\tself.register(Routes, { \"GET /status\": handler });              // registry keys are the registry's own keys; `lifetime: \"host\"` objects only\n\treturn { status: visible, errors, report: (args, ctx) => { status.replace(ctx, args); } };  // exposed state = what is returned\n}, {\n\tlinger: 30_000,                                                  // with lifetime \"referenced\" (the default) only\n});\n```\n\n| Declaration | Survives reload | Notes |\n| --- | --- | --- |\n| `state(name, schema, initial \\| { init }, { version?, migrate? })` | same cell and value (`initial` ignored, `init` not run); migrated when its version rises; a cell the reload adds runs `init` at prepare (gated objects: at the gate) | Schema required (no TypeScript at runtime). `change(ctx, draft => void)`, `replace(ctx, value)`, `value`, `subscribe`. Writes through `change` touch only mutated paths. Private state is a cell the implementation does not return. `init(ctx)` (may be async) gives the first value, see activation |\n| `get(type, input, { at? })` | kept if declared again (same address) | returns a reference |\n| `resource(name, { version, deps?, exclusive?, open, close? })` | kept unless `version`, `deps` or `exclusive` change; a kept resource adopts the new declaration (its `close`) at commit | `open` runs at prepare (non-exclusive) or at the gate (exclusive); returns a resource whose `.value` is the open value; a replaced resource closes with its own declaration's `close` |\n| `on(name, source, [select,] handler)` | same subscription while source and `select` (identity or source text) are unchanged; `handler` swapped | each event starts a fresh root in the current code version; `select` is not re-run for a kept subscription. State sources deliver their current value at start and after every reload that replaced them |\n| `task(fn)` | restarted with new code at commit: the old run is aborted via ctx.signal, and the new one starts once it finished (at most `taskStopDeadline`, default 5 s, then anyway, reported) | a code holder; calls inside start fresh roots |\n| `derive(sources, fn)` | a new derivation, computed at commit | synchronous fn |\n| `register(registry, { key: value })` | value swapped at commit | `lifetime: \"host\"` objects only |\n| `expose(resource)` | follows the resource | a local member (usually `opaque`) whose reference value is the resource's value once live |\n\nState members of an implementation may be cells, derivations, or state members of references the object holds (they are defined once the object is live). `cell.replace(ctx, value, { diff? })` publishes a structural diff when the new value shares containers with the old one (or `diff: true`), else `r`. Facet `setup` receives `{ serve, name, root, registry, signal }` of its host; `signal` aborts once this run of the facet is no longer installed (a committed reload replaced or removed it, the reload that loaded it aborted, or the host closed).\n\n`migrate` per cell is `(old, fromVersion) => new`. A cell may be several versions behind (author bumped more than once between reloads). Without `migrate`, a raised version keeps the value, which must fit the new schema.\n\n**Listeners on state reconcile.** A listener whose source is state (a cell, a derivation, a reference's state member) is subscribed when it starts and gets the current value, then every new value. A reload that replaces its source (every derivation is new; a reference to another address is another source) subscribes it again, so it gets the current value again: its handler must reconcile (be idempotent), not count events. An event source (a resource's events, any other `subscribe`) delivers no current value; a kept subscription to it is not touched by a reload.\n\n**Methods.** `method(args?, result?)`: without `args` the method takes none (callers `ref.name(ctx?)`, implementation `(ctx) => …`; `method(undefined, result)` for one that returns something); without `result` it is void (callers get `undefined`, whatever the implementation returned). The description records both (absent fields), and the `call` frame carries no `args` and the `result` frame no `value` for them. A consumer and provider must agree on whether a method takes arguments; a consumer without a result accepts a provider's.\n\nRemoved vs guide: `self.own`, `holdWhile`, async factories, inferred dependencies.\n\n### Synchronous factory, asynchronous activation\n\n`self.get` and `self.resource` only declare and return placeholders. Activation of an object:\n\n1. run the factory synchronously (declarations);\n2. await declared resources' `open()` (e.g. connect a DB);\n3. await declared references becoming live (dependencies activate first, coalesced);\n4. run the `init` of every cell declared with one, in parallel, each in a root of its own acting as the host principal; validate and install the values;\n5. attach derivations, start listeners and tasks;\n6. object is live: references `live`, `ready` resolves.\n\nInside the factory, placeholders must not be used (`resource.value` and an `init` cell's `value` throw `not_ready`; reference calls throw). An `init` that throws, or returns a value its schema refuses, fails the activation like a failed `open`. An `init` must not wait for its own object or one that holds it (it is not live yet), and must not read another `init` cell of its object (they run in parallel). `init` gives a first value from a source the object reads anyway; it does not persist anything, and a cell is not saved anywhere by having one. Methods/listeners/tasks run only on live objects, so their dependencies are live. An object goes live only once its references are ready (live and, for remote ones, hydrated) and the state sources it exposes or derives from have their values, also where they forward a remote reference held some other way; waiting for references is bounded by the host's `readyTimeout` (default 60 s, `unavailable/ready_timeout`, also for a reference cycle across hosts). External consumers holding a `loading` reference (`host.hold`): calls wait until live (ctx cancellation, deadline); state members throw `not_ready` until the first hydration. Activation failures propagate to dependents and all waiting gets; the next get retries.\n\n### Lifetime\n\n`serve(type, factory, { lifetime })`:\n\n| `lifetime` | Created | Ends | `linger` |\n| --- | --- | --- | --- |\n| `\"referenced\"` (default) | at the first `get` of its address | after the last reference is released, plus `linger` | allowed (default 0) |\n| `\"host\"` | when its facet loads (or at `serve`); loading waits until it is live | when the host closes, or a reload removes its facet | refused |\n\n- `\"host\"` is refused at `serve` for remote types, for an input that does not accept `{}`, and together with `linger`;\n  the types (`ServeOptionsFor`) refuse the same at compile time, where such an input is one with a required field.\n  The type has exactly one object: the empty input (defaults applied), created for the host principal. Every `get`\n  reaches it, whatever its principal (the factory sees the host principal); a `get` with another input is\n  `invalid/input`. Other references do not change its lifetime: the host keeps its own until it closes.\n- Only `\"host\"` objects may register (`invalid/not_host_lifetime` otherwise).\n- A `\"host\"` object whose activation fails is started again by the next reload that changes its type. A reload that\n  makes a type `\"host\"` creates its object; one that makes it `\"referenced\"` releases the host's reference, so the\n  object lives on only while others reference it.\n\n### Host objects readiness\n\n- `host.install(facet, { awaitHostObjects? })` installs declarations synchronously and returns a promise for this\n  facet's host object activations. `host.load(facets, { awaitHostObjects? })` installs the entire batch before awaiting\n  its host object activations. `awaitHostObjects` defaults to true. Unreferenced objects are not activated.\n- Factory, resource-open, dependency and activation-finalization failures reject the returned promise with the\n  activation error (ChordError codes preserved; ordinary errors wrapped as `unavailable`). Host object failures also\n  reach `onError`. Installation is not a rollback transaction; setup/registration errors from `install` throw\n  synchronously. Abandoned startup should close the host.\n- `{ awaitHostObjects: false }` starts the same host objects but resolves after installation, without waiting for\n  their activation. `host.ready()` awaits a snapshot of the host's references to its host objects at call time\n  (including direct `serve` and deferred loads), rejects their activation failures, and does not acquire additional\n  references. It is not an ongoing health check and does not await task completion or remote state hydration.\n- Local bootstrap readiness and upstream admission are separate. A worker connects its upstream link, loads with\n  `{ awaitHostObjects: false }`, awaits its local bootstrap reference (e.g. Harness, with no upstream dependency), then\n  announces control readiness so its parent can accept the link. Only after admission does it await `host.ready()`.\n  Otherwise a host object reporting to the server's SessionStatus deadlocks with the parent waiting for control\n  readiness. Deferred loads skip the whole barrier, not selected dependencies; callers explicitly choose local\n  bootstrap references, keep bootstrap busy until full readiness, and propagate later startup errors.\n\n## 4. Principals, variants, references\n\n- Principals are the app's type `P` (`createHost<P>`, `Host<P>`, `defineFacet<P>`, `Factory<C, P>`, `Ctx<P>`; default `unknown`). Chord invents none: `createHost({ self })` is the principal of the host's own code (roots, code inside objects, `lifetime: \"host\"` objects), `connect(link, { principal })` what the parent's own requests act as, `accept(link, { principal })` what a link acts as, `Route.principal(hostName)` what a routed host acts as when it calls back. All are required.\n- `createHost({ principalKey })` maps principal to a key that includes connection provenance and grant revision. Objects are per principal key. The host's own principal is recognized by key (`principalKey(p) === principalKey(self)`), not by object identity, so an equal principal never creates a second object.\n- Remote factories return `{ variant, ...members }`; `variant` must be a declared variant; members must match it. Local factories return any object. `variant` is reserved as a member name.\n- Reference: `status` (`loading | live | released | gone`), `ready` promise, `variant`, `shape` (`{ variant, members, contract }`), `onStatus(listener)` (every status change, with the error for `gone`), late-bound members, `release()`. `self.get` is synchronous (the object goes live after its references are ready); `ctx.get` and `host.get` resolve once the reference is ready: live, and every state member of its variant hydrated (`state: false`: live only). They reject with the activation error, `aborted` (ctx aborted or settled, `signal`), or `unavailable/ready_timeout`, releasing only that reference. `host.hold` is the synchronous form for UI runtimes. A held remote reference stays subscribed to its variant's state until released; after a link loss its last values stay readable while it re-holds. State members are typed `T`; reading before the first hydration throws `not_ready`, outside the variant `not_in_variant`, after the end the end's error. A remote reference type is a union discriminated by `variant`. Shape changes (reload, variant) are pushed; members missing from the current variant reject `not_in_variant`; released references reject `released`; forced disposal makes references `gone`.\n- Shared bindings: remote references on a host with the same key (type, normalized input identity, principal key, consumer contract fingerprint) are views onto one binding: one hold, one stream per state member, calls over that hold. Views have their own owner (object, call, external), status, observers and `release`; the binding ends (gone) or re-holds for all of them. Without views a binding closes, or lingers for `linger.types[typeName]` ms (`createHost({ linger: { types, cap } })`, at most `cap`, default 4, least recently released first) while staying held and hydrated.\n- Methods are `(args, ctx) => Promise<result>`. Factories still check arguments; variants are not complete authorization.\n- Revocation: `host.invalidatePrincipal(match)` force-disposes matching objects on this host and propagates over links; links re-stamp or close.\n\n## 5. Lifetime\n\n- Reference owners: object (declared `get`), call (`ctx.get`, released at settle), connection (remote references; relay maps per origin connection), host (`lifetime: \"host\"`), UI.\n- Activation coalesced per address; if all requesters leave before it completes, the result is disposed. Factory throw: unwind declarations, reject gets, next get retries.\n- Disposal after the last reference is released + `linger`: tasks aborted, listeners removed, resources closed (reverse order; all run; errors aggregated), the object's references released (cascade).\n- Bulk forced disposal (`host.close`, `invalidatePrincipal`) goes in waves, referrers before what they reference, so a resource's `close` can still use its object's references; `host.close` closes links after the objects.\n- Forced disposal: references `gone` immediately, new calls rejected, in-flight call contexts aborted, then cooperative cleanup.\n- Reference graph acyclic in-host (checked at get); across hosts by convention.\n- Links have ping/timeout; a dead link releases everything held through it.\n\n## 6. Roots and code versions\n\n- `Ctx = { signal, rootId, codeVersion, get(...): Promise<Ref>, principal? }`. Explicit; no AsyncLocalStorage dependency (may be used in dev to warn).\n- A link creates a root for a request that carries none; nested remote calls carry `{rootId, codeVersion}`.\n- Dispatch: a call with code version E runs the object's implementation for E. A host retains at most `current` and `previous` code versions (two-version rule). A call for a code version the host doesn't have (older than previous, or host started later) is rejected `superseded` (retryable by starting a new root). Never substitute another code version.\n- Browsers never carry code versions into the server: browser calls start roots at the server. Browsers learn of changes via shape/reset pushes and a server `Manifest` object; incompatible contract fingerprint -> `upgrade_required`.\n\n## 7. Reload\n\nExplicit trigger (`reload_plugins` tool, button, API). Serialized; no nesting.\n\n1. Build/type-check gate (application tooling, outside Chord): dependency-closed set; refuse breaking remote contract changes without a contract version bump.\n2. Load: on every affected host, import new facet modules (cache-busted).\n3. Prepare: for each live object of each affected type, run the new factory in recording mode: existing cells readable, writes throw; listeners/tasks/registrations recorded, not started; references recorded; non-exclusive resources whose name is new or whose `version`/`deps` changed are opened as candidate-owned (closed on abort). Validate keys, variants, registry collisions. New objects activated during prepare use the current code version (old code). Any failure: abort, discard candidates. Vote.\n4. Gate (only objects with a cell whose version was raised or which replace an exclusive resource, plus objects depending on those resources):\n   - Admission cut: from now, an old-code-version root that has not touched the object gets `upgrade_conflict` (retryable), not queued. Roots that already hold an activation lease on it are grandfathered; their re-entry is admitted.\n   - Wait for grandfathered activations to finish, deadline (default 5 s). Missed deadline: abort the reload, reopen, nothing changed (report blocking roots).\n   - Exclusive resources: close old, open new; on failure reopen old and abort.\n   - Run `migrate` into private copies; validate.\n5. Commit (server commits itself, then sends commit to children; the decision is not persisted, see section 12; FIFO links guarantee no host receives a new-code-version root before committing): each host in one synchronous step installs migrated cell values and cell versions, installs implementations as code version E', swaps registry tables, restarts tasks with new code, rebinds listeners, pushes shape/reset to subscribers, opens gates. E' current, E previous.\n   The coordinator's result carries `children`: each participating child's prepared report (its vote).\n6. Retire: when no roots of `previous` remain (and, on a child, the parent retired it), drop its implementations and release declarations only it declared. The next reload first waits for that, at most `retireDeadline`; then it aborts the roots still running on `previous` (their ctx signals), gives them up to 1 s to stop, and retires anyway. A coordinator retires before it sends `prepare`, so its `retire` frames reach the children first.\n\nCompatible changes (no state bump, no exclusive resource change) skip the gate: no waiting.\n\nState compatibility rule: reload prepare compares each cell's old and new schema and version. Unchanged: compatible (no gate). Version raised: gate + `migrate` (or the kept value), validated against the new schema. Schema changed without a raised version: reload refused. A lowered version: refused. A change of meaning with an identical schema needs a bump too (undetectable; author's responsibility). Initial values and migrate outputs are validated; per-write validation in dev mode.\n\n## 8. Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\nself.register(Routes, { \"GET /download\": handler });       // in a `lifetime: \"host\"` object; keys are the registry's keys\nhost.registry(Routes).get(\"GET /download\", ctx);            // table of ctx.codeVersion (or current without ctx)\nhost.registry(Routes).subscribe((table, codeVersion) => ...);     // bridges for imperative consumers (Durable)\n```\n\nPer-code-version immutable tables; order = first registration order, kept on replace. An application can implement Durable's `RegistryReader` from the table (snapshot per code version) so phases pin a complete snapshot; Chord does not ship that bridge.\n\n## 9. Hosts and links\n\n- `createHost({ name, self, principalKey, upstream?: { link, principal } })`; `host.accept(link, { principal })` for children; `host.connect(link, { principal })` for the parent; `host.route(prefix, { authorize(principal, hostName), resolve(hostName) => Promise<Link>, principal(hostName) })`.\n- Links: `memoryLinkPair()` (tests), Node IPC (child_process), WebSocket (server `ws`, browser `WebSocket`).\n- Principal stamping: the accepting side stamps the principal of what connects (from its authenticator); forwarded principals accepted only from the parent link; the parent's own requests (no forwarded principal) act as the principal given to `connect`; payload principal fields ignored.\n- A child that connects behind its parent's code version is fenced (nothing routed to it) and reconciled by a reload of its modules to the parent's code version. Calls its roots of the old code version make upstream are admitted in the parent's current code version (the parent never ran that code version), not rejected `superseded`.\n- Relay: server forwards a hold to the child link of its target host (`at`, resolved by the consumer from the contract's placement and the normalized input, and checked against both), mapping references per origin connection; authorizes before resolving/spawning and re-checks after.\n- Wire frames (JSON): `hello`/`bye`, `hold`/`release` (a reference over a link: `hold` returns `shape` or `gone`; there is no separate `get`), `shape`, `call`/`result`/`cancel`, `sub`/`unsub`/`resync` and `state`, `gone`, `root_end`, `retire`, `invalidate`, `ping`/`pong`, `ack`, reload control (`prepare`/`vote`/`commit`/`abort`).\n- State: a `hold` implies the state of its variant (`state: false`: none until `sub`; `unsub` stops it). The provider answers with units: a `shape` whose `state` lists the members the consumer lacks, followed by one `state` frame `as: \"snap\"` per listed member; the consumer applies the shape once those snapshots are here. Later `state` frames of a member (addressed by hold id and member `m`) are `ops` (per-stream delta encoder), `reset`, or `error`; a `snap`/`reset` without `value` carries `undefined`. A failed snapshot before the first hydration ends the reference with its error. A reload or variant change sends a unit with the members that are new; members that left stop. `again` marks a re-hold after a link loss (the consumer has values). `origin` names the client connection a hold comes from (stamped by the host that accepted the client, forwarded by relays, honored only from the parent link).\n- Protocol version: `hello` carries `protocol` (`PROTOCOL_VERSION`, now 4; absent means 1). A side that sees another version processes nothing more, calls `LinkOptions.onProtocolMismatch({ local, peer })` and closes the link with reason `protocol-version <its version>`; the other side's connection reads that reason and reports the mismatch too. Peers do not negotiate: the outdated side must load the current code (a browser reloads its page). Frame fields follow the version: the `shape` frame names the variant `variant`, code versions travel as `codeVersion` (2); remote contract tokens carry their placement (`host`), and a `hold` carries the input normalized by the consumer (3); a hold implies its state, units, `origin`, and `shape` without a code version (4).\n- Flow control: both ends announce `acks` in their hello and acknowledge what they processed (`ack`, a cumulative count of every frame but `ack`). While the frames not yet acknowledged exceed the window (`window`, default 256 KiB of JSON text), state waits at the sender in slots flushed in creation order: one per member stream (ops are appended, a snapshot or reset replaces it, a release or `unsub` drops it) and one per hold for a unit (lazy: built from the current values when sent; a newer unit merges into it). Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams become a reset of their current value built at flush (a relay sends `resync` downstream and drops that member's ops until the reset arrives). The first unit of a hold that is not `again` and every frame but state are sent at once. Links that report no frame sizes (IPC) are never windowed; a peer without `acks` is neither windowed nor acknowledged. An acceptor can limit the calls a peer has in progress (`maxCalls`); calls beyond it are answered at once with `unavailable/too_many_calls`. Nothing outside the connection may add frames to a link: acknowledgements count them.\n\n## 10. Host manager (library)\n\n`createHostManager({ driver, keepAlive })`: fork driver (entry, init, lock, env), serialized start/stop/replace, exit-before-replace, process-tree kill after grace, control channel (ready/fatal/shutdown), liveness callback (booting, activeCalls, viewers with heartbeats, durable tasks, idleFor).\n\n## 11. Test coverage\n\nThe tests cover at least: lifetime/references/linger/forced disposal/gone; activation coalescing and abandonment; principals and variants; remote calls over memory links incl. relay and principal forgery attempts; state snap/ops/reset over a link with reconnect; registries per code version; reload: compatible (no wait, pinned roots), prepare isolation (writes throw, failing factory leaves everything unchanged), state bump with migrate and gate, `upgrade_conflict` for late old roots, deadline abort, exclusive resource replacement, cross-host commit ordering, two-version retirement and `superseded`.\n\n## 12. Known limitations and decisions\n\n### Still open\n\n- **Retirement across hosts.** A child retires a code version only after its parent retired it (`retire` frame) and its own roots of that code version ended; the server decides from its own roots. A child that comes in step (hello or reconciliation) is told which code versions the parent retired before. A root started on a worker that calls up to a server which has already retired the code version both had still gets `superseded` (retryable).\n- **The commit decision is not persisted** (section 7, step 5). A server crash between its own commit and a child's commit leaves that child on the old code version until it reconnects and is reconciled.\n- **One level of reload participants.** Only the coordinator's direct child hosts take part; a child does not forward `prepare` to its own children.\n- **Reconciliation needs the child's module list.** It is remembered from cluster reloads (`host.remoteModules`), so a server restart forgets it, and a child behind a restarted server then reloads no modules: it gets the code version id with its own code. The reconciliation vote deadline is fixed at 30 s; a failed reconciliation closes the link, so the host manager restarts the worker.\n- **Links do not resume.** The provider releases everything held through a closed link at once, so re-held objects are new ones. Keeping references for a reconnect window would need session ids on links. Calls in flight when a link drops reject `unavailable`; whether they ran is unknown.\n- **Refused clients cannot tell.** The WebSocket adapter closes unauthenticated sockets; the client sees a dropped upstream and its calls wait for a reconnect until their ctx is aborted.\n- **The publication queue is process-wide and synchronous.** A slow observer delays every later notification in the process, including those of other hosts in the same process.\n- **No automatic restart.** A terminal host failure is picked up by the host manager's `keepAlive` or `fatal` path, not inside Chord.\n- **The Node loader leaks.** Every reload imports fresh module copies that are never evicted, and it does not isolate plugins (`TODO(loader)`).\n- **Stale socket locks.** On Unix systems other than Linux, two processes that probe the same stale socket file at the same moment could both take the lock. Managers serialize starts per lock name.\n- **Listener `select` identity.** A listener's subscription is kept across factory runs when its origin and `select` are unchanged; `select` counts as unchanged when identical or with the same source text, so a `select` whose implementation depends on captured values that change between runs is not detected.\n- **Shutdown and dead links.** `host.close()` disposes objects in waves (referrers before what they reference) and closes links last. A `close` that awaits a remote call over a link that is already down waits for a reconnect; bound such calls.\n\n### Decisions\n\n- **Errors.** The guide's codes; a `reason` field carries the detail, for example `invalid/args`, `invalid/cycle`, `invalid/duplicate`, `invalid/read_only`, `invalid/not_host_lifetime`, `invalid/registry_conflict`, or `unavailable/unknown_type`. A `ChordError` thrown by a factory or a dependency keeps its code through every dependent; other errors become `unavailable`, with the original as `cause`.\n- **Reload errors.** There is no `reload_aborted` code. A failed reload throws the cause's code and reason with the message `reload aborted: …` (the cause is attached). Reasons of its own are `aborted/gate_deadline` (with the blocking roots in `details`), `aborted/deadline`, `aborted/vote_deadline` and `unavailable/reload_running`. Across hosts, the message names the host that failed. A child cannot reload on its own (`unavailable/not_coordinator`).\n- **New objects during prepare.** A reload pre-allocates the next code version (\"pending\"). Types the same reload adds are served from it; their objects install implementation only for the pending code version and are force-disposed if the reload aborts.\n- **Gate admission.** From the gate until commit or abort, creating a new object of a changed type fails with `upgrade_conflict` (retryable, reason `gate`); nothing is paused, so nothing can deadlock. Gated objects get `minCodeVersion = E'`: calls and writes carrying an older code version are rejected `upgrade_conflict` permanently, and a gated object drops its previous-code-version implementation at commit. Only objects whose cell versions or exclusive resources change are gated.\n- **Tasks and listeners during the gate.** Tasks of gated objects are aborted at the admission cut and restart with new code at commit, or with the old code on abort. Listener events for gated objects are queued and replayed through the new listener at commit, or the old one at abort.\n- **Grandfathered roots** are the roots that called into the object and are still running at the cut. Every host remembers on which links a root's calls went out and sends `root_end` on them when the root ends, so leases outlive calls across hosts.\n- **Removed types and missed retirement.** Objects of removed types are force-disposed at commit. When a retirement deadline is missed, retirement is forced: those roots are aborted first (their ctx signal, reason `superseded`; a relayed call's root cancels its downstream call), then what only that code version declared is closed, and their next call is `superseded`.\n- **Joining during a reload.** Participants are the child hosts in step when a reload starts. A child whose hello arrives while it runs is decided when it ends: in step, or reconciled to the committed code version; a child already behind is fenced at once, so the calls its roots make meanwhile are admitted in the parent's code version. A reconciliation that is running when a reload starts syncs its child only after that reload ended, against the code version it committed. A reconciliation announces the parent's retired code versions before its `prepare`.\n- **Two-version rule.** An object of a changed type created after a commit has only new code; a previous-code-version root calling it gets `superseded`.\n- **Validation.** Remote calls are validated at reference dispatch, in-host too: a direct binding against the provider's schema, a binding through an adapter against the consumer's. A provider validates each snapshot and reset of a remote state member once per (re)subscription, against the consumer's contract through an `accepts` adapter and the provider's otherwise; ops are validated only in dev mode. At reload, the new provider's description must cover the old provider's when the version is unchanged, in addition to the check against live consumers and facets' `uses`.\n- **Contracts.** Local contracts list their members, so a loading reference knows whether a member is a method, state or opaque. Canonical identity omits input fields equal to their default; with TypeBox, use `Type.Optional(Type.X({ default }))` to let callers omit them. Provider variants must cover consumer variants; members the consumer's contract does not list are hidden. Schema change detection compares canonical JSON, so reordering `anyOf` branches counts as a change. Fingerprints use a non-cryptographic 64-bit hash: synchronous and browser-safe; contracts are trusted.\n- **Principals.** A root ctx at ingress acts as its principal (`host.root(fn, { principal })`); contexts inside objects act as `host`. The acceptor decides a link's kind and name (`accept(link, { principal, kind, name })`); only configured host links are addressable, carry roots and may vote, and control frames are accepted only from the parent. Clients never carry roots or code versions. Relaying needs a matching `route`; `authorize(principal, hostName)` runs before `resolve` and again after, and revoking or re-stamping a principal closes its relays at once. `invalidatePrincipal(match)` re-stamps or closes matching links and propagates pattern matches to child hosts.\n- **Cells and readables.** A value returned from `change` replaces the value and is always validated; `replace` transfers ownership without copying; by default (`{ diff }` overrides) it publishes a diff when the new value reuses a container of the old one at the same key, else `r`. `subscribe(fn)` delivers values, starting with the current one; `events(fn)` delivers `snap`, then `ops`, or `reset`. Reference state members are stable readables that throw `not_ready` while loading and `not_in_variant` outside the variant, and reset when their source moves; a listener added before they are readable gets its snapshot then. Remote state members stay hydrated while the reference is held.\n- **References and calls.** A reference's identity is its address; a second `self.get` of the same address in one run returns the same reference. A reference call without ctx, or with a task ctx, starts a fresh root in the current code version; a ctx whose call or root already settled is refused (`aborted/expired`), one from another host `invalid/foreign_ctx`. Calling a reference inside a factory throws `not_ready`.\n- **Resources and disposal.** New and replaced exclusive resources open at the gate, and so do non-exclusive resources of the same object that depend on them. A new object for an address that is still being disposed waits for that disposal. A resource open that completes after its signal aborted closes its value at once.\n- **Links and the host manager.** Every received frame is validated against its schema; malformed frames, duplicate ids and control frames from the wrong side close the link. Each connection pings every `pingInterval` and closes after `pingTimeout` without a frame. The child holds a kernel lock (an abstract socket on Linux and Android, a named pipe on Windows, a probed socket file elsewhere), acquired by `childControl().lock()` before storage opens. The manager kills the child's process group on every exit path and derives `booting` and `idleFor` from the child's liveness reports. WebSocket support is structural, so Chord does not depend on `ws`.\n- **Flow control holds back state only.** A relay (Cloudflare to a browser) can hide a slow reader, so transport backpressure cannot be relied on; acknowledgements are end to end. Results must not wait behind state, so links are FIFO except that waiting state frames can be overtaken: a result may arrive before state changes made before it. Frames of one subscription stay in order. Waiting state is coalesced rather than queued and capped per link, so a stalled peer costs bounded memory, and the next frame it gets is current. The first unit of a new reference is not held back, so a `get` never waits behind other streams.\n- **The state API stays.** Objects keep named cells and unnamed derivations; giving each object exactly one state value was rejected. Do not reopen this without a new, concrete problem.\n","packages/chord/docs/tutorial.md":"# Build a shared todo list with Chord\n\n## 1. What problem does Chord solve?\n\nA todo list has data, operations on that data, and several people looking at it.\nYou could hand-write object creation, subscription cleanup, messages between\nprocesses, and code replacement. Chord connects these pieces for you.\n\nA **host** is the runtime that owns objects. An **object** combines state with\nmethods at an address, such as “the family todo list.” A **reference** is a caller's\nreference to that object, not the object itself. Several references can reach the\nsame object. References can also reach another host over a connection.\n\n```text\nbrowser host                         server host\nref + subscribed rows -- link --> Todos view (owner or guest)\n                                           |\n                                        ref\n                                           v\n                                   TodoStore: rows + methods\n```\n\nWe will start inside one host, add observable state, then share it with a\nbrowser. Finally we will add background work, plugins, and hot reload.\n**Live state** means subscribers receive new values when data changes, without\nasking for it again.\n\n<!-- example:01 -->\n```ts\nconst host = createHost({ self: \"server\", name: \"todo-server\" });\nassert.equal(host.name, \"todo-server\");\nawait host.close();\n```\n\nThe assertion passes: the host's name is `todo-server`. Creating a host does\nnot create a todo object, start a server socket, or persist data. `close()`\nshuts down its objects and connections.\n\n> **Tip:** These are executable TypeScript examples, using assertions to show\n> results. Shared imports are at the end. Later sections reuse earlier contracts\n> and functions, but each section's test starts with fresh hosts. Test-only\n> cleanup closes hosts even when an assertion fails; snippets omit that cleanup.\n\n## 2. Your first object\n\nBefore callers can use an object, they need to agree on its name and members.\nThat agreement is a **contract**: a runtime value that also gives TypeScript its\ntypes. A **schema** describes a value's shape. We use TypeBox's `Type` to write\nschemas. `method(args, result)` declares a method by its argument and result\nschemas: `method()` takes no arguments and returns nothing, and\n`method(undefined, Type.String())`, used below, takes no arguments and returns a\nstring.\n\nStart with a label for a named list. `local` means only code inside this host\ncan reach it. The input `{ list: \"family\" }` identifies which list we want;\nit is not an argument to `label`.\n\n<!-- example:02-contract -->\n```ts\nconst TodoLabel = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { label: method(undefined, Type.String()) },\n});\n```\n\nA **factory** is a synchronous function that creates the object's implementation:\nthe methods and state callers will see. `serve` registers a contract and its\nfactory. Registration alone does not activate this lazy object; the first\n`get` does.\n\nA **root** is one short tree of calls, such as the work for a button click.\n`host.root` starts it. Its **context**, called `ctx`, carries cancellation,\ncaller identity, and the code version for nested calls. Pass it to methods you\ncall from that root.\n\n<!-- example:02-call -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoLabel, ({ list }) => ({ label: () => `Todos for ${list}` }));\nconst result = await host.root(async (ctx) => {\n\tconst todos = await ctx.get(TodoLabel, { list: \"family\" });\n\treturn todos.label(ctx);\n});\nassert.equal(result, \"Todos for family\");\n```\n\nThe result is `\"Todos for family\"`. `await ctx.get` resolves once the object is\nlive (and, as we will see, has its state). The reference acquired through\n`ctx.get` is released when this root ends. We never receive the factory's\nreturned object directly. A standalone call without `ctx`, such as\n`todos.label()`, starts its own root. A method without arguments takes only\nthe context: `todos.label(ctx)`; its implementation is `(ctx) => …`. Inside a root or method, pass the\nexisting `ctx` so nested calls stay in the same call tree.\n\nThe factory may also receive `principal` (who is asking) and `self` (the\nobject's declaration API). We did not need them yet.\n\n> **Why:** The reference separates callers from implementations. Later, replacing\n> code will not require replacing every caller's reference.\n\n## 3. Give the list live state\n\nA label is not a todo list. We need rows that can change, and a count that always\nagrees with them. A **cell** owns an observable JSON value. A **derivation**\ncomputes another observable value from cells or other readable sources.\n\nExtend the contract with `rows`, `openCount`, and editing methods. State\nmembers describe values, not methods. This adds members at contract version 1;\nolder callers that only need `label` still have everything they need.\n\n`self.state(name, schema, initial)` declares a cell and returns it. The name\n`rows` is important: on reload, Chord matches that name to the existing cell and\nkeeps its value. `self.derive(sources, fn)` declares a derivation; it has no\nname, because it holds nothing a reload must keep.\n\n<!-- example:03-contract -->\n```ts\nconst Todo = Type.Object({ text: Type.String(), done: Type.Boolean() });\nconst Rows = Type.Array(Todo);\nconst TodoStore = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: {\n\t\tlabel: method(undefined, Type.String()),\n\t\trows: Rows,\n\t\topenCount: Type.Number(),\n\t\tadd: method(Type.Object({ text: Type.String() })),\n\t\tfinish: method(Type.Object({ index: Type.Integer() })),\n\t\treset: method(Rows),\n\t},\n});\n```\n\nThe factory now declares a cell and a derivation. It is wrapped in `todoStore`,\nwhich takes the cell's options (section 9 raises its version); for now\n`todoFactory` uses the defaults. Each method receives its arguments and a\ncontext; changing the cell updates its readers and its derived count together.\n`add`, `finish` and `reset` declare no result, so they return nothing.\n\n<!-- example:03-factory -->\n```ts\n/** The store's factory; `options` versions its rows (section 9). */\nconst todoStore =\n\t(options?: StateOptions<Static<typeof Rows>>): Factory<typeof TodoStore> =>\n\t({ list }, _principal, self) => {\n\t\tconst rows = self.state(\"rows\", Rows, [], options);\n\t\tconst openCount = self.derive([rows], (todos) => todos.filter((todo) => !todo.done).length);\n\t\treturn {\n\t\t\tlabel: () => `Todos for ${list}`,\n\t\t\trows,\n\t\t\topenCount,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tdraft.push({ text, done: false });\n\t\t\t\t});\n\t\t\t},\n\t\t\tfinish: ({ index }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tconst todo = draft[index];\n\t\t\t\t\tif (!todo) throw invalid(\"args\", \"No such todo\");\n\t\t\t\t\ttodo.done = true;\n\t\t\t\t});\n\t\t\t},\n\t\t\treset: (next, ctx) => {\n\t\t\t\trows.replace(ctx, next, { diff: true });\n\t\t\t},\n\t\t};\n\t};\nconst todoFactory = todoStore();\n```\n\n`change(ctx, fn)` gives `fn` a temporary mutable **draft**. Here `add`\npushes a row and `finish` changes one field. The draft is discarded if the\ncallback throws. The callback must be synchronous.\n\n`replace(ctx, value)` replaces the whole value. We use it for resetting a\nlist we own completely. `{ diff: true }` asks Chord to publish just the\nstructural changes. Without that option, replacement diffs automatically when\nthe new value shares containers at the same keys with the old one; unrelated\nvalues normally publish a whole-value replacement. Either way, subscribers\nreceive the same resulting value.\n\n<!-- example:03-call -->\n```ts\nconst host = createHost({ self: \"server\", dev: true });\nhost.serve(TodoStore, todoFactory);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nconst counts: (number | undefined)[] = [];\nconst stop = todos.openCount.subscribe((value) => counts.push(value));\nawait host.root(async (ctx) => {\n\tawait todos.add({ text: \"Buy milk\" }, ctx);\n\tawait todos.finish({ index: 0 }, ctx);\n\tawait todos.reset([], ctx);\n});\nassert.deepEqual(counts, [0, 1, 0]);\nassert.deepEqual(todos.rows.value, []);\nstop();\ntodos.release();\n```\n\nThe count subscriber sees `0, 1, 0`: initially empty, one unfinished todo,\nthen no unfinished todos. Resetting to empty does not add another count\nnotification because the derived count is still zero. The final rows are `[]`.\n`subscribe` calls back immediately with the current value and returns an\nunsubscribe function. The awaited `get` makes sure there is a value: a reference\nyou hold is live, with its state.\n\n> **Why:** Store reloadable state in cells, not closure variables. Treat\n> `cell.value`, `ref.rows.value`, and subscriber values as immutable.\n> Only mutate drafts inside `change`; do not keep a draft for later use.\n> A derivation runs synchronously, so use it for projections, not database work.\n\n## 4. Let objects use objects and resources\n\nSaving a todo may require both the list and a database connection. A\n**reference** keeps the object it refers to alive. `self.get` declares a\nreference owned by this object; it lasts longer than a single method call.\n\nA **resource** is something the runtime opens and later closes, such as a\ndatabase connection. Its `open` may be asynchronous even though the factory\nmust be synchronous. The example database is deliberately fake: it just saves\nstrings in memory.\n\n`opaque<T>(\"name@version\")` describes a native, host-only member. It is not\nJSON state and cannot be sent over a link. `self.expose` connects that member\nto a declared resource, without reading the resource during the factory.\n\n<!-- example:04-database -->\n```ts\nconst Database = local(\"tutorial.database\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { db: opaque<{ saved: string[] }>(\"tutorial.database@1\") },\n});\nconst SavedTodos = local(\"tutorial.saved-todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { add: method(Type.Object({ text: Type.String() })) },\n});\nconst host = createHost({ self: \"server\" });\nlet opens = 0;\nconst closed = Promise.withResolvers<void>();\nhost.serve(Database, (_input, _principal, self) => {\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async () => {\n\t\t\topens++;\n\t\t\treturn { saved: [] as string[] };\n\t\t},\n\t\tclose: () => closed.resolve(),\n\t});\n\treturn { db: self.expose(db) };\n});\n```\n\nThe factory returns an exposed marker, not an opened connection. Chord runs\n`open` after declarations finish. The promise named `closed` lets the test\nobserve eventual cleanup.\n\nNow declare `SavedTodos` as an object that holds both the store and the database.\nIts editing method uses these dependencies only after activation.\n\n<!-- example:04-saving -->\n```ts\nhost.serve(TodoStore, todoFactory);\nhost.serve(\n\tSavedTodos,\n\t({ list }, _principal, self) => {\n\t\tconst database = self.get(Database, {});\n\t\tconst todos = self.get(TodoStore, { list });\n\t\treturn {\n\t\t\tadd: async ({ text }, ctx) => {\n\t\t\t\tdatabase.db!.saved.push(text);\n\t\t\t\tawait todos.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t},\n\t{ linger: 0 },\n);\n```\n\nAn outside consumer holds a `SavedTodos` reference. We also hold `Database`\ndirectly so we can inspect what was saved.\n\n<!-- example:04-call -->\n```ts\nconst saved = await host.get(SavedTodos, { list: \"family\" });\nconst database = await host.get(Database, {});\nawait host.root((ctx) => saved.add({ text: \"Buy milk\" }, ctx));\nassert.deepEqual(database.db!.saved, [\"Buy milk\"]);\nassert.equal(opens, 1);\nconst statuses: string[] = [];\nsaved.onStatus((status) => statuses.push(status));\ndatabase.release();\nsaved.release();\nawait closed.promise;\nassert.equal(saved.status, \"released\");\nassert.ok(statuses.includes(\"released\"));\n```\n\nAfter the method, the fake database contains `[\"Buy milk\"]`. It opened once,\neven though two objects referenced it. `SavedTodos` and `Database` become live\nonly after their resources and declared dependencies are ready. That is why\n`database.db!` is safe inside this live object's method: activation has\nalready established that dependency. An outside caller awaits its `get`.\n\nWhen both external references are released, `SavedTodos` is disposed and\nreleases its own database reference. With no references left, the database closes; we\nwait for `closed.promise` to prove it. The released reference stays\n`released`, and `onStatus` reports that transition. `onStatus` watches\nchanges; unlike state subscription, it does not deliver an initial status.\n\n`linger` is a delay in milliseconds after the last reference is released. Zero disposes\nwithout a grace period; a positive value can avoid reopening an expensive\nresource when a screen is briefly hidden. Linger is not persistence.\n\n> **Tip:** A factory only declares dependencies. Never call a referenced object's method or\n> read `resource.value` there, including during reload. Activation happens\n> afterward. Bump a resource's `version` when its configuration should be reopened.\n> Disposal stops tasks and listeners, closes resources, then releases references.\n\n## 5. Who is asking? One store, two views\n\nOwners may add todos; guests may only read them. A **principal** is the\napplication-defined identity of a caller. A **variant** is a named set of\nmembers a remote object offers to that principal. It lets the UI discover\nwhether an editing method is available.\n\nDo not create a separate data cell for each viewer: their lists would diverge.\nInstead, each view holds the same host-local `TodoStore` and exposes its rows.\n`self.get` acts as the host's own principal, so owner and guest views share\nthat store. Remote view objects themselves are separate per principal key.\nA remote contract also says where its objects live: `host: hostNamed(\"server\")`\nputs every `Todos` object on the host named `server`. Section 6 uses that.\n\n<!-- example:05-views -->\n```ts\nconst Todos = remote(\"tutorial.todo-view\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\thost: hostNamed(\"server\"),\n\tvariants: {\n\t\tviewer: { state: { rows: Rows } },\n\t\teditor: {\n\t\t\tstate: { rows: Rows },\n\t\t\tmethods: { add: method(Type.Object({ text: Type.String() })) },\n\t\t},\n\t},\n});\n\n/** Who may ask: the owner, a guest, or the server's own code. */\ntype Principal = \"owner\" | \"guest\" | \"server\";\n\nfunction serveViews(api: Pick<Host<Principal>, \"serve\">): void {\n\tapi.serve(Todos, ({ list }, principal, self) => {\n\t\tif (list !== \"family\") throw forbidden(\"This list is not shared\");\n\t\tconst store = self.get(TodoStore, { list });\n\t\tif (principal === \"guest\") {\n\t\t\treturn { variant: \"viewer\", rows: store.rows };\n\t\t}\n\t\treturn {\n\t\t\tvariant: \"editor\",\n\t\t\trows: store.rows,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\tif (text.trim().length === 0) throw invalid(\"args\", \"A todo needs text\");\n\t\t\t\treturn store.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t});\n}\n```\n\nHere principals are the strings `\"owner\"` and `\"guest\"`, assigned by\ntrusted application code, and `\"server\"`, the host's own code: every host is\ncreated with `self`, the principal its own roots and objects act as. Chord\ninvents no principals. The `Principal` type parameter of `createHost` and\n`Host` makes `principal` in the factory that type, so TypeScript checks the\ncomparisons. `principalKey` maps identity to object identity. Real\napplications should use stable identity, connection provenance, and grant\nrevision, not display names.\n\n<!-- example:05-call -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", principalKey: (principal) => principal });\nhost.serve(TodoStore, todoFactory);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nconst guest = await host.get(Todos, { list: \"family\" }, { principal: \"guest\" });\nassert.ok(owner.variant === \"editor\"); // narrows the reference to the editor variant\nassert.equal(guest.variant, \"viewer\");\nawait host.root((ctx) => owner.add({ text: \"Buy milk\" }, ctx), { principal: \"owner\" });\nassert.deepEqual(guest.rows.value, [{ text: \"Buy milk\", done: false }]);\n// @ts-expect-error A viewer has no add: TypeScript refuses it, and so does the provider.\nawait assert.rejects(guest.add({ text: \"Not allowed\" }), (error) => isChordError(error, \"not_in_variant\"));\nawait assert.rejects(owner.add({ text: \" \" }), (error) => isChordError(error, \"invalid\"));\n```\n\nThe owner gets `editor`; the guest gets `viewer`. Both see the same milk\ntodo. A reference is typed as the union of its variants: `add` is reachable only\nafter narrowing on `variant` (`assert.ok(owner.variant === \"editor\")` does that).\nThe guest's `add` is refused by TypeScript, and, called anyway, rejects with\n`not_in_variant`. Whitespace-only text\nrejects with `invalid`: it is a string, as the schema demands, but it breaks a\nrule only the method knows. Use `invalid` for bad input and `forbidden` only\nfor callers who may not do something.\n\n> **Why:** A variant is not all of authorization. The factory checks which\n> list may be opened; the method checks the operation's arguments. Production\n> code must also enforce grants that can change. Use\n> `host.invalidatePrincipal(match)` to revoke matching objects; held references\n> become `gone`. A UI watches `onStatus` and gets a new reference when appropriate.\n> Guest-provided names or message text must never establish identity.\n\n## 6. Reach the list from another process\n\nA browser cannot call a host-only contract. `remote` contracts describe only\nschema-checked state and methods that can travel as JSON. Our `Todos` view is\nremote; the underlying `TodoStore` and database stay local. A caller never\nnames a host: `browser.get(Todos, …)` goes to `server` because the contract\nsays so, and the server refuses to serve, or to be asked for, a type it is not\nthe place of. A type that lives on one host per session says\n`host: hostFamily(\"session:\", \"session\")`: its input's `session` field picks\nthe host (`{ session: \"S1\" }` lives on `session:S1`).\n\nA **link** carries ordered messages between hosts. A memory link runs both ends\nin one test process, but uses the remote protocol. The accepting host stamps\nthe caller's principal from its authenticator, not from method arguments or a\nclient's claimed identity. The connecting side names the principal its parent's\nown requests act as (`upstream: { link, principal }`, or `connect(link, {\nprincipal })`).\n\n<!-- example:06 -->\n```ts\nconst server = createHost<Principal>({ self: \"server\", name: \"server\" });\nserver.serve(TodoStore, todoFactory);\nserveViews(server);\nconst [down, up] = memoryLinkPair();\nserver.accept(down, { principal: \"owner\", kind: \"client\" });\nconst browser = createHost({\n\tself: \"browser\",\n\tname: \"browser\",\n\tkind: \"client\",\n\tupstream: { link: up, principal: \"server\" },\n});\nconst todos = await browser.get(Todos, { list: \"family\" });\nassert.ok(todos.variant === \"editor\");\nassert.deepEqual(todos.rows.value, []);\nconst updated = Promise.withResolvers<void>();\nconst stop = todos.rows.subscribe((rows) => {\n\tif (rows.length === 1) updated.resolve();\n});\nawait browser.root((ctx) => todos.add({ text: \"Buy milk\" }, ctx));\nawait updated.promise;\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\n// Deliberately bypass TypeScript to simulate a malformed caller.\nawait assert.rejects(todos.add({ text: 123 } as never), (error) => isChordError(error, \"invalid\"));\nstop();\n// A held reference keeps its state without listeners.\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\ntodos.release();\n```\n\nThe browser sees an empty snapshot, then the new milk todo. `ready` means\nthe remote reference has bound to a live object **and** has its state: every\nstate member of its variant has a snapshot. A held reference stays subscribed,\nwith or without listeners, until it is released.\n\nWhat crosses the wire: the input and consumer's contract description, the\nprovider's shape (variant and members), method arguments and results, and the\nstate of the reference's variant. State starts with a **snapshot**, the whole current value;\nsubsequent **deltas** describe changes. Rebinds and reloads can send a\n`reset`, a fresh whole value. Functions, resource connections, and cell\ninstances do not cross the wire.\n\nThe deliberate bad argument rejects with `invalid`. Remote arguments,\nresults, and outgoing state are validated at the provider boundary. Local\nmethods do not get per-call schema validation; they still need app-level checks.\n`dev: true` additionally checks cell writes while developing.\n\n> **Tip:** Replace `memoryLinkPair` with `webSocketLink` for a WebSocket or\n> `ipcLink` for a child-process IPC channel; object contracts stay the same.\n> Those adapters wrap an existing connection, not an authenticator. Configure\n> `accept` with an authenticated principal. A browser is `kind: \"client\"`;\n> a trusted child host is accepted with `kind: \"host\"` and its configured name.\n\n## 7. React to changes and run background work\n\nNot all work comes from method calls. The app may update an activity indicator\nwhen rows change, or run a background reminder. A **listener** reacts to an\nobservable source. A **task** is background execution owned by an object,\nwhich is aborted when the object is disposed or its code is reloaded.\n\nOur activity object mirrors the row count. Its task inserts a sample todo,\nthen waits for shutdown. The promises make those events deterministic in a\ntest; they are not stored application state.\n\n<!-- example:07 -->\n```ts\nconst Activity = local(\"tutorial.activity\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { seen: Type.Number() },\n});\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst observed = Promise.withResolvers<void>();\nconst seeded = Promise.withResolvers<void>();\nconst stopped = Promise.withResolvers<void>();\nhost.serve(Activity, (_input, _principal, self) => {\n\tconst todos = self.get(TodoStore, { list: \"family\" });\n\tconst seen = self.state(\"seen\", Type.Number(), 0);\n\tself.on(\"rows\", todos.rows, (rows, ctx) => {\n\t\tseen.replace(ctx, rows?.length ?? 0);\n\t\tif (rows?.length === 1) observed.resolve();\n\t});\n\tself.task(async (ctx) => {\n\t\tawait todos.add({ text: \"Review the list\" }, ctx);\n\t\tseeded.resolve();\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tif (ctx.signal.aborted) resolve();\n\t\t\telse ctx.signal.addEventListener(\"abort\", () => resolve(), { once: true });\n\t\t});\n\t\tstopped.resolve();\n\t});\n\treturn { seen };\n});\nconst activity = await host.get(Activity, {});\nawait Promise.all([seeded.promise, observed.promise]);\nassert.equal(activity.seen.value, 1);\nactivity.release();\nawait stopped.promise;\n```\n\nThe final `seen` is `1`. Each listener event receives a fresh root's\n`ctx`; its write belongs to that short call tree. A task is not one enormous\nroot: Chord method calls made with its task context start fresh short roots.\nWe pass that context into `todos.add` so cancellation follows the task.\nReleasing the activity reference aborts its task and resolves `stopped`.\n\n`self.on(name, source, handler)` declares a listener; `self.task(fn)` a task.\nA listener on state, like `rows` here, gets the current value when it starts,\nthen every change. It gets the current value again whenever a reload replaced\nits source (a derivation is new at every reload), so its handler must\nreconcile (`seen` is set from the rows, not counted up). Listeners can also\nsubscribe to a resource's event stream with `self.on(name, resource, select,\nhandler)`: `select` gets the opened resource and returns something with\n`subscribe`; an event stream delivers no current value. Kept subscriptions\nsurvive reload while the resource and selector remain unchanged; the handler\nswitches to new code. A selector that captures changing configuration should\nget a new listener name. Tasks have no names: a reload restarts them all.\n\n> **Why:** Do not wait for a person, sleep for minutes, or run an endless loop\n> inside a method. That pins a root and can block state migration. Store a\n> pending request in a cell and answer in another call, or use a task that obeys\n> `ctx.signal`. Never retain a completed method's `ctx` for background work.\n\n## 8. Package features as plugins\n\nNow a plugin can contribute a menu action without editing a central switch.\nA **registry** is a named table of contributions. A **facet** is a plugin's code\nfor one kind of host: it serves object types. A plugin can have server, worker, and browser\nfacets, each installing that part of the feature.\n\nRegistrations belong to objects of **lifetime `\"host\"`**: the host creates such\nan object when its facet loads and keeps it until the host closes, even if no\nscreen asks for it. That keeps our menu contribution present. Objects of the\ndefault lifetime, `\"referenced\"`, are still created only when referenced.\n\n<!-- example:08 -->\n```ts\nconst Actions = defineRegistry<(ctx: Ctx) => Promise<number>>(\"tutorial.actions\");\nconst Menu = local(\"tutorial.menu\", { version: 1, input: Type.Object({}) });\nlet click: (() => Promise<number>) | undefined;\nconst storeFacet: Facet = {\n\tname: \"todo-store\",\n\t...defineFacet((api) => api.serve(TodoStore, todoFactory)),\n};\nconst menuFacet: Facet = {\n\tname: \"todo-menu\",\n\t...defineFacet(\n\t\t(api) => {\n\t\t\tapi.serve(\n\t\t\t\tMenu,\n\t\t\t\t(_input, _principal, self) => {\n\t\t\t\t\tconst todos = self.get(TodoStore, { list: \"family\" });\n\t\t\t\t\tself.register(Actions, {\n\t\t\t\t\t\taddSample: async (ctx) => {\n\t\t\t\t\t\t\tawait todos.add({ text: \"Try plugins\" }, ctx);\n\t\t\t\t\t\t\treturn todos.openCount.value;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\treturn {};\n\t\t\t\t},\n\t\t\t\t{ lifetime: \"host\" },\n\t\t\t);\n\t\t\tclick = () => api.root((ctx) => api.registry(Actions).get(\"addSample\", ctx)!(ctx));\n\t\t},\n\t\t{ uses: [TodoStore] },\n\t),\n};\nconst host = createHost({ self: \"server\" });\nawait host.load([menuFacet, storeFacet], { awaitHostObjects: false });\nawait host.ready();\nassert.equal(await click!(), 1);\nassert.equal(host.registry(Actions).table().size, 1);\n```\n\nClicking returns `1`, and the registry has one entry. The menu's dependency\ncan come from a later facet in the load batch: `load` installs the whole\nbatch before waiting for host objects to activate. The facet declares `uses` so\nreload can also check the contract it consumes.\n\nThe facet API supplies `serve`, `name`, `root`, and `registry`.\nOur ordinary UI callback uses its saved `api.root`, not a process-global\nhost. Looking up a registry entry with `ctx` uses the table belonging to that\nroot's code version. Without `ctx`, lookup uses the current table. Duplicate\nkeys fail by default; `subscribe` can bridge tables to an imperative consumer.\n\nNormally use `await host.load(facets)` (or `await host.install(facet)`),\nwhich waits for those host objects. This example explicitly defers that barrier\nwith `awaitHostObjects: false`, then awaits `host.ready()`. That waits for a\nsnapshot of the host's objects, not for task completion or later loads.\nFailures reject the readiness promise; abandoning startup requires closing the\nhost, because installation is not a rollback transaction.\n\n> **Tip:** Use deferred readiness only when needed. A worker may have to await\n> an independent local bootstrap reference, announce control readiness so its\n> parent admits its link, and only then await `host.ready()`. Waiting for\n> upstream-dependent host objects before admission would deadlock.\n\nFor real plugin files, default-export `defineFacet(...)`. Node hosts configure\n`createHost({ loader: nodeLoader })`, with `nodeLoader` imported from\n`@earendil-works/chord/node`, then pass module URLs to `load` and `reload`.\nThe in-memory facet objects here avoid filesystem setup. Built-in facets are\ntrusted in-process code, not a sandbox; the current Node loader keeps old module\ncopies in memory, so repeated loading has that limitation.\n\n## 9. Change running code without losing the list\n\nA reference is useful precisely because callers keep it while implementations\nchange. An **code version** identifies one committed set of code and registry tables.\nA root keeps the code version it started with. New roots use the latest one.\n\n### Compatible code change: no waiting\n\nGive the same named facet a new factory. It changes the label but keeps the\ncell declaration. This test deliberately pauses one caller to show what an\nold root sees; application methods should not pause like this.\n\n<!-- example:09-compatible -->\n```ts\nconst host = createHost({ self: \"server\" });\nconst facet = (label: string): Facet => ({\n\tname: \"todos\",\n\tsetup: (api) =>\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tlabel: () => label,\n\t\t})),\n});\nawait host.load([facet(\"Original\")]);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nawait todos.add({ text: \"Buy milk\" });\nconst entered = Promise.withResolvers<void>();\nconst resume = Promise.withResolvers<void>();\nconst oldRoot = host.root(async (ctx) => {\n\tconst first = await todos.label(ctx);\n\tentered.resolve();\n\tawait resume.promise; // Test-only pause: pin an old caller during reload.\n\treturn [first, await todos.label(ctx)];\n});\nawait entered.promise;\nconst compatible = await host.reload([facet(\"Updated\")]);\nassert.deepEqual(compatible.gated, []);\nassert.equal(await todos.label(), \"Updated\");\nresume.resolve();\nassert.deepEqual(await oldRoot, [\"Original\", \"Original\"]);\nassert.equal(todos.rows.value?.[0]?.text, \"Buy milk\");\n```\n\nReload commits while the old root is paused: `gated` is empty. New calls see\n`\"Updated\"`; the resumed old root sees `\"Original\"` twice. The milk row\nsurvives. Compatible code changes do not wait for that old root to finish.\n\n### State change: bump its version and migrate\n\nSuppose a new policy converts existing todo text to uppercase. The schema is\nunchanged, but the data's meaning changes. We still bump the cell's **state\nversion** (`self.state`'s `version` option, default 1). It belongs to one cell,\nindependently of the contract version. A **migration** (`migrate`) converts an\nold value of that cell to the new representation.\n\n<!-- example:09-migrate -->\n```ts\nconst events: string[] = [];\nconst stopEvents = todos.rows.events((event) => events.push(event.type));\nconst migrated = await host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tTodoStore,\n\t\t\t\ttodoStore({\n\t\t\t\t\tversion: 2,\n\t\t\t\t\tmigrate: (old, from) => {\n\t\t\t\t\t\tassert.equal(from, 1);\n\t\t\t\t\t\treturn (old as Static<typeof Rows>).map((todo) => ({ ...todo, text: todo.text.toUpperCase() }));\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t),\n\t},\n]);\nassert.equal(migrated.gated.length, 1);\nassert.deepEqual(todos.rows.value, [{ text: \"BUY MILK\", done: false }]);\nassert.deepEqual(events, [\"snap\", \"reset\"]);\nstopEvents();\n```\n\nThe migration receives the old rows and their state version, `1`. It returns\nnew rows; the reference now reads `[{ text: \"BUY MILK\", done: false }]`.\nSubscribers get a `reset` event containing the migrated value. The event\nassertion sees an initial `snap`, then that `reset`.\nOne object is gated. A **gate** waits for calls already inside that object to\nfinish before migrating a private copy, validating it, and committing. Old\nroots that had not entered it cannot later use old code over migrated data:\nthey get `upgrade_conflict` and should retry in a new root.\n\nChanging a cell's schema also requires a version bump, and a migration unless\nthe old value already fits the new schema. A change of meaning with the same schema,\nlike this example, cannot be detected automatically. Real migrations must\nreference each stored version they support, not assume every installation is\nexactly one version behind.\n\n### Contract change: keep an adapter for old callers\n\nRename `label` to `heading`. That is a breaking contract change, so use\ncontract version 2. The provider's `accepts[1]` adapter builds the implementation\nthat version-1 consumers still expect.\n\n<!-- example:09-contract -->\n```ts\n// Remove label from the new provider's contract: it is now called heading.\nconst { label: _label, ...members } = TodoStore.def.members;\nconst RenamedStore = local(\"tutorial.todos\", {\n\tversion: 2,\n\tinput: TodoStore.def.input,\n\tmembers: { ...members, heading: method(undefined, Type.String()) },\n});\nawait host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tRenamedStore,\n\t\t\t\t(input, principal, self) => {\n\t\t\t\t\tconst { label, ...implementation } = todoStore({ version: 2 })(input, principal, self);\n\t\t\t\t\treturn { ...implementation, heading: label };\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\taccepts: {\n\t\t\t\t\t\t1: (current) => ({ ...current, label: current.heading }),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t},\n]);\nassert.equal(await todos.label(), \"Todos for family\");\nconst newer = await host.get(RenamedStore, { list: \"family\" });\nassert.equal(await newer.heading(), \"Todos for family\");\nassert.equal(todos.rows.value?.[0]?.text, \"BUY MILK\");\n```\n\nThe old reference's `label` and the new reference's `heading` both return\n`\"Todos for family\"`. Both still reach the migrated rows. The adapter maps\nimplementations, not references; forwarding the existing method preserves its context.\nWe keep the cell at version 2 here so this code-only change does not repeat\nmigration.\n\n> **Why:** Contract versions protect callers; state versions protect stored\n> data; resource versions control reopening. Do not use one number for all\n> three. Additive compatible contracts can keep their version; breaking shape\n> or meaning changes need a contract bump and adapters for old consumers.\n> Reload prepares candidates first; preparation failure leaves the old code\n> running. A migration gate that misses its deadline aborts rather than\n> switching data underneath active old code.\n\n## 10. Test the objects, not a whole deployment\n\nMost implementation needs only a bare host. Use real factories for state tests, and a\nfake facet when you want to test delegation without a real database or service.\nHere the fake records what the editing view sends to the store.\n\n<!-- example:10 -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", dev: true });\nconst received: string[] = [];\nconst fake: Facet = {\n\tname: \"fake-store\",\n\t...defineFacet((api: FacetApi) => {\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tadd: ({ text }) => {\n\t\t\t\treceived.push(text);\n\t\t\t},\n\t\t}));\n\t}),\n};\nawait host.load([fake]);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nassert.ok(owner.variant === \"editor\");\nawait owner.add({ text: \"Test delegation\" });\nassert.deepEqual(received, [\"Test delegation\"]);\nawait assert.rejects(owner.add({ text: \"\" }), (error) => isChordError(error, \"invalid\"));\nowner.release();\nawait assert.rejects(owner.add({ text: \"Too late\" }), (error) => isChordError(error, \"released\"));\n```\n\nThe fake received `[\"Test delegation\"]`. Empty text failed before reaching\nthe fake. The released reference rejects further calls with `released`.\n`isChordError(error, code)` checks a stable error code; avoid testing only a\nmessage's wording.\n\nOur other tests exercise the real cells (section 3), resource cleanup (4),\nprincipal variants (5), a client over a memory link (6), and callers kept through\nreload (9). After an awaited `get` the state is there; for later changes, await\nthe value you need. Always unsubscribe and close hosts; leaked references can hide\nlifetime bugs or leave background tasks running.\n\n> **Tip:** A fake must honor the contract it serves. This fake delegates all\n> ordinary members to `todoFactory` and replaces only `add`; it deliberately\n> objects calls rather than updating rows. Do not confuse that with persistence.\n\n## 11. Keep these habits\n\nA temporary call reference should not accidentally become a permanent one. A\nscreen should not lose its data merely because a button click finishes. Use\n`ctx.get` for the first lifetime and `host.get` for the second.\n\n<!-- example:11 -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst screen = await host.get(TodoStore, { list: \"family\" });\nawait host.root(async (ctx) => {\n\tconst temporary = await ctx.get(TodoStore, { list: \"family\" });\n\tawait temporary.add({ text: \"Keep roots short\" }, ctx);\n}); // The temporary reference is released here; screen still holds the store.\nassert.equal(screen.openCount.value, 1);\nscreen.release(); // No reference remains; without persistence, disposal loses these rows.\n```\n\nThe screen still sees count `1` after the root finishes, because it owns a\nseparate reference. When the screen releases it, no reference remains. This tutorial's cells\nare in memory: reload preserves a live object, but disposal or process restart\nloses its rows. Real persistence belongs in a database resource, with an\nexplicit load/save policy.\n\n### Checklist and common mistakes\n\n- Keep reloadable state in cells, not closure variables: factories rerun.\n- Change drafts, not published values: mutation outside cells bypasses updates.\n- Use `change` for field edits and `replace` for owned snapshots: replacing\n  can erase fields your code does not know about.\n- Declare dependencies in a synchronous factory, not during methods: activation\n  and reload need a complete declaration list.\n- Use `ctx.get` for temporary method dependencies: their references end with the call.\n- Pass `ctx` into nested calls: cancellation and code versions must stay together.\n- Keep roots short and tasks cancellable: migration and shutdown need work to settle.\n- Check grants and arguments, not just variants: available methods are not a full policy.\n- Stamp principals at trusted ingress, not from client payloads: names are not identities.\n- Await `get` before reading state: a reference you hold is live and hydrated, a\n  loading one (`host.hold`) throws `not_ready`.\n- Release references and unsubscribe: otherwise unused objects remain referenced or observed.\n- Bump state versions, contract versions, and resource versions for their own changes:\n  each protects a different boundary.\n- Await host object readiness before reporting startup success: declared is not yet live.\n- Keep reference dependencies acyclic: an object cannot wait for itself through another object.\n\n### Imports and running the examples\n\nThese are the shared imports from `test/tutorial.test.ts`. The relative\npath is correct from that test directory; applications import the same names\nfrom `@earendil-works/chord`. `assert` expresses expected values; `it` and\n`onTestFinished` come from the test runner; `readFileSync` is used by the\ntutorial's source-matching check, not by the application.\n\n<!-- example:imports -->\n```ts\nimport assert from \"node:assert/strict\";\nimport { readFileSync } from \"node:fs\";\nimport { type Static, Type } from \"typebox\";\nimport { it, onTestFinished } from \"vitest\";\nimport {\n\ttype Ctx,\n\tcreateHost,\n\tdefineFacet,\n\tdefineRegistry,\n\ttype Facet,\n\ttype FacetApi,\n\ttype Factory,\n\tforbidden,\n\ttype Host,\n\thostNamed,\n\tinvalid,\n\tisChordError,\n\tlocal,\n\tmemoryLinkPair,\n\tmethod,\n\topaque,\n\tremote,\n\ttype StateOptions,\n} from \"../src/index.ts\";\n```\n\nFrom `packages/chord`, run:\n\n```sh\nnode ../../node_modules/vitest/dist/cli.js --run test/tutorial.test.ts\n```\n\nThere is one test per numbered section. Every TypeScript block is matched\nagainst its runnable source, so changing either copy without the other fails a\ntest. The test harness adds teardown and drift checks; it never uses a real\ndatabase, network service, or provider API.\n\nNext read [the usage guide](guide.md) for the API reference and\n[the specification](spec.md) for compatibility, lifetime, and reload\nrules, including its known limitations. If a reference disagrees with\n`src`, the current implementation wins.\n","packages/chord/src/delta/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fchord%2Fsrc%2Fdelta%2FREADME.md -->","packages/codemode/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcodemode%2FREADME.md -->","packages/codemode/examples/cloudflare/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcodemode%2Fexamples%2Fcloudflare%2FREADME.md -->","packages/coding-agent/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2FREADME.md -->","packages/coding-agent/docs/cli-integration.md":"# CLI Integration\n\nBy default, running `pi` opens the interactive terminal interface. When input or output is piped or redirected, Pi uses print mode instead. You can also select print, JSON, or RPC mode explicitly for scripts and applications.\n\nAll four modes use the same agent, sessions, resources, and tools. The mode determines how input enters Pi, how output is exposed, and whether the process remains available for more commands.\n\nThe SDK is not a CLI mode. It embeds the agent directly in a Node.js or Bun process. See the [SDK](sdk.md) when direct TypeScript access is preferable to a process boundary.\n\n## Choose a mode\n\n| Mode | Interface | Lifetime | Use it when |\n|---|---|---|---|\n| Interactive | Terminal UI | Until the user exits | A person is working with Pi directly |\n| Print | Final text on stdout | One invocation | A script needs the final assistant response |\n| JSON | JSONL events on stdout | One invocation | A process needs structured progress from a run |\n| RPC | JSONL commands, responses, and events | Long-lived | A process needs bidirectional control |\n\nCLI options still select the working directory, model, tools, resources, and session persistence independently of the mode. See [Command Line](cli.md) for the complete startup options.\n\n## Print to stdout\n\nPrint mode runs the supplied prompts, writes the final assistant text to stdout, and exits:\n\n```bash\npi --print \"Summarize the changes in this repository\"\n```\n\nUse print mode when only the final text is needed, including command substitution, pipelines, and one-shot jobs. Intermediate events are not exposed.\n\nPrint mode writes errors to stderr. A final assistant response with an `error` or `aborted` stop reason produces a nonzero exit status. A response cut off at the model's output token limit (`length` stop reason) still prints its partial text to stdout, writes a warning to stderr, and produces a nonzero exit status.\n\nWhen no mode is selected explicitly, non-TTY stdin or stdout also selects print mode. This allows piped input and output without adding `--print`.\n\n## Stream JSON events\n\nJSON mode writes a session header followed by agent and session events as newline-delimited JSON:\n\n```bash\npi --mode json \"Review this repository\" > events.jsonl\n```\n\nThis is structured event output, not a single JSON result or a constraint on the format of the model’s response.\n\nAll prompts are supplied when the process starts. The process streams events for that run and then exits; it does not accept later commands.\n\nA failed or aborted assistant response appears in the event stream but does not by itself produce a nonzero exit status. Inspect the events when success or failure matters. Pi still exits nonzero if the invocation throws an error.\n\nStreaming `message_update` records contain deltas rather than a growing message snapshot. Assemble live output from the delta events, then replace it with the authoritative message from `message_end`.\n\n`agent_end` can be followed by automatic recovery or queued work. `agent_settled` marks the end of automatic work for the current run.\n\nStdout is reserved for JSONL. Diagnostics and application logging are written to stderr. See [JSON Event Stream](json.md) for framing, event shapes, and reconstruction rules.\n\n## Control Pi with RPC\n\nRPC mode keeps Pi running while another process sends commands and receives responses and events:\n\n```bash\npi --mode rpc --no-session\n```\n\nCommands are JSON objects written to stdin. Responses and events are JSON objects written to stdout. Every record occupies one line.\n\nAdd an `id` to commands that need correlation. The matching response repeats that ID. Events generally have no command ID because they describe session activity rather than one request.\n\nA successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean the run completed. Continue consuming events through `agent_settled` when completion matters.\n\nRPC commands can change models, inspect state, manage sessions, run shell commands, and answer extension UI requests.\n\nExtension dialogs form a request-response subprotocol. Other extension UI updates are notifications that a client may display or ignore. TUI-only extension capabilities are unavailable or degraded outside interactive mode.\n\nFor Node.js or TypeScript integrations, prefer `RpcClient` from `@earendil-works/pi-coding-agent`. It starts a Pi RPC child process, correlates requests, exposes typed command methods, and delivers session events to listeners.\n\nThe [RPC client example](../examples/rpc-client.ts) sends one prompt, streams text and tool activity, waits for `agent_settled`, and shuts down the child process. It is included in the repository’s TypeScript checks.\n\n`RpcClient.promptAndWait()` installs its event listener before sending the prompt, avoiding a race with fast completions. For separate operations, subscribe before calling `prompt()` and call `waitForIdle()` only while a run is active.\n\nThe client requires a path to a runnable Pi CLI. The repository example points at `dist/cli.js`, so the package must be built before that example runs from a checkout.\n\nIf you are building a client without `RpcClient`, start with [RPC Protocol](rpc.md), then use [RPC Commands](rpc-commands.md) and [JSON Event Stream](json.md) as the wire references.\n\n## Fork and rebrand Pi\n\nA source fork can change the CLI name and configuration directory through `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"my-agent\",\n    \"configDir\": \".my-agent\"\n  }\n}\n```\n\nChange the top-level `bin` field to set the executable name. These settings affect the CLI banner, configuration paths, and derived environment variable names.\n\n## Examples and references\n\n- [RPC client](../examples/rpc-client.ts): typed Node.js integration\n- [RPC extension UI](../examples/rpc-extension-ui.ts): custom terminal client with extension dialogs\n- [Command Line](cli.md): startup options and mode selection\n- [JSON Event Stream](json.md): JSON event reference\n- [RPC Protocol](rpc.md): RPC lifecycle, framing, errors, and shutdown\n- [RPC Commands](rpc-commands.md): command and response reference\n- [RPC Extension UI](rpc-extension-ui.md): extension interaction subprotocol\n- [SDK examples](../examples/sdk/): in-process TypeScript integrations\n","packages/coding-agent/docs/cli.md":"<a id=\"cli-and-modes-reference\"></a>\n\n# Command Line\n\nThis page documents Pi's built-in command-line commands and options. Run `pi --help` or append `--help` to a command for the exact interface in your installed version. The top-level help also includes options registered by loaded extensions.\n\n```sh\npi [options] [--] [@files...] [messages...]\npi install <source> [options]\npi remove <source> [options]\npi uninstall <source> [options]\npi update [target] [options]\npi list\npi config [options]\npi auth <check|print-api-key|print-bearer-token> [options]\npi mcp <list|login|logout> [options]\n```\n\n<a id=\"modes\"></a>\n\n## Invocation and output\n\n```sh\npi\npi --print \"Summarize this repository\"\ngit diff | pi --print \"Review this change\"\npi --mode json \"Inspect this repository\" > events.jsonl\n```\n\nWith terminal stdin and stdout, Pi opens the terminal UI unless `--print`, `--mode json`, or `--mode rpc` selects another interface. When either stream is redirected and neither JSON nor RPC mode is selected, Pi uses print mode. See [CLI Integration](cli-integration.md) for choosing between interactive, print, JSON, RPC, and SDK integration.\n\n| Input | Behavior |\n|---|---|\n| `message` | Provide an initial prompt |\n| `@path` | Include a text file or image in the first prompt |\n| Piped stdin | Prepend its contents to the first prompt |\n| `--` | Stop option parsing so a prompt can begin with `-` |\n\nPi resolves `@path` from the current working directory. The working directory also controls project configuration, resource discovery, and session grouping.\n\n`--print` controls whether Pi runs once and exits. `--mode` selects the output interface. `--mode text` does not force one-shot execution when stdin and stdout are terminals; use `--print` for that behavior.\n\n| Option | Behavior |\n|---|---|\n| `-p`, `--print` | Run the supplied prompts, write the final assistant text to stdout, then exit |\n| `--mode text` | Select text output; still open the terminal UI when stdin and stdout are terminals |\n| `--mode json` | Run the supplied prompts, write JSONL events to stdout, then exit |\n| `--mode rpc` | Read JSONL commands from stdin and write responses and events to stdout until shutdown |\n| `--export <input> [output]` | Export a session file to HTML and exit; derive the destination when `output` is omitted |\n\nRPC mode rejects `@file` arguments. JSON and RPC modes reserve stdout for protocol records. See [JSON Event Stream](json.md) and [RPC Protocol](rpc.md).\n\n<a id=\"model-options\"></a>\n\n## Models\n\n```sh\npi --model sonnet:high\n```\n\nSee [Choose a Model](models.md) for model selection and [Providers](providers.md) for credentials.\n\n- `--provider <name>`<br>\n  Restricts `--model` lookup to one provider. It requires `--model`.\n- `--model <pattern>`<br>\n  Selects by exact ID or fuzzy ID/name match. It accepts `provider/id` and an optional `:<thinking>` suffix. If several providers match, Pi prefers the one with credentials, otherwise it asks for `--provider`.\n- `--api-key <key>`<br>\n  Uses a non-persistent API-key override. It requires a model selected through `--model` or `--models`. It does not choose between providers that offer the same model.\n- `--thinking <level>`<br>\n  Sets `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. It overrides a `--model` suffix and is clamped to the model's capabilities.\n- `--models <patterns>`<br>\n  Sets a comma-separated scope for startup and cycling. It accepts exact IDs, fuzzy matches, case-insensitive globs, and optional `:<thinking>` suffixes. A model offered by several providers is added for each, with a warning.\n- `--list-models [search]`<br>\n  Lists available models, optionally filtered by a fuzzy search, then exits.\n\n<a id=\"session-options\"></a>\n\n## Sessions\n\n```sh\npi --continue\n```\n\nSee [Sessions and Context](sessions.md) for resuming, forking, naming, and storing sessions.\n\n- `-c`, `--continue`<br>\n  Continues the most recent session for the current project.\n- `-r`, `--resume`<br>\n  Opens the session selector.\n- `--session <path|id>`<br>\n  Opens by file path, exact ID, or partial ID. Pi searches the current project first and offers to fork a cross-project match.\n- `--session-id <id>`<br>\n  Opens the exact project session ID or creates it if absent. IDs accept letters, numbers, `.`, `_`, and `-`.\n- `--fork <path|id>`<br>\n  Forks an existing session into a new session for the current project.\n- `--session-dir <dir>`<br>\n  Overrides storage and lookup. It takes precedence over `PI_CODING_AGENT_SESSION_DIR` and the `sessionDir` setting.\n- `--no-session`<br>\n  Uses an in-memory session that is not persisted.\n- `-n`, `--name <name>`<br>\n  Sets the session display name.\n\nConstraints:\n\n- Session IDs must start and end with a letter or number.\n- `--fork` cannot be combined with `--session`, `--continue`, `--resume`, or `--no-session`.\n- `--session-id` cannot be combined with `--session`, `--continue`, or `--resume`. Combine it with `--fork` to choose the new ID.\n\n<a id=\"tool-options\"></a>\n\n## Tools\n\n```sh\npi --tools read,grep,find,ls --print \"Review this project\"\n```\n\nSee [Settings](settings.md#tools) for configuring the default tool selection.\n\n- `-t`, `--tools <list>`<br>\n  Replaces the default selection with a comma-separated allowlist of built-in, extension, or custom tools. Entries are tool names or patterns where `*` matches any characters. MCP tools are kept unless an entry starts with `mcp__` (see [MCP tools](#mcp-tools)). A list of only `+name` and `-name` entries is not an allowlist; it changes the default selection instead.\n- `-xt`, `--exclude-tools <list>`<br>\n  Disables comma-separated tool names or patterns after all other selection options, MCP tools included.\n- `-nbt`, `--no-builtin-tools`<br>\n  Disables default built-in tools while retaining extension and custom tools.\n- `-nt`, `--no-tools`<br>\n  Starts with all built-in, extension, custom, and MCP tools disabled.\n\nDefault enabled tools are `read`, `bash`, `edit`, and `write`, unless `defaultTools` changes them. `--tools` with plain names replaces the whole selection, so name every tool you want. Like `defaultTools`, it also accepts a list of only `+name` and `-name` entries, which adds tools to or removes them from the default selection: `pi --tools +codemode,-write` keeps the other default tools, enables `codemode`, and disables `write`. These entries take exact tool names, not `*` patterns; use `--exclude-tools` to disable tools by pattern. Plain names and `+name`/`-name` entries cannot be mixed. `/reload` enables tools newly added to `defaultTools`, but a tool removed with `-name` stays removed.\n\n<a id=\"mcp-tools\"></a>\n\n`--tools` selects the tools declared to the model. It does not remove MCP tools, whose reach is set by their [exposure](mcp.md#control-tool-exposure): `pi --tools read,codemode` keeps every MCP tool callable from codemode scripts. An MCP tool that no entry names or matches is never declared directly, whatever its exposure; only `tool_search`, if listed, can load it. Once an entry starts with `mcp__`, `--tools` filters MCP tools too, so this keeps only the tools of the `radius` server:\n\n```sh\npi --tools read,bash,codemode,'mcp__radius__*'\n```\n\nThe MCP resource tools (`list_mcp_resources`, `list_mcp_resource_templates`, `read_mcp_resource`) count as MCP tools. To remove MCP tools, use `--exclude-tools 'mcp__*'` or [`--no-mcp`](#resource-options).\n\n| Built-in | Purpose |\n|---|---|\n| `read` | Read text files and supported images |\n| `bash` | Run shell commands |\n| `powershell` | Run PowerShell commands on Windows |\n| `edit` | Apply exact text replacements to an existing file |\n| `write` | Create or overwrite a file |\n| `grep` | Search file contents |\n| `find` | Find paths using glob patterns |\n| `ls` | List directory contents |\n\nBuilt-in extensions add two more tools. They are off by default; the MCP extension turns them on when an MCP server needs them (see [MCP](mcp.md#exposure)). To enable them yourself, name them in `--tools` or `defaultTools`.\n\n| Built-in extension | Purpose |\n|---|---|\n| `codemode` | Run JavaScript that calls the other tools, for example in parallel with `Promise.allSettled`; only the script's output reaches the model |\n| `tool_search` | Search tools that are not declared to the model (`codemode` and `deferred` exposure, such as MCP tools) and declare the matches for the next call |\n\n### Enable codemode\n\nTo turn on `codemode` for every session, add it to the default tools in `~/.pi/agent/settings.json` or a project's `.pi/settings.json`:\n\n```json\n{\n  \"defaultTools\": [\"+codemode\"]\n}\n```\n\nThis keeps `read`, `bash`, `edit`, and `write` and adds `codemode`. For one invocation, add it with `--tools`:\n\n```sh\npi --tools +codemode\n```\n\nCodemode is useful without MCP: scripts can run several tool calls in parallel, filter large output before it reaches the model, call classifier models such as TypeSafe's Jev through `models.classify()` (see [Classifier models](models.md#use-classifier-models)), and generate images through `models.generateImages()` (see [Image models](models.md#use-image-models)).\n\n### How codemode works\n\nScripts run in a QuickJS sandbox and reach the other tools through `tools.<name>(args)`. [Codemode](codemode.md) describes the script API, how tools are listed and found, the `store()` and `models` globals, and the limits.\n\n### Tool search\n\n`tool_search` is off by default; enable it with `\"defaultTools\": [\"+tool_search\"]` or `--tools`. It uses the same ranking as `searchTools()` over tools that are not declared yet and declares the matches for the next model call. Loaded tools are recorded in the session like other tool changes, so they stay declared on that branch.\n\n<a id=\"resource-options\"></a>\n\n## Resources\n\n```sh\npi --extension ./review.ts\n```\n\nSee [Configuration](configuration.md) for conventional directories and project trust, [Settings](settings.md#resources) for configured paths, and [Pi Packages](packages.md) for package sources.\n\n- `-e`, `--extension <path>`<br>\n  Loads an extension file or directory, or a built-in extension such as `builtin:mcp`, and is repeatable.\n- `-ne`, `--no-extensions`<br>\n  Disables discovered, configured, and built-in extensions. Explicit `-e` paths still load, so `pi -ne -e builtin:mcp` keeps only the built-in MCP support.\n- `--no-mcp`<br>\n  Disables the built-in MCP support for this run: no servers connect, and there are no MCP tools or `/mcp`. It does not affect an extension that replaces the built-in MCP support.\n- `--skill <path>`<br>\n  Loads a skill file or directory and is repeatable.\n- `-ns`, `--no-skills`<br>\n  Disables discovered and configured skills. Explicit `--skill` paths still load.\n- `--prompt-template <path>`<br>\n  Loads a prompt-template file or directory and is repeatable.\n- `-np`, `--no-prompt-templates`<br>\n  Disables discovered and configured templates. Explicit `--prompt-template` paths still load.\n- `--theme <path>`<br>\n  Loads a theme file or directory and is repeatable.\n- `--use-theme <name[/name]>`<br>\n  Selects the initial interactive theme for this run.\n- `--no-themes`<br>\n  Disables discovered and configured themes. Explicit `--theme` paths still load.\n- `-nc`, `--no-context-files`<br>\n  Disables `AGENTS.md` and `CLAUDE.md` discovery.\n\nResource paths apply only to the current process. Relative paths resolve from the current working directory.\n\n<a id=\"prompt-and-display-options\"></a>\n\n## Prompts and process\n\n```sh\npi --append-system-prompt ./instructions.md\n```\n\nSee [Configuration](configuration.md) for saved configuration, [Security](security.md#understand-project-trust) for project trust, and [Environment Variables](environment-variables.md) for process controls.\n\n- `--system-prompt <text|path>`<br>\n  Replaces the default system prompt with text or the contents of an existing file.\n- `--append-system-prompt <text|path>`<br>\n  Appends text or an existing file to the system prompt and is repeatable.\n- `--tui-mode <mode>`<br>\n  Uses `fullscreen` (default) or `regular` terminal mode.\n- `--verbose`<br>\n  Shows verbose interactive startup information, overriding `quietStartup`.\n- `-a`, `--approve`<br>\n  Trusts project-local configuration and resources for this process.\n- `-na`, `--no-approve`<br>\n  Ignores trust-gated project-local configuration and resources for this process.\n- `--offline`<br>\n  Disables automatic network activity, including model catalog refreshes. Equivalent to `PI_OFFLINE=1`.\n- `-h`, `--help`<br>\n  Shows help, including flags registered by loaded extensions, then exits.\n- `-v`, `--version`<br>\n  Shows the Pi version, then exits.\n\nExtensions may register additional long-form options. Unknown short options are rejected.\n\n## Package commands\n\n```sh\npi install npm:@scope/package\n```\n\nSee [Pi Packages](packages.md) for source formats, filtering, installation, and project scope.\n\n### Common tasks\n\n| Task | Command |\n|---|---|\n| Install a package | `pi install <source>` |\n| List configured packages | `pi list` |\n| Remove a package and its settings entry | `pi remove <source>` |\n| Configure which package resources load | `pi config` |\n\nAdd `--local` or `-l` to `install`, `remove`, `uninstall`, or `config` to use project settings instead of global settings.\n\n### Update Pi or packages\n\nRunning `pi update` without a target updates Pi itself.\n\n| Task | Command |\n|---|---|\n| Update Pi | `pi update` |\n| Update all installed packages | `pi update --extensions` |\n| Update one installed package | `pi update <source>` |\n| Refresh model catalogs | `pi update --models` |\n| Update Pi and all installed packages | `pi update --all` |\n\nAdd `--force` to reinstall Pi when the selected update includes Pi.\n\n`pi update` cannot update Pi when another package manager provides it, such as Nix. Update Pi with that package manager, for example `nix profile upgrade pi`. Package and model catalog updates still work.\n\n### Aliases and command options\n\n- `pi uninstall <source>` is an alias for `pi remove <source>`.\n- `pi update --self`, `pi update self`, and `pi update pi` are aliases for `pi update`.\n- `pi update --extension <source>` is an alias for `pi update <source>`.\n- `-a`, `--approve` trusts project-local files for one command. `-na`, `--no-approve` ignores trust-gated project-local files.\n- Append `-h` or `--help` to a command for its exact usage and option constraints.\n\n## Credential commands\n\n```sh\npi auth check --provider openai --json\n```\n\nAuthentication commands require `--provider <provider>` or `--model <model>`. See [Providers](providers.md) for supported methods.\n\n| Command | Description |\n|---|---|\n| `pi auth check` | Print `ready`, `not_ready`, or `invalid`; exit with status `0`, `1`, or `2`, respectively |\n| `pi auth print-api-key` | Print the resolved API key |\n| `pi auth print-bearer-token` | Print a resolved OAuth bearer token |\n\n| Option | Applies to | Description |\n|---|---|---|\n| `--provider <provider>` | All | Resolve credentials for a provider |\n| `--model <model>` | All | Resolve credentials from a model; may be combined with `--provider` |\n| `--json` | `auth check` | Write the structured result as JSON |\n| `--credentials` | `auth check` | Emit the resolved credential when ready |\n| `--no-refresh` | `auth check` | Do not refresh expired OAuth credentials; refresh is the default |\n| `--min-expiry <duration>` | `print-bearer-token` | Require remaining token lifetime using `ms`, `s`, `m`, or `h`, such as `30m` |\n\nCredential-printing commands write secrets to stdout.\n\n## MCP commands\n\nThese commands work outside a session, so agents can run them through `bash`. See [MCP Servers](mcp.md).\n\n| Command | Description |\n|---|---|\n| `pi mcp add <server> [options] -- <command> [args...]` | Add or replace a stdio server in `mcp.json`; `--env KEY=VALUE` (repeatable) and `--cwd <dir>` set its environment and working directory. Arguments after the command are passed to it |\n| `pi mcp add <server> [options] --url <url>` | Add or replace a streamable HTTP server; `--header KEY=VALUE` (repeatable), `--bearer-token-env-var <NAME>` (sends `Authorization: Bearer ${NAME}`), `--oauth-client-id`, `--oauth-client-secret`, `--oauth-callback-port`, and `--oauth-client-name` configure authentication |\n| `pi mcp remove <server>` | Remove a server from `mcp.json`; stored OAuth credentials are kept |\n| `pi mcp list [--json]` | Connect to every enabled server and print its state, tools, and errors; exit with `1` when a config entry is invalid or an enabled server is not connected |\n| `pi mcp login <server> [--timeout <seconds>]` | Sign in to an OAuth server: open the authorization page and wait for the browser; a terminal also accepts the pasted redirect URL. `--timeout` limits the whole sign-in, including requests to the authorization server (default 300 seconds) |\n| `pi mcp logout <server>` | Delete the stored OAuth credentials of a server |\n\n`add` and `remove` change `~/.pi/agent/mcp.json`, or `.pi/mcp.json` in the current directory with `--local` (`-l`). `add` also takes `--exposure <mode>` (see [Exposure](mcp.md#exposure)) and `--description <text>` and does not connect; run `pi mcp list` to check the server.\n\nProject `.pi/mcp.json` files are only read for projects that are already trusted.\n","packages/coding-agent/docs/codemode.md":"# Codemode\n\nThe `codemode` tool lets the model write a JavaScript script that calls pi's other tools and runs non-LLM models, such as classifiers and image models. Only the script's output reaches the model, so a script can run calls in parallel and filter large results before the model sees them. To turn it on, see [Enable codemode](cli.md#enable-codemode).\n\n## Scripts\n\nThe tool input is raw JavaScript source, not JSON and not a markdown code fence. It runs as the body of an async function in a QuickJS sandbox, so top-level `await` and `return` work. The sandbox has no Node APIs, file system, network, or timers; scripts reach the outside world only through tools and `models`.\n\nA script may start with an options line. It must be the first line; an options line anywhere else fails the call before the script runs:\n\n```js\n// @options: {\"max_output_tokens\": 2000, \"timeout_ms\": 60000}\n```\n\n- `max_output_tokens` (default 10000) limits the output. Longer output keeps its start and end, and the full text is written to a temp file whose path is included in the result. A script fails when its output passes 16777216 characters of text and base64 image data or 100000 `text()`, `image()`, and `console` calls; write large data to a file with a tool instead.\n- `timeout_ms` is a hard deadline for the whole script, including any wait for a free worker. It is unset by default. Image generation can take minutes, so do not set a short deadline for scripts that generate images.\n\nThe result starts with `Script completed` or `Script failed`, the wall time, and the output. Text and image items appear in order, each on its own line. When the output has more than one text item (from `text()` or `return`), each starts with a `==> text N/M <==` line. `console` calls follow in one `<console_output>` block with one line per call. A failed script keeps its partial output, followed by `Script error:` and the error. Tool calls are real: calls made before a failure are not undone. Calls still running when the script ends are cancelled, and unawaited promises are discarded.\n\n## Globals\n\n| Global | Purpose |\n|---|---|\n| `tools.<name>(args)` | Call a tool. See [Call tools](#call-tools). |\n| `text(value)` | Add a text item to the output. Strings are added as is, other values as JSON. |\n| `image(value)` | Add an image to the output: a base64 `data:` URL, an `{ image_url }` object, or an image block `{ type: \"image\", data, mimeType }` such as those returned by MCP tools and `models.generateImages()`. Remote URLs are not supported. PNG, JPEG, GIF, and WebP are accepted. Each image is also saved to a temp file, and the result names the path before the image. |\n| `console.log(...)` | Add a line to the `<console_output>` block after the other output. Arguments are joined with spaces; `info`, `warn`, `error`, and `debug` do the same. |\n| `return value` | A top-level `return` adds the value like `text()`. |\n| `exit()` | End the script successfully. |\n| `store(key, value)` / `load(key)` | Keep small JSON values across `codemode` calls. See [Store values](#store-values). |\n| `ALL_TOOLS` | Every callable tool as `{ name, description }`, including tools the description does not list. |\n| `searchTools(query, { limit?, namespace? })` | Rank callable tools by relevance (BM25, default limit 8). Resolves to `{ name, description }[]`. |\n| `describeTool(name)` | Resolves to a tool's description and TypeScript declaration, or `undefined`. |\n| `describeNamespace(name)` | Resolves to `{ name, description?, instructions?, tools }` for a namespace such as an MCP server, or `undefined`. |\n| `models` | List and run non-LLM models. See [Models](#models). |\n\n## Call tools\n\nEvery tool the session can call is a method of `tools`, named by its identifier: characters that are not valid in a JavaScript identifier become `_`, so the MCP tool `mcp__dev-radius__search` is `tools.mcp__dev_radius__search`. Each method takes one object with the tool's arguments.\n\nWhat a call resolves to depends on the tool:\n\n- Tools with an output schema resolve to a structured value. `bash` resolves to `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`, also for non-zero exit codes. Its `output` is not limited to the 2000 lines or 50KB the model sees: it holds up to 1 MiB, and longer output keeps its first and last 512 KiB around an omission marker, with `truncated` set and the full output in `full_output_path`.\n- MCP tools resolve to their `CallToolResult`, including `isError` and `structuredContent`.\n- `read` resolves to the file's text, or for an image to an image block `{ type: \"image\", data, mimeType, note }` that `image()` shows. `data` is the base64 image the model would see and `note` the text that goes with it, such as resize hints.\n- Other tools, such as `edit` and `write`, resolve to their text output.\n\nA call that fails, is blocked, or gets invalid arguments rejects with an `Error` that carries the tool's error text. Use `Promise.allSettled()` to keep the results of the calls that succeed.\n\nThe `codemode` description lists tools with their TypeScript declarations, grouped by namespace (for example one MCP server). Tools with `deferred` exposure, which includes MCP tools with the default `codemode` exposure, are not listed, so the description stays the same while MCP servers connect. Listed declarations share a budget of 3000 estimated tokens (`codemode.inlineBudget` in [settings](settings.md#tools)). Scripts find the other tools with `searchTools()`, `describeTool()`, `describeNamespace()`, or by filtering `ALL_TOOLS`.\n\nWhile `codemode` is active, `codemode.mode` in [settings](settings.md#tools) decides how the other tools are presented. With `on` (default) declared tools stay declared, and their descriptions say how to call them from scripts. With `only` they are hidden from the model and listed in the `codemode` description instead, so the model calls them through scripts. Tool declarations in the `codemode` description, `describeTool()`, and `ALL_TOOLS` carry the tools' prompt guidelines, since the system prompt rules only cover declared tools.\n\n## Store values\n\n`store(key, value)` keeps a JSON value under a string key for later `codemode` calls; storing `undefined` deletes the key. `load(key)` returns the value, or `undefined`. Writes are kept only when the script succeeds: each successful script that stores values appends a `codemode-store` custom entry to the session, so resumed sessions keep the values and each branch sees only the values written on its path.\n\nThe store is for small state such as IDs, cursors, or summaries. One value may have at most 262144 characters of JSON and all values together at most 1048576. Do not store image data; show images with `image()`, which also saves them to a temp file.\n\n## Models\n\n`models` reaches the model catalog and runs non-LLM models with the session's credentials: classifiers, which answer typed questions about JSON state and, for some models, images, and image models, which generate images. Chat models are listed but cannot be run from scripts. Which classifier and image models exist is described in [Use classifier models](models.md#use-classifier-models) and [Use image models](models.md#use-image-models).\n\n```ts\ntype ModelType = \"chat\" | \"image\" | \"classifier\";\n\n/** A catalog entry. `provider` and `id` identify it; other fields depend on the type. */\ninterface ModelInfo {\n  type?: ModelType;\n  provider: string;\n  id: string;\n  name: string;\n  api: string;\n  input: (\"text\" | \"image\")[];\n  contextWindow?: number;\n  [key: string]: unknown;\n}\n\ndeclare const models: {\n  /** Every known model of a type, optionally for one provider. */\n  getModelsOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** Models of a type whose provider has working credentials. */\n  getAvailableOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** One catalog entry, or undefined. */\n  getModelOfType(type: ModelType, provider: string, id: string): Promise<ModelInfo | undefined>;\n  /** Answer `context.questions` about `context.state`; answers are in `result.answers` by question ID. */\n  classify(model: ModelInfo, context: ClassifierContext): Promise<ClassifierResult>;\n  /** Generate images from `context.input` text and image blocks; show `result.output` blocks with image(). Can take minutes. */\n  generateImages(model: ModelInfo, context: ImagesContext): Promise<ImagesResult>;\n};\n```\n\n`classify()` and `generateImages()` use only the `provider` and `id` of `model`, so `{ provider, id }` works as well. They do not throw on provider errors: check `stopReason` and `errorMessage`. At most four such calls run at once per script; more calls wait for a free slot, so `Promise.all()` over many items is fine. Their usage is added to the `codemode` tool result and counts toward the session cost.\n\nModel IDs differ between providers, for example `typesafe/jev-latest` and `openrouter/typesafe/jev-1.13`. Use `models.getAvailableOfType(type)` to find the IDs that work with the current credentials.\n\n### Classify\n\n```ts\ninterface ClassifierContext {\n  /** The data to classify. */\n  state: Record<string, unknown>;\n  /** Images judged together with `state`. Only models whose `input` includes \"image\" accept them. */\n  images?: { type: \"image\"; data: string; mimeType: string }[];\n  /** Questions by ID. One call answers all of them. */\n  questions: Record<string, ClassifierQuestion>;\n}\n\ntype ClassifierQuestion =\n  /** Pick one label. `criteria` maps each label to what it means. */\n  | { type: \"choice\"; instructions: string; criteria: Record<string, string> }\n  /** Score on an ordered scale. `criteria` describes each level, lowest first. */\n  | { type: \"score\"; instructions: string; criteria: string[] }\n  /** Yes or no. */\n  | { type: \"bool\"; instructions: string; criteria: { true: string; false: string } };\n\ninterface ClassifierResult {\n  provider: string;\n  model: string;\n  /** Answers by question ID. */\n  answers: Record<string, ClassifierAnswer>;\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype ClassifierAnswer =\n  | { type: \"choice\"; choice: string; probabilities: Record<string, number>; confidence: number }\n  /** `score` is the expected level index, from 0 to `criteria.length - 1`. */\n  | { type: \"score\"; score: number; confidence: number }\n  /** Probability of `true`. */\n  | { type: \"bool\"; probability: number };\n\n/** Token counts and cost in USD, when the service reports them. */\ntype ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };\n```\n\nClassify several items by calling `classify()` once per item. This script sorts feedback messages, for example ones a tool returned earlier in the script:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst results = await Promise.all(\n  messages.map((message) =>\n    models.classify(jev, {\n      state: { message },\n      questions: {\n        sentiment: {\n          type: \"choice\",\n          instructions: \"How does the user feel about the product?\",\n          criteria: { positive: \"Satisfied or happy\", negative: \"Unhappy or frustrated\", neutral: \"Neither\" },\n        },\n        urgency: {\n          type: \"score\",\n          instructions: \"How urgently does this need a reply?\",\n          criteria: [\"no reply needed\", \"reply this week\", \"reply today\"],\n        },\n      },\n    }),\n  ),\n);\nreturn results.map((result, i) =>\n  result.stopReason === \"stop\"\n    ? { message: messages[i], sentiment: result.answers.sentiment.choice, urgency: result.answers.urgency.score }\n    : { message: messages[i], error: result.errorMessage },\n);\n```\n\nClassifiers whose `input` includes `\"image\"` also judge images. `tools.read()` returns an image file as an image block that `images` accepts. Other classifiers return an error result when `images` is not empty.\n\n```js\nconst luna = await models.getModelOfType(\"classifier\", \"openai\", \"gpt-6-luna\");\nconst photo = await tools.read({ path: \"screenshot.png\" });\nconst result = await models.classify(luna, {\n  state: { task: \"Settings page redesign\" },\n  images: [photo],\n  questions: {\n    broken: {\n      type: \"bool\",\n      instructions: \"Does the screenshot show a broken layout?\",\n      criteria: { true: \"Overlapping, cut-off, or misaligned elements\", false: \"Clean layout\" },\n    },\n  },\n});\n```\n\n### Generate images\n\n```ts\ninterface ImagesContext {\n  /** The prompt as text blocks, plus image blocks to edit or use as references. */\n  input: (TextBlock | ImageBlock)[];\n}\n\ninterface ImagesResult {\n  provider: string;\n  model: string;\n  /** Generated images, and text blocks for models that also return text. */\n  output: (TextBlock | ImageBlock)[];\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype TextBlock = { type: \"text\"; text: string };\n/** `data` is base64. */\ntype ImageBlock = { type: \"image\"; data: string; mimeType: string };\n```\n\nShow generated images with `image(block)`. Do not print `data` with `text()`, `console`, or `return`: it is large and the model cannot read it as text. `image()` also saves each image to a temp file and puts its path in the result, so a later turn can copy or move the file.\n\n```js\n// @options: {\"timeout_ms\": 300000}\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) {\n  if (block.type === \"image\") image(block);\n  else text(block.text);\n}\n```\n\n## Limits\n\n- A script's VM has 256 MB of memory. Running out throws `InternalError: out of memory`; filter or aggregate large data instead of accumulating it.\n- On Bun, stopping a script or reaching its deadline ends the call promptly, but a built-in that does not poll QuickJS's interrupt handler can keep computing in its worker until it returns.\n- At most two codemode workers may be alive across all sessions in one pi process; `PI_CODEMODE_MAX_WORKERS` changes the limit. A cancelled worker occupies its slot until it exits. At capacity, new scripts wait for a free worker in arrival order. A script that gets no worker within 60 seconds, or before its `timeout_ms`, fails without running.\n- A script that waits on a promise that can never settle (no tool call pending) fails immediately, since there are no timers.\n- Scripts cannot start other `codemode` scripts.\n","packages/coding-agent/docs/compaction.md":"# Compaction Reference\n\nThis reference describes automatic compaction, branch summarization, persisted entries, and extension hooks. For the user workflow, see [Sessions and Context](sessions.md#manage-conversation-context).\n\n**Source files** ([pi](https://github.com/earendil-works/pi)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - Auto-compaction logic\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - Branch summarization\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - Shared utilities (file tracking, serialization)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts) - Entry types (`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) - Extension event types\n\nFor TypeScript definitions in your project, inspect `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Overview\n\nPi has two summarization mechanisms:\n\n| Mechanism | Trigger | Purpose |\n|-----------|---------|---------|\n| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |\n| Branch summarization | `/tree` navigation | Preserve context when switching branches |\n\nBoth use closely related structured formats and track file operations cumulatively. Summarization requests disable prompt-cache writes because these one-off prompts are unlikely to be reused.\n\n## Compaction\n\n### When It Triggers\n\nAuto-compaction triggers when:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nBy default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`). It only decides when compaction starts. Summary requests use the model's output limit, reduced when needed so input and output fit the context window.\n\nDuring a multi-turn agent run, Pi checks the canonical projected context after tools finish and their results are appended, before starting the next assistant response. If the threshold is crossed, Pi compacts during `prepareNextTurn`, then performs the existing catch-up steering poll before `turn_start`. It skips this between-turn check when the completed tool batch terminates the run and no queued message requires another response. Pi also checks before a new user prompt and performs final-attempt overflow recovery after the low-level run ends.\n\nA provider context-overflow error or an early final `stopReason: \"length\"` can select one compact-and-retry recovery attempt. Length responses with tool calls retain their synthetic failed tool results and follow the ordinary tool/queue scheduler rather than forcing the run to end.\n\nYou can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary.\n\n### How It Works\n\n1. **Find cut point**: Walk backwards through the finalized session projection, accumulating token estimates until `keepRecentTokens` (default 20k, configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`) is reached\n2. **Extract messages**: Collect projected messages from the previous kept boundary (or session start) up to the cut point\n3. **Generate summary**: Call LLM to summarize with structured format, passing the previous summary as iterative context when present\n4. **Append entry**: Save `CompactionEntry` with summary and `firstKeptEntryId`\n5. **Rebuilds context**: Session rebuilds the context for the next request, using summary + messages from `firstKeptEntryId` onwards\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\nOn repeated compactions, the summarized span starts at the previous compaction's kept boundary (`firstKeptEntryId`), not at the compaction entry itself, falling back to the entry after the previous compaction if that kept entry cannot be found in the path. A retain-none compaction records its own ID as `firstKeptEntryId`; repeated compaction starts after that entry. This preserves messages that survived the earlier compaction by including them in the next summarization pass as well. Pi also recalculates `tokensBefore` from the rebuilt, context-edited session projection before writing the new `CompactionEntry`, so the token count reflects the actual pre-compaction context being replaced. Omitted raw entries remain stored but do not affect cut selection, summaries, checkpoints, or token estimates.\n\n### Overflow and Length Recovery Ordering\n\nRecovery preserves the existing lifecycle and queue order. The completed attempt remains visible to `turn_end` and `agent_end`; post-run recovery then repairs persisted model context before a fresh retry:\n\n```text\npersist final assistant response\n→ extension/public turn_end\n→ extension/public agent_end\n→ append context_edit omissions for the selected attempt\n→ for overflow/length: run session_before_compact and append compaction on success\n→ start the retry as a fresh run\n```\n\nIf recovery compaction fails or is cancelled, Pi keeps the omission edits, appends no compaction, and schedules no internal retry. Existing queued work remains governed by ordinary steering and follow-up rules. `agent_before_settle` sees the repaired projection after recovery processing. Raw transcript history, exports, billing totals, and history-search extensions can still inspect the omitted attempt.\n\n### Split user-message spans\n\nA user-message span starts with a user message and includes all turns until the next user message. Normally, compaction cuts at user-message boundaries.\n\nWhen one user-message span exceeds `keepRecentTokens`, the cut point lands within that span at an assistant message. This is a split user-message span:\n\n```\nSplit user-message span (one span exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no earlier user-message spans)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\nFor split user-message spans, Pi generates two summaries and merges them:\n1. **History summary**: Previous context (if any)\n2. **User-message-span prefix summary**: The early part of the split user-message span\n\n### Cut Point Rules\n\nValid cut points are:\n- User messages\n- Assistant messages\n- BashExecution messages\n- Custom messages (custom_message, branch_summary)\n\nNever cut at tool results (they must stay with their tool call).\n\nPreparation advances the kept boundary into a context-invisible suffix only when that suffix contains an omitted assistant attempt and no unomitted context-producing entries. Recovery `context_edit` omissions satisfy this rule; intrinsically context-invisible metadata may coexist with them. Metadata alone and newly appended custom messages do not move the cut. A replacement edit affecting the candidate input or summarized prefix also blocks advancement because the omitted assistant answered the pre-edit input; replacements of suffix entries that are ultimately omitted remain safe. This allows an over-budget recovered input to be summarized while retaining the edits that keep the abandoned attempt omitted, without making bookkeeping change whether new model input is preserved verbatim.\n\n### CompactionEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions can store any JSON-serializable data in `details`. The default compaction tracks file operations, but custom extension implementations can use their own structure. Generated and extension-provided summaries store their LLM `usage` when available so session totals include summarization work.\n\nSee [`prepareCompaction()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) and [`compact()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) for the implementation. For direct programmatic summarization, `generateSummary()` returns the summary text and `generateSummaryWithUsage()` returns `{ text, usage }`.\n\n## Branch Summarization\n\n### When It Triggers\n\nWhen you use `/tree` to navigate to a different branch, Pi offers to summarize the work you're leaving. This injects context from the left branch into the new branch.\n\n### How It Works\n\n1. **Find common ancestor**: Deepest node shared by old and new positions\n2. **Collect entries**: Walk from old leaf back to common ancestor\n3. **Prepare with budget**: Include messages up to token budget (newest first)\n4. **Generate summary**: Call LLM with structured format\n5. **Append entry**: Save `BranchSummaryEntry` at navigation point\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### Cumulative File Tracking\n\nDefault compaction and branch summarization track files cumulatively. Both extract file operations from tool calls in the messages being summarized. Compaction also carries file lists from the previous Pi-generated compaction. Branch summarization carries file lists from Pi-generated branch summaries in the entries it summarizes.\n\nFile tracking therefore accumulates across default compactions and nested default branch summaries. Pi does not automatically carry file lists from extension-generated summaries whose `fromHook` field is `true`; extensions manage their own `details` format.\n\n### BranchSummaryEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nSame as compaction, extensions can store custom data in `details`.\n\nSee [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), and [`generateBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) for the implementation.\n\n## Summary Format\n\nBoth formats include Goal, Constraints & Preferences, Progress, Key Decisions, and Next Steps. Compaction summaries also include Critical Context. Branch summaries stop after Next Steps. Pi appends file lists to either format when relevant.\n\nCompaction summaries use this format:\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### Message Serialization\n\nBefore summarization, messages are serialized to text via [`serializeConversation()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts):\n\n```\n[User]: What they said\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\nThis prevents the model from treating it as a conversation to continue.\n\nThinking blocks are omitted. Providers return summaries of the real reasoning, and replayed thinking is often not part of the model's live context, so including it could make the summarization request larger than the context it summarizes.\n\nTool results are truncated to 2000 characters during serialization. Content beyond that limit is replaced with a marker indicating how many characters were truncated. This keeps summarization requests within reasonable token budgets, since tool results (especially from `read` and `bash`) are typically the largest contributors to context size.\n\n## Custom Summarization via Extensions\n\nExtensions can intercept and customize both compaction and branch summarization. See [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for event type definitions.\n\n### session_before_compact\n\nFired before auto-compaction or `/compact`. Can cancel or provide custom summary. See `SessionBeforeCompactEvent` and `CompactionPreparation` in the types file.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - user-message-span prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - effective settings after applying model overrides\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### Converting Messages to Text\n\nTo generate a summary with your own model, convert messages to text using `serializeConversation`:\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\nSee [custom-compaction.ts](../examples/extensions/custom-compaction.ts) for a complete example using a different model.\n\n### session_compact_failed\n\nFired when manual or automatic compaction fails or is aborted. This is useful for telemetry extensions that need to pair `session_before_compact` attempts with terminal outcomes.\n\n```typescript\npi.on(\"session_compact_failed\", async (event, ctx) => {\n  const { reason, errorMessage, aborted, willRetry, fromExtension } = event;\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // errorMessage - present for non-abort failures\n  // aborted - true for canceled/aborted compactions\n  // willRetry - whether the aborted turn would have retried after compaction\n  // fromExtension - whether extension-provided compaction content was being used\n});\n```\n\n### session_before_tree\n\nFired before `/tree` navigation. Always fires regardless of whether user chose to summarize. Can cancel navigation or provide custom summary.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\nSee `SessionBeforeTreeEvent` and `TreePreparation` in the types file.\n\n## Settings\n\nConfigure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`:\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `enabled` | `true` | Enable auto-compaction |\n| `reserveTokens` | `16384` | Free context tokens below which auto-compaction starts |\n| `keepRecentTokens` | `20000` | Recent tokens to keep (not summarized) |\n\nDisable auto-compaction with `\"enabled\": false`. You can still compact manually with `/compact`.\n\n### Per-model overrides\n\nUse `compaction.modelOverrides` to tune token budgets for different models:\n\n```json\n{\n  \"compaction\": {\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000,\n    \"modelOverrides\": {\n      \"some-provider/big-model\": {\n        \"reserveTokens\": 400000\n      }\n    }\n  }\n}\n```\n\nFor a model with a 1M context window, this override triggers compaction above 600K tokens and keeps the ordinary 20000 recent tokens. Other models retain the ordinary 16384-token reserve.\n\nKeys are exact, case-sensitive `provider/modelId` values, including any slashes within the model ID. Each `reserveTokens` and `keepRecentTokens` value falls back independently from the model override to the ordinary setting to the built-in default. Values must be non-negative safe integers. Invalid values in the matching model override produce an error when read; only omitted fields fall back to the ordinary setting. Model override entries must be objects. Invalid ordinary token settings produce an error when read, even if the active model has a valid override. Only omitted ordinary values use built-in defaults. `enabled` remains global, not model-specific.\n\nThese resolved values are used for manual compaction, all automatic threshold checks, overflow recovery, and extension-visible `preparation.settings`. Model switches affect subsequent checks and compactions without changing ordinary settings. Compaction already in progress uses the model and settings captured for that operation. Branch summarization settings are unaffected.\n\nOverrides work in both global and project settings. The files merge recursively before lookup, so a global model-specific value beats a project-wide fallback; a project must override that model entry to change it. See [Settings](settings.md#per-model-compaction-overrides) for details.\n","packages/coding-agent/docs/configuration.md":"# Configuration\n\nPi supports user-level and project configuration. User-level configuration lives in the agent directory, which defaults to `~/.pi/agent`. Project configuration lives in `.pi` under the working directory and loads after [project trust](security.md#understand-project-trust) is granted. The only exception is `sessionDir`, which Pi reads before resolving trust so it can locate sessions.\n\nIn interactive mode, use `/settings` to change common preferences. For other options, ask Pi to update the configuration or edit the relevant files directly. Run `/reload` after manually changing settings, keybindings, instructions, or resources.\n\n## Agent directory\n\nThe agent directory is shown as `<agent-dir>` below. Set its location with the `PI_CODING_AGENT_DIR` environment variable or the SDK's [`agentDir`](sdk.md) option.\n\n| Path | Responsibility |\n|---|---|\n| `<agent-dir>/settings.json` | User-level [settings](settings.md), including preferences, defaults, resource paths, and Pi package declarations. |\n| `<agent-dir>/keybindings.json` | Custom terminal UI and application [keybindings](keybindings.md). |\n| `<agent-dir>/mcp.json` | [MCP servers](mcp.md) available in every project. |\n| `<agent-dir>/models.json` | [Compatible endpoints, models, and model overrides](models.md#configure-a-compatible-endpoint). |\n| `<agent-dir>/auth.json` | Saved API keys and OAuth credentials. |\n| `<agent-dir>/AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, or `CLAUDE.MD` | User instructions applied across working directories. |\n| `<agent-dir>/SYSTEM.md` | Replaces Pi’s default system prompt. |\n| `<agent-dir>/APPEND_SYSTEM.md` | Adds instructions to Pi’s system prompt. |\n| `<agent-dir>/extensions/` | User [extensions](extensions.md). |\n| `<agent-dir>/skills/` | User [skills](skills.md) and supporting files. |\n| `<agent-dir>/prompts/` | User [prompt templates](prompt-templates.md) exposed as slash commands. |\n| `<agent-dir>/themes/` | User [theme](themes.md) files. |\n\n## Project `.pi` directory\n\n| Path | Responsibility |\n|---|---|\n| `.pi/settings.json` | Project-level [settings](settings.md), resource paths, and Pi package declarations. |\n| `.pi/mcp.json` | Project [MCP servers](mcp.md). |\n| `.pi/SYSTEM.md` | Replaces the system prompt for the project. |\n| `.pi/APPEND_SYSTEM.md` | Adds project-specific instructions to the system prompt. |\n| `.pi/extensions/` | Project extensions. |\n| `.pi/skills/` | Project skills and supporting files. |\n| `.pi/prompts/` | Project prompt templates exposed as slash commands. |\n| `.pi/themes/` | Project theme files. |\n\nFor `SYSTEM.md` and `APPEND_SYSTEM.md`, the trusted project file takes precedence over the corresponding agent-directory file. Files with the same name are not combined.\n\n## Context files\n\nContext files are separate from project `.pi` configuration. Pi loads them from the agent directory, the working directory, and its parent directories. A context file applies whenever Pi runs in its directory or anywhere below it.\n\nAn `AGENTS.override.md` replaces `AGENTS.md` or `CLAUDE.md` only in the same directory. It does not suppress context files from the agent directory or other directories.\n\nContext-file discovery does not require project trust.\n","packages/coding-agent/docs/containerization.md":"# Run Pi in an isolated environment\n\nUse an isolated environment to limit the files, credentials, processes, and network services that generated commands can access or affect.\n\nYou can isolate the complete Pi process or keep Pi on the host and route selected tools into an isolated environment.\n\n## Choose an isolation method\n\n| Method | Where Pi runs | What is isolated | Credential handling | Best for |\n|---|---|---|---|---|\n| Plain Docker | Container | Pi, built-in tools, `!` commands, and extensions | Credentials passed into the container | A straightforward local container boundary |\n| Docker Sandboxes | Managed sandbox | Pi, built-in tools, `!` commands, and extensions | Provider credentials remain on the host and are substituted by the proxy | Managed local isolation without exposing the real provider key |\n| OpenShell | Local or remote sandbox | Pi, built-in tools, `!` commands, and extensions | Policy-controlled credentials and inference routing | Filesystem, process, network, and credential policies |\n| Gondolin extension | Host | Built-in tools and `!` commands | Stored Pi credentials remain on the host, but commands inherit host environment variables | A local micro-VM for tool execution while retaining the host interface |\n\nThe method changes where extensions run. When the complete Pi process runs inside an isolated environment, its extensions run there too. When host Pi delegates built-in tools through Gondolin, other extension tools still run on the host unless they also delegate their work.\n\n## Decide what Pi can access\n\nAn isolated process can still affect resources you expose to it:\n\n- A read-write host mount lets Pi modify those host files.\n- Mounting `~/.pi/agent` exposes your Pi credentials, settings, extensions, and sessions.\n- Environment variables passed into a container are available to processes inside it.\n- Network access may allow code or tool output to leave the environment.\n- Tool-only isolation does not constrain the host Pi process or extension tools that do not use the isolated backend.\n\nExpose only the working folder, credentials, and network destinations needed for the task. Use read-only mounts or copy files into and out of the environment when you do not want writes to affect the host.\n\n## Run Pi in plain Docker\n\nPlain Docker provides the simplest whole-process container boundary.\n\n### Build the image\n\nCreate `Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\nBuild it from the directory containing the file:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n```\n\n### Start Pi\n\nFrom the working folder you want Pi to access, run:\n\n```bash\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\nReplace `ANTHROPIC_API_KEY` with the credential required by your provider. The named `pi-agent-home` volume keeps container-local settings, credentials, and sessions between runs.\n\nDo not mount the host's `~/.pi/agent` unless the container should have access to your host Pi configuration and credentials.\n\n### Verify the workspace\n\nInside Pi, run:\n\n```text\n!pwd\n```\n\nThe command should report `/workspace`. Changes under `/workspace` write through to the mounted host folder. Remove the bind mount or use a read-only mount when that is not acceptable.\n\n## Run Pi with Docker Sandboxes\n\n[Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) runs the complete Pi process inside a managed sandbox. Its proxy can keep the real provider credential on the host and substitute it when requests leave the sandbox.\n\nConfigure credentials before creating the sandbox. Do not run `/login` inside the sandbox because that writes a real credential into it.\n\n### Use a Claude Pro or Max token\n\nGenerate the token with `claude setup-token` on a machine with Claude Code. If an `anthropic` secret is already configured, remove it first so the proxy does not add an API-key header alongside the bearer token:\n\n```bash\nsbx secret rm anthropic\n\nsbx secret set-custom \\\n  --host api.anthropic.com \\\n  --env ANTHROPIC_OAUTH_TOKEN \\\n  --placeholder 'sk-ant-oat01-{rand}'\n```\n\n`sbx secret set-custom` reads the real token from standard input. The sandbox receives an OAuth-shaped placeholder, which the proxy replaces only for requests to the configured host.\n\nFor an Anthropic API key, use `sbx secret set anthropic` instead.\n\n### Start Pi\n\nRun this from the working folder you want mounted:\n\n```bash\nsbx run --kit \"docker.io/sbx/pi-kit:latest\" pi\n```\n\nFor an existing sandbox, run Pi non-interactively with:\n\n```bash\nsbx exec <sandbox-name> -- pi -p \"list the failing tests\"\n```\n\nSee the [Pi kit documentation](https://github.com/docker/sbx-kits-contrib/tree/main/pi) for other providers, troubleshooting, and image pinning.\n\n## Run Pi with OpenShell\n\n[NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) provides local or remote sandboxes with filesystem, process, network, credential, and inference policies.\n\n### Select a gateway\n\nEvery sandbox requires an active gateway:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\n### Create the sandbox\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nPi, its built-in tools, `!` commands, and extension tools run inside the OpenShell boundary.\n\n### Transfer files to a remote sandbox\n\nA remote gateway does not bind-mount your host working folder. Clone the repository inside the sandbox or transfer files explicitly:\n\n```bash\nopenshell sandbox upload pi-sandbox ./working-folder /workspace\nopenshell sandbox download pi-sandbox /workspace/working-folder ./working-folder-out\n```\n\nOpenShell inference routing can keep raw model credentials outside the sandbox. When configured, point Pi at the corresponding OpenAI-compatible or Anthropic-compatible endpoint exposed by the gateway.\n\n## Route tools through Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) is a local Linux micro-VM. Its example extension keeps the Pi process and file-based provider credentials on the host while routing the built-in tools and user `!` commands into the VM.\n\nCommands inside the VM inherit the host process environment. Provider keys supplied through environment variables can therefore be visible inside the VM. Do not use this pattern as a credential boundary unless you remove sensitive variables or change the extension's environment handling.\n\nGondolin requires Node.js 23.6 or newer and QEMU installed through your operating-system package manager.\n\n### Install the extension\n\nFrom a Pi source checkout:\n\n```bash\nmkdir -p ~/.pi/agent/extensions\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\n### Start Pi\n\nRun Pi from the working folder you want mounted:\n\n```bash\ncd /path/to/working-folder\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nThe extension mounts the host working folder at `/workspace` in the VM and overrides `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls`. File changes under `/workspace` write through to the host.\n\nOther extension tools still run on the host unless they explicitly delegate their operations. Review the [Gondolin example](../examples/extensions/gondolin/) before adding tools that could bypass the VM boundary.\n","packages/coding-agent/docs/custom-provider.md":"# Custom Providers\n\nA provider extension connects Pi to a model service that needs custom authentication, model discovery, request handling, or streaming. If the service already speaks a supported API, configure it in `models.json` instead.\n\nProvider extensions run inside Pi and can inspect credentials, prompts, tool definitions, model responses, and usage. Treat them as trusted code and avoid logging secrets or provider payloads.\n\n## Choose the smallest integration\n\n| Requirement | Use |\n|---|---|\n| Add models behind a supported API | [`models.json`](models.md#configure-a-compatible-endpoint) |\n| Change an existing provider endpoint or headers | `models.json` or a small provider extension |\n| Discover models dynamically | A provider with `refreshModels` |\n| Add a `/login` flow | A provider with native or legacy OAuth configuration |\n| Implement an unsupported wire protocol | A provider with `stream` or `streamSimple` |\n\nA provider extension is an [extension](extensions.md), so it follows the same loading, trust, reload, and error behavior.\n\n## Register a provider\n\nCall `pi.registerProvider()` from the extension factory. Pi waits for asynchronous factories before startup continues, so providers registered there are available to startup model selection and `pi --list-models`.\n\nThere are two registration forms:\n\n- Register a complete `Provider` from `@earendil-works/pi-ai` for native authentication, filtering, discovery, refresh, and streaming behavior.\n- Register a provider name with `ProviderConfig` for the legacy configuration form used by existing extensions.\n\nPrefer a complete provider for new integrations that own more than static endpoint and model metadata. Pi composes `models.json` overrides above a registered native provider.\n\nRegistering only `baseUrl` or `headers` for an existing provider preserves its built-in models. Supplying `models` in the legacy form replaces that provider's models across chat, image, and classifier operations. An omitted `type` means `\"chat\"`; image and classifier models require explicit discriminants and implementations keyed by their `api` values through the `images` and `classifiers` fields.\n\n### Redirect an existing provider\n\nRegister only the fields that should change. This keeps the provider's built-in catalog, authentication, API implementation, and compatibility settings:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://gateway.example.com/anthropic\",\n    headers: {\n      \"X-Gateway-Key\": \"$GATEWAY_API_KEY\",\n    },\n  });\n}\n```\n\nUse this form for a proxy or gateway that preserves the upstream protocol. Do not copy the built-in model list into the extension because doing so turns a small endpoint override into a catalog replacement that can drift.\n\n### Register a compatible endpoint\n\nWhen a service implements a supported protocol but has its own model catalog, register the endpoint and only the models the service actually exposes:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"acme\", {\n    name: \"Acme AI\",\n    baseUrl: \"https://api.acme.example/v1\",\n    apiKey: \"$ACME_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        type: \"chat\",\n        id: \"acme-chat\",\n        name: \"Acme Chat\",\n        reasoning: false,\n        input: [\"text\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 8192,\n      },\n    ],\n  });\n}\n```\n\nPrefer `models.json` for this case when no executable extension behavior is needed. Use an extension when registration depends on runtime discovery, custom authentication, or code shared with other extension features.\n\n### Register mixed-operation models\n\nA mixed-operation provider can register non-chat models and their implementations together:\n\n```typescript\npi.registerProvider(\"media-tools\", {\n  apiKey: \"$MEDIA_TOOLS_API_KEY\",\n  models: [\n    {\n      type: \"image\",\n      id: \"image-v1\",\n      name: \"Image V1\",\n      api: \"media-images\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      output: [\"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n    },\n    {\n      type: \"classifier\",\n      id: \"classifier-v1\",\n      name: \"Classifier V1\",\n      api: \"media-classifier\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 64000,\n    },\n  ],\n  images: {\n    \"media-images\": { generateImages: async (model, context, options) => result },\n  },\n  classifiers: {\n    \"media-classifier\": { classify: async (model, context, options) => result },\n  },\n});\n```\n\nModel-level `baseUrl` values take precedence over the provider endpoint. If no `models` list is supplied, built-in models of every operation remain registered. Equal model IDs in different operations remain distinct, including their model-specific headers.\n\nCalls made after initial extension loading take effect immediately. Use `pi.unregisterProvider()` to remove the dynamic provider and restore built-in behavior that it replaced.\n\nSee the checked [GitLab Duo provider](../examples/extensions/custom-provider-gitlab-duo/) for a complete registration that delegates streaming to built-in API implementations.\n\n## Provide authentication\n\nStatic providers can resolve an API key from a literal, environment interpolation, or a command. These values use the same syntax as `models.json`:\n\n- `$NAME` and `${NAME}` read environment variables.\n- A leading `!command` uses command output.\n- `$$` emits a literal `$`.\n- `$!` emits a literal leading `!`.\n\nUse native provider authentication when the integration needs stored credentials, custom resolution, provider-scoped environment, or multiple login methods.\n\nAn OAuth provider supplies a display name, login flow, token refresh, and access-token resolution. After registration it appears in `/login`, and Pi stores returned credentials in `~/.pi/agent/auth.json`.\n\nOAuth callbacks are UI-neutral. They can open an authorization URL, show a device code, report progress, request input, or ask the user to choose a login method. Honor cancellation and the supplied abort signal during network requests.\n\nPress Enter (`tui.select.confirm`) to open the device-login URL and copy the code, or log in manually. Providers can disable either action with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false` in `onDeviceCode` or native `AuthEvent` notifications.\n\nNever write access tokens, refresh tokens, authorization headers, or complete provider responses to ordinary logs.\n\n## Supply and refresh models\n\nEvery model needs an ID, display name, input capabilities, and cost metadata. Chat and classifier models also need a context window; chat models need an output limit and reasoning support; image models declare their output modalities. Choose the API implementation at the provider level unless one model requires an override.\n\nSet `promptCache.short` or `promptCache.long` to the provider's best-effort cache lifetime in seconds when Pi should keep an idle prompt cache warm. Leave them unset to disable cache warming for that retention tier.\n\nCompatibility flags describe verified differences in an otherwise supported API. Do not enable them based only on an endpoint claiming compatibility.\n\nConfirm the request fields and response behavior against the actual server.\n\nUse `refreshModels` when the available catalog comes from a live service. Pass `context.signal` to blocking I/O so callers can cancel refreshes.\n\nThe two registration forms have different refresh contracts:\n\n- A complete `Provider` returns nothing. It calls `context.publish({ update })` to install provider-owned model state, after which its synchronous `getModels()` exposes the latest list.\n- Legacy `ProviderConfig.refreshModels` returns mixed-operation model definitions. Pi replaces that registration’s live models with the returned list and applies any requested persistence.\n\nPublish persisted catalog data only when it should survive across runs. A live service such as llama.cpp can update its in-memory list without persisting it; a remote catalog can retain a snapshot for offline startup.\n\n## Reuse a supported streaming API\n\nUse one of Pi AI’s API implementations whenever the provider protocol matches it.\n\nSupported implementations cover Anthropic Messages, OpenAI Chat Completions and Responses, Google Generative AI and Vertex, Azure OpenAI Responses, Mistral Conversations, and Bedrock Converse.\n\nThe provider can still customize authentication, base URLs, headers, model filtering, and discovery while delegating request conversion and streaming to an existing API implementation.\n\nThis is safer than copying a stream implementation because it preserves Pi’s message conversion, tool handling, usage accounting, cancellation, and compatibility behavior.\n\n## Implement custom streaming\n\nImplement `streamSimple` only when no existing API implementation can represent the service. Study the implementations under [`packages/ai/src/api`](https://github.com/earendil-works/pi/tree/main/packages/ai/src/api) first.\n\nThe stream receives a normalized `TranscriptContext`. System prompts and tool declarations live in transcript system messages, so read them with `getCurrentSystemPrompt(context.messages)` and `getCurrentTools(context.messages)` rather than expecting `context.systemPrompt` or `context.tools`. A model that supports mid-conversation system messages can receive them in place; otherwise call `collapseSystemMessages(context)` to fold later system messages into the leading one.\n\nA custom stream must:\n\n1. Create an assistant message with provider, model, timestamp, pending stop reason, content, and zeroed usage.\n2. After request setup succeeds, emit one `start` event before content events.\n3. Update the message while emitting balanced text, thinking, and tool-call events.\n4. Finalize usage, cost, content, and stop reason.\n5. Emit exactly one terminal `done` or `error` event and close the stream.\n6. Convert cancellation into an aborted result.\n\nRequest setup can fail before `start`; in that case the stream can terminate directly with `error`. Missing request authentication may also throw synchronously before a stream is returned.\n\nContent indexes refer to blocks in the assistant message. Update each block before emitting the event whose `partial` field exposes that state. Tool-call arguments must contain valid parsed input by `toolcall_end`.\n\nThe stream must also honor request instrumentation supplied through `SimpleStreamOptions`:\n\n- Call `options.onPayload` before sending the provider request and use any replacement payload it returns.\n- Call `options.onResponse` after receiving the response but before consuming its body.\n- Await `options.onProviderStreamEvent?.(providerEvent, model)` for each parsed provider event before normalizing it.\n- Pass through the abort signal and provider-scoped environment.\n\nThese hooks power extension request inspection, response-header events, and provider-stream observation. Omitting them makes the provider behave differently from Pi’s built-in providers.\n\n## Report failures and usage\n\nSet a concrete terminal stop reason. Error and aborted messages need an `errorMessage`; successful messages need accurate input, output, cache, total-token, and cost values.\n\nPi can compact and retry after recognized context-overflow errors. If the service uses an unknown message, normalize only that provider’s overflow response to `context_length_exceeded` in a guarded `message_end` handler.\n\nDo not rewrite rate limits or transient provider failures as context overflow. Those failures use Pi’s normal retry behavior instead.\n\n## Test the integration\n\nTest at least:\n\n- ordinary and empty text responses\n- tool calls and tool results\n- image input and image tool results when supported\n- usage and cost accounting\n- abort behavior\n- context overflow\n- malformed or partial streams\n- Unicode boundaries\n- cross-provider session handoff\n- authentication refresh and cancellation\n\nThe provider tests under [`packages/ai/test`](https://github.com/earendil-works/pi/tree/main/packages/ai/test) define the behavior expected from built-in providers. Adapt the relevant suites rather than relying only on manual prompts.\n\nRun the extension directly while developing, then move it to a discovered extension location or distribute it through a [Pi package](packages.md). Use `/reload` after changing a discovered provider extension in an active session.\n","packages/coding-agent/docs/docs.json":"{\n  \"navigation\": [\n    {\n      \"title\": \"Get Started\",\n      \"items\": [\n        {\n          \"title\": \"Overview\",\n          \"path\": \"index.md\"\n        },\n        {\n          \"title\": \"Quickstart\",\n          \"path\": \"quickstart.md\"\n        },\n        {\n          \"title\": \"How Pi Works\",\n          \"path\": \"how-pi-works.md\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Guides\",\n      \"items\": [\n        {\n          \"title\": \"Run Pi\",\n          \"items\": [\n            {\n              \"title\": \"Use Pi in the Terminal\",\n              \"path\": \"usage.md\"\n            },\n            {\n              \"title\": \"Choose a Model\",\n              \"path\": \"models.md\"\n            },\n            {\n              \"title\": \"Manage Sessions and Context\",\n              \"path\": \"sessions.md\"\n            },\n            {\n              \"title\": \"Run Pi Safely\",\n              \"path\": \"security.md\"\n            },\n            {\n              \"title\": \"Isolate Pi\",\n              \"path\": \"containerization.md\"\n            },\n            {\n              \"title\": \"Run Local Models\",\n              \"path\": \"llama-cpp.md\"\n            },\n            {\n              \"title\": \"Run Local Models on NVIDIA RTX\",\n              \"path\": \"nvidia-rtx-llama-cpp.md\"\n            },\n            {\n              \"title\": \"Configure Your Terminal\",\n              \"path\": \"terminal-setup.md\"\n            },\n            {\n              \"title\": \"Configure Shell Commands\",\n              \"path\": \"shell-aliases.md\"\n            },\n            {\n              \"title\": \"Run Pi in tmux\",\n              \"path\": \"tmux.md\"\n            },\n            {\n              \"title\": \"Run Pi on Windows\",\n              \"path\": \"windows.md\"\n            },\n            {\n              \"title\": \"Run Pi on Android\",\n              \"path\": \"termux.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Customize Pi\",\n          \"items\": [\n            {\n              \"title\": \"Configure Pi\",\n              \"path\": \"configuration.md\"\n            },\n            {\n              \"title\": \"Create Prompt Templates\",\n              \"path\": \"prompt-templates.md\"\n            },\n            {\n              \"title\": \"Add Skills\",\n              \"path\": \"skills.md\"\n            },\n            {\n              \"title\": \"Create Themes\",\n              \"path\": \"themes.md\"\n            },\n            {\n              \"title\": \"Use Pi Packages\",\n              \"path\": \"packages.md\"\n            },\n            {\n              \"title\": \"Connect MCP Servers\",\n              \"path\": \"mcp.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Build on Pi\",\n          \"items\": [\n            {\n              \"title\": \"Build Extensions\",\n              \"path\": \"extensions.md\"\n            },\n            {\n              \"title\": \"Add Custom Providers\",\n              \"path\": \"custom-provider.md\"\n            },\n            {\n              \"title\": \"Route with Virtual Models\",\n              \"path\": \"virtual-models.md\"\n            },\n            {\n              \"title\": \"Build Terminal UI Components\",\n              \"path\": \"tui.md\"\n            },\n            {\n              \"title\": \"Integrate with the CLI\",\n              \"path\": \"cli-integration.md\"\n            },\n            {\n              \"title\": \"Use the SDK\",\n              \"path\": \"sdk.md\"\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"title\": \"Reference\",\n      \"items\": [\n        {\n          \"title\": \"CLI\",\n          \"path\": \"cli.md\"\n        },\n        {\n          \"title\": \"Codemode\",\n          \"path\": \"codemode.md\"\n        },\n        {\n          \"title\": \"Slash Commands\",\n          \"path\": \"slash-commands.md\"\n        },\n        {\n          \"title\": \"Settings\",\n          \"path\": \"settings.md\"\n        },\n        {\n          \"title\": \"Models Configuration\",\n          \"path\": \"models-configuration.md\"\n        },\n        {\n          \"title\": \"Environment Variables\",\n          \"path\": \"environment-variables.md\"\n        },\n        {\n          \"title\": \"Keybindings\",\n          \"path\": \"keybindings.md\"\n        },\n        {\n          \"title\": \"Providers\",\n          \"path\": \"providers.md\"\n        },\n        {\n          \"title\": \"Session File Format\",\n          \"path\": \"session-format.md\"\n        },\n        {\n          \"title\": \"Compaction and Branch Summaries\",\n          \"path\": \"compaction.md\"\n        },\n        {\n          \"title\": \"JSON Event Stream\",\n          \"path\": \"json.md\"\n        },\n        {\n          \"title\": \"RPC Protocol\",\n          \"path\": \"rpc.md\"\n        },\n        {\n          \"title\": \"RPC Commands\",\n          \"path\": \"rpc-commands.md\"\n        },\n        {\n          \"title\": \"RPC Extension UI\",\n          \"path\": \"rpc-extension-ui.md\"\n        },\n        {\n          \"title\": \"Message Types\",\n          \"path\": \"message-types.md\"\n        }\n      ]\n    }\n  ],\n  \"redirects\": [\n    {\n      \"from\": \"development.md\",\n      \"to\": \"index.md\"\n    },\n    {\n      \"from\": \"session.md\",\n      \"to\": \"session-format.md\"\n    },\n    {\n      \"from\": \"tree.md\",\n      \"to\": \"sessions.md\"\n    },\n    {\n      \"from\": \"process-integration.md\",\n      \"to\": \"cli-integration.md\"\n    }\n  ]\n}\n","packages/coding-agent/docs/environment-variables.md":"# Environment Variables\n\nPi uses environment variables in three ways:\n\n- Variables such as `PI_OFFLINE` configure the Pi process.\n- Pi sets process markers so child processes can identify Pi as the launching agent.\n- Commands run by the LLM-callable shell tools receive `PI_*` variables describing the current session.\n\nProvider API-key variables are documented separately in [Providers](providers.md#use-an-api-key-from-the-environment).\n\n## Process Marker\n\nThe CLI and RPC entry points set two process markers:\n\n- `AI_AGENT=pi` is a generic marker that lets tooling identify Pi as the agent that launched the process.\n- `PI_CODING_AGENT=true` is Pi-specific and lets child processes detect that they run inside Pi.\n\nChild processes inherit both markers. They are not session-specific and are not set automatically when Pi is embedded through the SDK.\n\n## Shell Tool Session Environment\n\nCommands run by the `bash` and `powershell` tools receive the current Pi session state:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_SESSION_ID` | Current session ID |\n| `PI_SESSION_FILE` | Absolute path to the current session JSONL file; unset for ephemeral sessions |\n| `PI_PROVIDER` | Currently selected model provider |\n| `PI_MODEL` | Currently selected model ID |\n| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |\n\nThe values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next shell command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.\n\nWhen asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt:\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\nThe session file can be inspected directly when the session is persistent:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nThese variables are injected into the LLM-callable `bash` and `powershell` tools. They are not injected into user-entered `!` or `!!` commands.\n\n### Custom Shell Tools\n\nTools created with `createBashTool()` or `createPowerShellTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nDisable session metadata independently of the spawn hook:\n\n```typescript\nconst powershellTool = createPowerShellTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nWhen disabled, Pi removes inherited values for these variables so nested Pi processes do not expose stale parent-session metadata.\n\n## Pi Process Configuration\n\nThese variables are read by Pi itself:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Override the config directory; default is `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Override session storage; overridden by `--session-dir` |\n| `PI_PACKAGE_DIR` | Override the package directory, useful for Nix/Guix store paths |\n| `PI_OFFLINE` | Disable automatic network activity, including model catalog refreshes |\n| `PI_SKIP_VERSION_CHECK` | Disable the `pi.dev` latest-version request |\n| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no` |\n| `PI_CODEMODE_MAX_WORKERS` | Maximum codemode workers alive at once across all sessions in the process; default `2`. Each worker's script may use up to 256 MB. Raise it when one process runs several sessions, such as in-process subagents. Further scripts wait for a free worker. See [Codemode](codemode.md) |\n| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |\n| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |\n| `PI_RADIUS_GATEWAY` | Override the Radius gateway origin used by `/bug` uploads |\n| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |\n| `PI_CLIPBOARD_OSC52` | Set to `1` to also send copied text to the terminal with OSC 52 after the local clipboard write, as Pi does over SSH. Use it when a remote client reaches Pi without SSH variables, such as a multiplexer attached from another machine or a remote IDE daemon |\n| `PI_HYPERLINKS` | Override OSC 8 hyperlink detection with `1`, `0`, or `auto` |\n| `PI_PROGRAM_STATUS` | Override OSC 7501 program status detection: `1` always reports, `0` never reports; otherwise Pi reports only after the terminal confirms support. See [Terminal setup](terminal-setup.md#program-status) |\n| `PI_IMAGE_PROTOCOL` | Override inline image detection with `kitty`, `iterm2`, `none`, or `auto` |\n| `PI_TRUE_COLOR` | Override truecolor detection with `1`, `0`, or `auto` |\n| `PI_TUI_ESC_TIMEOUT` | How long to wait after a lone ESC before treating it as Escape, in milliseconds; defaults to `100` over SSH and `10` otherwise. Increase if Alt-key input is misread as Escape |\n| `VISUAL`, `EDITOR` | External editor fallback when `externalEditor` is unset |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |\n\nProvider credentials such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and provider-specific configuration are listed in [Providers](providers.md#use-an-api-key-from-the-environment).\n","packages/coding-agent/docs/extensions.md":"# Extensions\n\nExtensions are TypeScript modules that add executable behavior to Pi. Use one when a workflow needs tools, commands, event handlers, model providers, session state, or terminal UI rather than instructions alone.\n\nAn extension runs inside the Pi process with the same operating-system permissions. It can inspect prompts, tool calls, files, credentials, and session history, so load extensions only from sources you trust.\n\nTypical extensions add an agent tool, protect paths, confirm dangerous commands, react to session events, modify context, expose a command, or display persistent status.\n\n<a id=\"quick-start\"></a>\n<a id=\"writing-an-extension\"></a>\n<a id=\"create-an-extension\"></a>\n\n## Create and load an extension\n\nAn extension exports a default factory that receives `ExtensionAPI`. The factory registers capabilities for the current extension runtime.\n\nCreate `~/.pi/agent/extensions/hello.ts`:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"hello\", {\n    description: \"Show a greeting\",\n    handler: async (name, ctx) => {\n      ctx.ui.notify(`Hello, ${name || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\nStart Pi and run `/hello`. During development, load a file directly:\n\n```bash\npi --extension ./hello.ts\n```\n\nPi uses `jiti`, so local TypeScript extensions do not need a separate compilation step. Use [Pi packages](packages.md) for distributed extensions and dependencies.\n\n<a id=\"extension-locations\"></a>\n<a id=\"available-imports\"></a>\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the extension in your user or project extensions directory. Pi loads direct TypeScript or JavaScript files and subdirectories containing an `index.ts` or `index.js` entry point.\n\nUse a single file for a small extension and a directory for a multi-file implementation. Put npm dependencies in a nearby `package.json`. See [Configuration](configuration.md) for conventional locations and [Settings](settings.md#resources) for additional paths.\n\nReload replaces the extension runtime, so code after `await ctx.reload()` must not reuse state from the old runtime. Only personal and explicit command-line extensions can participate in the `project_trust` event that runs before project extensions load.\n\n<a id=\"understand-the-lifecycle\"></a>\n\n## Understand the runtime lifecycle\n\nThe factory can be synchronous or asynchronous. Pi waits for an asynchronous factory before startup continues, allowing it to fetch configuration or register providers needed during startup.\n\nDo not start processes, sockets, watchers, or timers in the factory because some invocations load extensions without starting a session.\nStart long-lived resources from `session_start` or from the command or tool that needs them.\nClose session-scoped resources from an idempotent `session_shutdown` handler.\n\nA normal prompt starts a run. The run repeats turns, then checks whether more work starts another low-level agent run:\n\n```mermaid\nflowchart TD\n    Input[\"input\"] --> Before[\"before_agent_start\"]\n    Before --> AgentStart[\"agent_start\"]\n    AgentStart --> Turns[[\"turns\"]]\n    Turns --> AgentEnd[\"agent_end\"]\n    AgentEnd --> MoreRuns{\"More work?\"}\n    MoreRuns -->|yes| AgentStart\n    MoreRuns -->|no| BeforeSettle[\"agent_before_settle\"]\n    BeforeSettle --> Continue{\"Continuation requested?\"}\n    Continue -->|yes| AgentStart\n    Continue -->|no| Settled[\"agent_settled\"]\n```\n\nMore work means a retry, recovery, compaction retry, or queued work.\n\nEach turn sends one model request and executes the tool calls in the assistant response:\n\n```mermaid\nflowchart TD\n    TurnStart[\"turn_start\"] --> Request[[\"model request\"]]\n    Request --> ToolDecision{\"Tool calls?\"}\n    ToolDecision -->|yes| Tools[[\"tool execution\"]]\n    Tools --> TurnEnd[\"turn_end\"]\n    ToolDecision -->|no| TurnEnd\n    TurnEnd --> MoreTurns{\"Another turn?\"}\n    MoreTurns -->|yes| TurnStart\n    MoreTurns -->|no| AgentEnd([\"agent_end\"])\n```\n\nThe model request runs these hooks in order:\n\n1. `context`\n2. `context_with_system`\n3. `before_provider_headers`\n4. `before_provider_request`\n5. `after_provider_response`\n6. `provider_stream_event` and assistant message events\n\nTool execution runs these hooks in order for each tool call:\n\n1. `tool_execution_start`\n2. `tool_call`\n3. `tool_execution_update` while the tool executes\n4. `tool_result`\n5. `tool_execution_end`\n\nThe lifecycle involves these event groups:\n\n| Area | Events |\n|---|---|\n| Input preparation | `input`, `before_agent_start` |\n| Agent run | `agent_start`, `agent_end` |\n| Model turn | `turn_start`, `context`, `context_with_system`, `turn_end` |\n| Provider call | `before_provider_headers`, `before_provider_request`, `after_provider_response`, `provider_stream_event` |\n| Message lifecycle | `message_start`, `message_update`, `message_end` |\n| Tool execution | `tool_execution_start`, `tool_call`, `tool_execution_update`, `tool_result`, `tool_execution_end` |\n| Settlement | `agent_before_settle`, `agent_settled` |\n\nPi counts as busy while a run starts, including the compaction check and `before_agent_start`, so messages sent meanwhile go into the starting run. Message lifecycle events also cover user and tool-result messages; the model request list places only the assistant stream where it occurs in each turn.\n\nTool hooks run for each tool call, and one assistant response can start another turn by returning tool calls. Provider hooks run only when that transport exposes the corresponding stage. See the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event payloads and result types.\n\nSession startup and replacement follow a separate lifecycle:\n\n```mermaid\nflowchart TD\n    Startup[\"Process startup\"] --> Trust[\"project_trust when needed\"]\n    Trust --> SessionStart[\"session_start\"]\n    Switch[\"/new or /resume\"] --> BeforeSwitch[\"session_before_switch\"]\n    Fork[\"/fork or /clone\"] --> BeforeFork[\"session_before_fork\"]\n    BeforeSwitch --> Shutdown[\"session_shutdown\"]\n    BeforeFork --> Shutdown\n    Reload[\"reload\"] --> Shutdown\n    Shutdown --> Replacing{\"Replacing the runtime?\"}\n    Replacing -->|yes| Replacement[\"load replacement extension runtime\"]\n    Replacement --> SessionStart\n    Replacing -->|no| Stop[\"process exits\"]\n    SessionStart --> Discover[\"resources_discover\"]\n```\n\n`project_trust` runs before trusted project resources load and can run again when session replacement enters a new working directory. Retries, recovery, compaction, and queued messages can start another agent run after `agent_end`, so `agent_end` is not a final-idle signal. Reload and session replacement invalidate the old runtime. Code after `await ctx.reload()` still belongs to the old runtime and must not reuse its session-bound state.\n\n### Choose an event by intent\n\n| Goal | Event |\n|---|---|\n| Inspect, transform, or consume raw user input | `input` |\n| Change prompt sections, active tools, or add context before a run | `before_agent_start` |\n| Observe one low-level agent run | `agent_start`, `agent_end` |\n| Act after one model response and its tool calls | `turn_end` |\n| Filter conversation messages for one provider request | `context` |\n| Transform the complete transcript, including system messages | `context_with_system` |\n| Change request headers or the serialized provider payload | `before_provider_headers`, `before_provider_request` |\n| Observe provider response metadata or parsed stream events | `after_provider_response`, `provider_stream_event` |\n| Authorize or modify a tool call | `tool_call` |\n| Redact, annotate, or replace a tool result | `tool_result` |\n| Append final entries or request one last continuation | `agent_before_settle` |\n| Run notification work after automatic processing has stopped | `agent_settled` |\n| Initialize or release session-scoped resources | `session_start`, `session_shutdown` |\n\n<a id=\"agent_start--agent_end--agent_before_settle--agent_settled\"></a>\n\nUse `agent_before_settle` when an extension must append entries or request one final continuation. Use `agent_settled` for notification-only work that should run after Pi has no automatic continuation remaining.\n\n<a id=\"extensionapi-methods\"></a>\n\n## Choose an integration point\n\n| Capability | Main API |\n|---|---|\n| Observe or modify lifecycle behavior | `pi.on()` |\n| Add a model-callable operation | `pi.registerTool()` |\n| Add a `/` command | `pi.registerCommand()` |\n| Add a shortcut or CLI flag | `pi.registerShortcut()` or `pi.registerFlag()` |\n| Send user or custom messages | `pi.sendUserMessage()` or `pi.sendMessage()` |\n| Persist non-context session data | `pi.appendEntry()` |\n| Change active tools, model, or thinking level | Session control methods on `pi` |\n| Add a model provider | `pi.registerProvider()` |\n| Add an MCP server | `pi.registerMcpServer()` |\n| Route each request to a model | [`pi.registerVirtualModel()`](virtual-models.md) |\n| Add terminal rendering | Renderer registration and `ctx.ui` |\n| Communicate with another extension | `pi.events` |\n\nUse the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event, context, tool, and result types.\n\n## Follow the extension contracts\n\n<a id=\"events\"></a>\n<a id=\"work-with-events\"></a>\n\n### Events and concurrency\n\nHandlers run in extension load and registration order. `pi.on()` returns a function that unsubscribes that registration; changes do not affect a dispatch already in progress.\nSome events notify; others transform data, replace results, or cancel an operation.\nUse each event’s declared result type rather than assuming every return value has an effect.\n\nEvents cover resource discovery, sessions, agent and message lifecycle, providers, tools, and raw input.\n\n#### Understand handler composition\n\n| Pattern | Behavior |\n|---|---|\n| Notification | Return values are ignored. Use the event for observation only. |\n| In-place mutation | Later handlers see changes to values such as `before_provider_headers.headers`, `before_agent_start.systemPromptOptions`, and `tool_call.input`. |\n| Chained transformation | Later handlers receive the value returned by earlier `input`, `context`, `context_with_system`, `message_end`, `tool_result`, and `before_provider_request` handlers. |\n| Gate | A handled input, blocked tool call, cancelled session operation, or handled `user_bash` command stops that operation's normal fallback path. |\n| Actionable boundary | Each `turn_end` or `agent_before_settle` handler sees the entries and continuation decision proposed so far. |\n| Last override | The last `cache_warming_decision` handler that returns an action wins. |\n\nNotification handlers should not perform slow work on a streaming event. Transform handlers should preserve fields they do not intentionally own, and gate handlers should fail closed when their safety decision cannot be made.\n\n`before_agent_start` exposes both the current prompt and its structured `systemPromptOptions`. `event.messageType` says what started the run: `\"user\"` for a prompt or `pi.sendUserMessage()`, with `prompt` and `images`; `\"custom\"` for `pi.sendMessage(..., { triggerTurn: true })`, with the starting `message` and no `prompt`. A returned `message` is added to either kind of run; check `messageType` to skip runs started by custom messages. Prefer changing prompt sections, selected tools, or guidelines so Pi can append a transcript delta. Returning `systemPrompt`, or setting `forceSystemPrompt`, replaces the whole prompt for that run while the transcript continues recording the structured sections. Providers receive the forced text as their leading system prompt.\n\n`message_end` can replace a finalized message while preserving its role. `tool_call` can mutate input or block execution. `tool_result` handlers compose, with each handler seeing prior changes.\n\n<a id=\"provider_stream_event\"></a>\n\n`provider_stream_event` fires for each parsed provider stream event before Pi normalizes it. The event identifies the provider, API, and model; `event.data` is the earliest structured value available to Pi, not necessarily the original HTTP bytes or SSE frame. Treat it as read-only because mutation can affect normalization. The event is notification-only and is not persisted.\n\nHandlers are awaited in stream order, so slow handlers delay stream consumption. Handler errors are reported without changing the provider response. See [`debug-provider.ts`](../examples/extensions/debug-provider.ts) for an opt-in viewer that groups raw events by assistant message.\n\n<a id=\"context_with_system\"></a>\n\n`context` transforms conversation messages without prompt and tool system messages; Pi restores that state afterward. Use `context_with_system` only when a request-local transformation must own the complete transcript, and keep a system message at index zero.\n\n`turn_end` and `agent_before_settle` are actionable boundaries. Their handlers can chain proposed `custom`, `custom_message`, `context_edit`, or `compaction` entries and return `continue: true` for one next model request. Guard continuation conditions because an unconditional continuation can loop. Use the exported event declarations for the complete validation and ordering contract.\n\n<a id=\"cache_warming_decision\"></a>\n\n`cache_warming_decision` can override an idle prompt-cache refresh with `{ action: \"warm\" }` or `{ action: \"stop\" }`. The last handler that returns an action wins.\n\nTool calls from one assistant message can run in parallel.\nDo not assume a sibling call or result exists when another tool event runs.\nUse `ctx.signal` for nested work owned by an active turn; commands and idle session events often have no operation signal.\n\nA `user_bash` handler that returns `undefined` passes the command to the next handler and then to local execution if no handler handles it. Returning `operations` or `result` stops propagation. A handler failure blocks the command rather than falling through to local execution.\n\n<a id=\"custom-tools\"></a>\n<a id=\"register-tools\"></a>\n\n### Tools\n\nA custom tool defines a name, model-facing description, TypeBox parameter schema, and `execute()` function.\nIts result requires model-facing `content` and a `details` field for rendering or state reconstruction.\nUse `details: undefined` when there are no structured details. If the tool makes nested model calls, include their `usage` in the result so session totals remain accurate.\n\nThrow from `execute()` to produce a failed tool result.\nReturning an object does not mark it as an error.\nReturn `terminate: true` only when the agent should skip its automatic follow-up after every completed tool in that batch agrees to terminate.\n\nUse sequential execution when tools share mutable in-memory state.\nFile-mutating tools should wrap the complete read-modify-write operation with `withFileMutationQueue()`.\nTruncate large model-facing results and tell the model where to read the complete output.\n\nDeclare `outputSchema` and return a matching `structuredContent` when the result is data. The model still receives `content`; programmatic callers such as codemode scripts receive `structuredContent` instead of the text. Tools without `outputSchema` are passed to scripts as their text content. To report a failure that still carries data, return the result with `isError: true` instead of throwing: the model sees an error, and scripts still receive `structuredContent`.\n\nA tool can run other tools with `ctx.executeTool(name, args, { signal, onUpdate })`. Nested calls go through argument validation and the `tool_call` and `tool_result` handlers like model-issued calls, and emit `tool_execution_start`, `tool_execution_update`, and `tool_execution_end`; all of these events carry `parentToolCallId`, and their `toolCallId` is assigned by pi as `<parent id>/<n>`. These ids do not appear as tool calls or tool results in the transcript. Nested calls do not add transcript entries: their results only reach the calling tool, which reports them itself, for example through `onUpdate` and `details`. The session keeps a bounded record of them (name, arguments, status, duration, error; never results) as `nestedCalls` on the calling tool's result message. It is used for compaction file lists and shown in HTML exports. Arguments over 8 KiB per call or 32 KiB per tool result are omitted, at most 256 calls are kept, and `complete: false` marks a record that lost anything. The `usage` of nested results, at every depth, is added to the calling tool's result `usage`, so a tool reports only its own usage, not that of the tools it called. `ctx.tools` lists the tools `ctx.executeTool()` can call. `tool_result` handlers that redact `content` should also replace `structuredContent`; replacing only `content` drops it.\n\nSee [`hello.ts`](../examples/extensions/hello.ts), [`todo.ts`](../examples/extensions/todo.ts), [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts), and [`truncated-tool.ts`](../examples/extensions/truncated-tool.ts).\n\n### Tool exposure\n\n`exposure` controls how the model reaches a tool. \"Callable\" means callable from other tools through `ctx.executeTool()` (`ctx.tools`), as the `codemode` tool's scripts do:\n\n- `direct` (default): declared to the model while active, and callable while active.\n- `model-only`: declared to the model while active, never callable. Use it for tools that orchestrate other tools or ask the user.\n- `codemode`: callable whenever registered, and listed by the `codemode` tool. Not declared to the model unless activated explicitly.\n- `deferred`: like `codemode`, but codemode tools do not list it; `tool_search` can find and activate it.\n- `hidden`: registered but unreachable. Re-register a tool with `exposure: \"hidden\"` to withdraw it, since tools cannot be unregistered.\n\n`namespace: { name, aliases, description, instructions }` groups related tools, as MCP servers do. Codemode tools list a namespace under one heading with its `description`. `instructions` holds longer usage guidance; it is not listed, and codemode scripts read it with `describeNamespace(name)`. `aliases` are other names scripts may look the namespace up by, for example the server name `docs` for `mcp__docs`; a name and the same name as a script identifier (`my-docs`, `my_docs`) match alike.\n\nRegistering a `direct` or `model-only` tool activates it; the other exposures are not activated on registration. The active set (`pi.getActiveTools()`, `pi.setActiveTools()`) is the set of tools declared to the model. `pi.getAllTools()` reports each tool's `exposure`, `namespace`, `annotations`, and `reachesTools`.\n\n`annotations` are hints about what a tool does, with the meaning of MCP tool annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. MCP tools carry the hints their server declares. Missing hints take the MCP defaults: a tool is not read-only, and may be destructive and reach an open world. The hints are not verified, but a permission extension can use them to decide which calls to confirm. This confirms the calls Codex asks approval for:\n\n```typescript\npi.on(\"tool_call\", async (event, ctx) => {\n  const hints = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;\n  const needsApproval =\n    hints?.destructiveHint === true ||\n    (!hints?.readOnlyHint && ((hints?.destructiveHint ?? true) || (hints?.openWorldHint ?? true)));\n  if (needsApproval && !(await ctx.ui.confirm(\"Allow tool call?\", event.toolName))) {\n    return { block: true, reason: `${event.toolName} was not approved` };\n  }\n});\n```\n\nA tool that orchestrates other tools can adjust what the model sees while it is active with `prepareLoadout(loadout)`. It runs whenever the active tools change and receives the declared tools, the callable tools, and every registered tool with its exposure, namespace, and prompt guidelines. It returns replacement `descriptions` for declared tools (including its own) and `hiddenDeclarations`: active tools whose declarations requests leave out while they stay active and callable. The default system prompt leaves hidden tools out of its tool list and rules, and names no tool in the skills hint when the file reader is hidden, so the orchestrating tool should present their guidelines itself. `codemode` uses only this hook, `exposure`, `reachesTools`, and `ctx.executeTool()`, so another tool can implement the same behavior under a different name.\n\nA tool that reaches other tools declares which ones a call may reach with `reachesTools(input)`: `\"all\"`, or the words of the input that may name them, where a word refers to every tool or namespace whose name it contains. `codemode` returns `\"all\"` for scripts that search or list tools and the words of the script otherwise; `tool_search` always returns `\"all\"`. Extensions that register tools late use it from their `tool_call` handler to finish registering the tools a call needs before it runs, as the MCP extension does for servers that are still connecting. The MCP extension also activates `codemode` or `tool_search` for MCP tools only when the tool of that name declares `reachesTools`.\n\n### Activate tools dynamically\n\nRegister every tool first, keep optional tools inactive, and use `pi.setActiveTools()` from a loader tool to select the desired active tools. Names must already be registered; unknown names are ignored.\n\nPi records the initial prompt and tool set in the transcript's first system message, then appends tool and prompt changes before the next model request. Providers that cannot represent the transition receive a complete transcript checkpoint, which can invalidate the cached prefix.\n\n### Tool rendering\n\nA tool's `renderCall` and `renderResult` draw its calls in the interactive transcript and in HTML exports. `pi.registerToolRenderer((toolName, next) => renderers)` chooses renderers for calls to any tool, including tools that are not registered yet, such as MCP tools in a resumed session before their server connected. `next()` returns what the remaining resolvers (in extension load order), then the registered tool, would use, so `next() ?? mine` only fills in.\n\n### MCP servers\n\n`pi.registerMcpServer(name, config)` adds an MCP server for the current session. `config` has the shape of an `mcpServers` entry in [`mcp.json`](mcp.md): `command`, `args`, `env`, and `cwd` for stdio servers, `url`, `headers`, and `oauth` for HTTP servers, plus `exposure`, `toolExposure`, `description`, `enabled`, and `timeout`.\n\n```typescript\npi.registerMcpServer(\"jira\", { url: \"https://mcp.example.com/jira\", exposure: \"codemode\" });\npi.unregisterMcpServer(\"jira\");\n```\n\nServers registered while the extension loads connect when the session starts, together with the `mcp.json` servers; servers registered later connect right away, and `pi.unregisterMcpServer()` closes the connection and makes the server's tools unreachable. Registrations are not saved: register again on every load, for example based on the extension's own settings. A server in `mcp.json` with the same name takes precedence, and `/mcp` shows the override. Registering the same name again replaces the extension's earlier registration; names registered by another extension, invalid names, and invalid configs throw.\n\nThe built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#other-mcp-extensions)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `pi.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.\n\n<a id=\"extensioncontext\"></a>\n<a id=\"extensioncommandcontext\"></a>\n<a id=\"use-extension-context\"></a>\n\n### Context and session changes\n\n`ExtensionContext` provides the working directory, mode, UI, session manager, model runtime, abort signal, context usage, and controls for compaction and shutdown.\nUse `ctx.modelRegistry.streamSimple()` for provider-neutral nested model calls.\n\nCommand handlers receive `ExtensionCommandContext`, which adds operations for waiting until idle, reloading, tree navigation, and session replacement.\nThese operations are command-only because calling them from lifecycle handlers can deadlock the runtime.\n\nSession replacement invalidates the old context. Capture only plain data before switching, then use the fresh context supplied to `withSession` for session-bound work.\n\n<a id=\"state-management\"></a>\n<a id=\"persist-state\"></a>\n\n### State\n\nChoose storage based on how state participates in the conversation:\n\n| State | Storage |\n|---|---|\n| Tool state that follows the active branch | Tool-result `details` |\n| Durable data excluded from model context | `pi.appendEntry()` |\n| Custom content stored and sent to the model | `pi.sendMessage()` |\n| Data outside one session | External storage |\n\nReconstruct branch-sensitive state from `ctx.sessionManager.getBranch()` during `session_start`.\nDo not rebuild it from every file entry because abandoned branches represent alternative histories.\nRegister an entry or message renderer when custom stored content should appear in the transcript.\n\n<a id=\"custom-ui\"></a>\n<a id=\"mode-behavior\"></a>\n<a id=\"interact-with-the-user\"></a>\n<a id=\"account-for-each-mode\"></a>\n\n### UI and modes\n\n`ctx.ui` provides dialogs, notifications, status text, widgets, titles, editor access, and custom components.\nUse `ctx.ui.custom()` only when the interaction needs its own rendering and input.\nSee [Terminal UI](tui.md) for component, focus, overlay, theme, and performance guidance.\n\nExtensions load in interactive, RPC, JSON, and print modes.\nInteractive mode provides the complete terminal UI.\nRPC can forward supported dialogs and notifications through the [RPC Extension UI protocol](rpc-extension-ui.md), but not custom terminal components; JSON and print modes have no UI.\nGuard terminal-only behavior with `ctx.mode === \"tui\"` and use `ctx.hasUI` for interactions supported by interactive and RPC clients.\n\nKeep tool and event behavior independent from rendering so non-interactive modes remain functional.\n\n<a id=\"error-handling\"></a>\n<a id=\"handle-errors-and-shutdown\"></a>\n\n### Errors and cleanup\n\nPi reports handler errors and continues where possible. A `tool_call` handler failure blocks the tool as a fail-safe; a tool execution failure becomes an error result for the model.\n\nRelease resources in `session_shutdown` even when normal operation attempted cleanup.\nKeep cleanup idempotent because cancellation, reload, session replacement, and process exit can converge on the same path.\nUse `ctx.shutdown()` to request an orderly process shutdown.\n\n<a id=\"examples-reference\"></a>\n<a id=\"use-examples-as-the-implementation-reference\"></a>\n\n## Examples and reference\n\nThe checked [extension examples](../examples/extensions/) are the implementation reference. Start with the smallest example matching the behavior you need:\n\n| Goal | Start with | Why |\n|---|---|---|\n| Confirm or block tool calls | [`permission-gate.ts`](../examples/extensions/permission-gate.ts) | Combines `tool_call`, mode-aware UI, and fail-closed behavior. |\n| Modify prompt sections for a run | [`prompt-customizer.ts`](../examples/extensions/prompt-customizer.ts) | Uses structured prompt options instead of replacing the complete prompt. |\n| Keep branch-aware tool state | [`todo.ts`](../examples/extensions/todo.ts) | Reconstructs state from persisted entries and renders it in the TUI. |\n| Register tools after startup | [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts) | Shares registration logic between lifecycle and command handlers. |\n| Run cancellable model work from a command | [`qna.ts`](../examples/extensions/qna.ts) | Combines nested model calls, cancellation, and custom UI. |\n| Replace the main editor | [`modal-editor.ts`](../examples/extensions/modal-editor.ts) | Extends `CustomEditor` while preserving application controls. |\n| Run finalization at shutdown | [`auto-commit-on-exit.ts`](../examples/extensions/auto-commit-on-exit.ts) | Uses the session shutdown hook for finalization work. |\n| Route built-in tools to another machine | [`ssh.ts`](../examples/extensions/ssh.ts) | Reuses tool operation interfaces instead of recreating tool contracts. |\n| Add OAuth and provider-specific streaming | [`custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/) | Shows a complete provider integration while delegating supported protocols. |\n\nUse [Custom Providers](custom-provider.md) for model-service integrations, [Terminal UI](tui.md) for custom components, and [Pi Packages](packages.md) to install or distribute extensions with other resources. Use the exported declarations for exact signatures, payloads, and result schemas; this guide focuses on lifecycle choices, composition, and durable implementation patterns.\n","packages/coding-agent/docs/how-pi-works.md":"# How Pi Works\n\nPi coordinates model requests, tool execution, context assembly, and session storage. A session is Pi's record of a conversation, including messages, tool calls and results, model changes, compactions, and other events.\n\nMessages and events in a session form a tree. Each path through that tree is a branch. The branch ending at the current entry is the active branch and supplies the history for the next model request.\n\n## Agent loop\n\nA submitted message is added to the active branch. Pi builds a model request from the system prompt, active branch, available tools, and model settings, then sends it through the selected provider.\n\nThe provider streams an assistant response, which can contain text and tool calls. Pi records the response, executes each tool call, and records the results. That completes one turn. If tool results or queued messages require another model request, Pi starts another turn. Otherwise, the run ends.\n\nSteering messages enter after the current assistant turn. Follow-up messages enter after the agent has finished its pending work. Aborting stops the current run and returns queued messages to the editor.\n\n## Context\n\nThe active branch supplies conversation history. Pi converts its session entries into model-compatible user, assistant, and tool-result messages.\n\nPi builds the system prompt from its base instructions and discovered context files. The request also carries tool definitions and skill descriptions.\n\nFull skill instructions are loaded on demand. Extensions can add instructions or transform context.\n\nPrompt templates expand editor input before it becomes a user message. Selected files, images, pasted text, and shell output can become message content.\n\n## Sessions\n\nPersistent sessions are JSONL files. Each tree entry has an ID and refers to its parent. The current entry identifies the active branch.\n\nContinuing from an earlier entry creates another branch in the same file. Forking and cloning copy selected history into a new session file.\n\nModel context is reconstructed from the active branch. Compaction inserts a summary entry that replaces older messages in subsequent model requests. The original entries remain in the session tree.\n\n## Interfaces\n\nInteractive mode renders session and agent events in the terminal. Print mode runs a prompt and writes the final response. JSON mode writes agent events as JSONL.\n\nRPC mode accepts JSONL commands on stdin and writes responses and events to stdout. The TypeScript SDK creates and controls agent sessions in process.\n\nAll interfaces use the same agent and session mechanisms.\n\n## Extensions and resources\n\nExtensions are TypeScript modules loaded into the Pi process. Their factory functions register tools, commands, shortcuts, providers, event handlers, renderers, and terminal UI.\n\nSkills provide on-demand instructions and supporting files. Prompt templates provide reusable message text. Themes provide terminal colors. Pi packages distribute these resources through npm or git.\n\n## Trust and permissions\n\nPi resolves project trust before loading project settings and resources. After the trust decision and project-resource loading, Pi loads context files. Enabled tools use the operating-system permissions of the Pi process. Extensions execute inside that process.\n","packages/coding-agent/docs/index.md":"# Pi\n\nPi is an extensible AI agent that works from your terminal. Give it a goal and a working folder, and it can inspect files, run commands, edit content, and work through multi-step tasks.\n\nUse Pi for software development, research notes, writing projects, data files, or hobby work. You can use Pi as is, prompt it to adapt itself to your workflow, or build other applications powered by Pi using the SDK.\n\n## Start using Pi\n\nNew to Pi? Follow the [Quickstart](quickstart.md) to install Pi, connect a model, and complete your first task.\n\nIf Pi is already installed, choose what you want to do:\n\n- [Use Pi interactively](usage.md) to add files, run commands, direct ongoing work, and export results.\n- [Choose a model](models.md) or connect a subscription, API key, local model, or compatible endpoint.\n- [Continue or branch a session](sessions.md) to resume work or explore another approach without losing history.\n- [Configure Pi](configuration.md) for your preferences, working folders, instructions, and reusable resources.\n- [Understand how Pi works](how-pi-works.md), including tools, context, sessions, and the agent loop.\n\n## Customize Pi\n\nPi can reuse prompts, load specialized instructions, add executable integrations, change its terminal interface, connect model services, and distribute these resources as packages.\nUse the [Quickstart customization chooser](quickstart.md#choose-how-to-customize-pi) to select the smallest mechanism that meets your need.\n\n## Automate or embed Pi\n\n- Use [print mode](cli.md#invocation-and-output) for one-off and scripted tasks.\n- Use [JSON event stream mode](json.md) to consume structured events from one run.\n- Use [RPC mode](rpc.md) to control a separate Pi process.\n- Use the [TypeScript SDK](sdk.md) to run Pi inside an application.\n\n## Find reference and setup information\n\nUse the reference pages to look up [CLI options](cli.md), [settings](settings.md), [providers](providers.md), [keybindings](keybindings.md), and [environment variables](environment-variables.md).\n\nFor platform-specific help, see [Terminal Setup](terminal-setup.md), [Windows](windows.md), [tmux](tmux.md), [Termux on Android](termux.md), or [Containerization](containerization.md).\n\n## Work safely\n\nPi's tools and extensions run with the permissions of the Pi process. Project trust controls which project resources Pi loads, but it does not sandbox tool calls. Review [Security](security.md) before using untrusted files, repositories, extensions, or unattended automation.\n","packages/coding-agent/docs/json.md":"# JSON Event Stream\n\nJSON mode emits structured progress for one invocation:\n\n```bash\npi --mode json \"Review this repository\"\n```\n\nPi writes one session header followed by session events, then exits after the supplied prompts finish. RPC mode emits the same session-event shapes but has no session header because it is a bidirectional, long-lived protocol. See [RPC Mode](rpc.md).\n\nThis page is the canonical reference for events shared by JSON and RPC mode. Message values use the [shared message types](message-types.md).\n\n## Framing and process I/O\n\nThe stream uses strict JSONL framing. Each record is one JSON object terminated by LF (`\\n`). Split records only on LF and strip an optional preceding carriage return. Unicode line and paragraph separators are valid inside JSON strings and are not record boundaries.\n\nNode.js `readline` is not suitable for this stream because it also recognizes those Unicode separators. Use a byte or UTF-8 stream decoder and split on LF.\n\nRead stdout continuously. A reader that stops consuming records can stall Pi when the pipe buffer fills. Stdout is reserved for JSONL; diagnostics and application logging go to stderr.\n\n## Session header\n\nThe first JSON-mode record is the current [session header](session-format.md#sessionheader):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path\"}\n```\n\nRPC mode does not emit this record. Use [`get_state`](rpc-commands.md#get_state) for its current session ID and file.\n\n## Event sequence\n\nA basic run produces records like these:\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],\"stopReason\":\"pending\",\"...\":\"...\"}}\n{\"type\":\"message_update\",\"usage\":{\"...\":\"...\"},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"}}\n{\"type\":\"turn_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[{\"...\":\"...\"}],\"willRetry\":false}\n{\"type\":\"agent_settled\",\"aborted\":false}\n```\n\n`agent_end` closes one low-level agent run. Automatic retry, overflow recovery, compaction retry, steering, or follow-up work can still continue. `agent_settled` means Pi has no remaining automatic work for that session-level run.\n\n## Agent and turn events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `agent_start` | None | A low-level agent run started. |\n| `agent_end` | `messages`, `willRetry` | That low-level run ended. `messages` contains messages generated by the run. |\n| `agent_settled` | `aborted` | Pi will not continue automatically through retries, compaction recovery, or queued messages. `aborted` is `true` when the run ended because it was aborted. |\n| `turn_start` | None | One assistant turn started. |\n| `turn_end` | `message`, `toolResults` | One assistant response and its resulting tool calls finished. |\n\nA turn is one assistant response plus any tool calls and tool results produced by that response.\n\n## Message events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `message_start` | `message` | A message started. |\n| `message_update` | `usage`, `assistantMessageEvent` | An assistant message emitted a content-block update. |\n| `message_end` | `message` | A message completed. This is the authoritative final message. |\n\n### Reconstruct streaming messages\n\nWire `message_update` records are delta-only. They omit the SDK event's cumulative `message` field and every `assistantMessageEvent.partial` snapshot so stream size remains linear.\n\nThe nested event is one of:\n\n| Type | Fields in addition to `type` | Meaning |\n|---|---|---|\n| `start` | None | The provider stream started; its cumulative `partial` field is removed on the wire. |\n| `text_start` | `contentIndex` | A text block started. |\n| `text_delta` | `contentIndex`, `delta` | Append text to the block. |\n| `text_end` | `contentIndex`, `content` | The text block ended with authoritative content. |\n| `thinking_start` | `contentIndex` | A thinking block started. |\n| `thinking_delta` | `contentIndex`, `delta` | Append thinking text to the block. |\n| `thinking_end` | `contentIndex`, `content` | The thinking block ended with authoritative content. |\n| `toolcall_start` | `contentIndex`, `id`, `toolName` | A tool-call block started. |\n| `toolcall_delta` | `contentIndex`, `delta` | Append serialized argument data. |\n| `toolcall_end` | `contentIndex`, `toolCall` | The tool call ended with the complete `ToolCall`. |\n| `done` | `reason`, `message` | The provider stream completed successfully. |\n| `error` | `reason`, `error` | The provider stream ended with an error or abort message. |\n\nThe normal agent loop translates provider-level `start`, `done`, and `error` into `message_start` and `message_end` session events rather than emitting them as `message_update`. They remain admitted by the exported `JsonAgentSessionEvent` transformation for callers that construct a matching session event.\n\nUse `contentIndex` to identify the content block. Buffer `delta` fields for a live display, but replace reconstructed data with the completed content in `text_end`, `thinking_end`, or `toolcall_end`. Replace the whole partial message with `message_end.message` when it arrives.\n\nThe top-level `usage` is the latest cumulative provider-reported usage for the assistant response. It can remain zero until completion when a provider does not report usage while streaming.\n\n```json\n{\"type\":\"message_update\",\"usage\":{\"input\":100,\"output\":1,\"cacheRead\":0,\"cacheWrite\":0,\"totalTokens\":101,\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0,\"total\":0}},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello \"}}\n```\n\n## Tool execution events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `tool_execution_start` | `toolCallId`, `toolName`, `args` | Tool execution started. |\n| `tool_execution_update` | `toolCallId`, `toolName`, `args`, `partialResult` | The tool reported a partial result. |\n| `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError`, `durationMs` | Tool execution finished. `durationMs` is how long the tool's `execute()` took, measured with a monotonic clock; absent when the tool did not run. |\n\nUse `toolCallId` to correlate the lifecycle. `partialResult` is the latest partial result supplied by the tool. Whether it replaces or extends an earlier update depends on that tool's result contract.\n\n```json\n{\"type\":\"tool_execution_start\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"}}\n{\"type\":\"tool_execution_update\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"},\"partialResult\":{\"content\":[{\"type\":\"text\",\"text\":\"partial output\"}],\"details\":{}}}\n{\"type\":\"tool_execution_end\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"complete output\"}],\"details\":{}},\"isError\":false}\n```\n\n## Queue and state events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `queue_update` | `steering`, `followUp` | The pending steering or follow-up queue changed. Both fields contain the complete current queue. |\n| `entry_appended` | `entry` | An extension appended a custom session entry through `pi.appendEntry()`. |\n| `session_info_changed` | `name` | The session display name changed. An absent `name` means it was cleared. |\n| `thinking_level_changed` | `level` | The active thinking level changed. |\n\nThe `entry` value uses a persisted [session entry type](session-format.md#entry-types).\n\n## Compaction events\n\n`compaction_start` reports why compaction began:\n\n```json\n{\"type\":\"compaction_start\",\"reason\":\"threshold\"}\n```\n\n`reason` is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n`compaction_end` contains the result when compaction succeeds:\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\"...\": \"...\"},\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\nIf compaction was aborted, `result` is absent and `aborted` is true. If it failed, `result` is absent, `aborted` is false, and `errorMessage` describes the failure. Successful overflow recovery sets `willRetry` to true before Pi retries the prompt.\n\nSee [Compaction and Branch Summaries](compaction.md) for result semantics.\n\n## Retry events\n\nAssistant-turn retry emits:\n\n```json\n{\"type\":\"auto_retry_start\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"529 overloaded\"}\n{\"type\":\"auto_retry_end\",\"success\":true,\"attempt\":2}\n```\n\nOn final failure, `auto_retry_end` has `success: false` and a `finalError` string.\n\nCompaction and branch-summary retry emit:\n\n```json\n{\"type\":\"summarization_retry_scheduled\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"terminated\"}\n{\"type\":\"summarization_retry_attempt_start\",\"source\":\"compaction\",\"reason\":\"threshold\"}\n{\"type\":\"summarization_retry_finished\"}\n```\n\nFor a branch summary, `source` is `\"branchSummary\"` and `reason` is absent. The `reason` on a compaction retry is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n## RPC-only events\n\nA direct RPC [`bash`](rpc-commands.md#bash) command emits one `bash_execution_update` for each output chunk. Its optional `id` matches the command ID. The final command response can contain truncated output, but these events stream all output:\n\n```json\n{\"type\":\"bash_execution_update\",\"id\":\"req-1\",\"delta\":\"total 48\\n\"}\n```\n\nRPC also adds `extension_error` when an extension handler throws:\n\n```json\n{\"type\":\"extension_error\",\"extensionPath\":\"/path/to/extension.ts\",\"event\":\"tool_call\",\"error\":\"Error message\"}\n```\n\nExtension UI records are a separate RPC subprotocol, not `AgentSessionEvent` values. See [RPC Extension UI](rpc-extension-ui.md).\n\n## TypeScript types\n\nThe SDK's `AgentSessionEvent` contains cumulative streaming snapshots for in-process consumers. JSON and RPC transform only `message_update`:\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAssistantMessageEvent<T> = T extends { type: \"toolcall_start\"; partial: unknown }\n  ? WithoutPartial<T> & { id: string; toolName: string }\n  : WithoutPartial<T>;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      usage: Usage;\n      assistantMessageEvent: JsonAssistantMessageEvent<AssistantMessageEvent>;\n    };\n```\n\nUse the exported `JsonAgentSessionEvent` type from `@earendil-works/pi-coding-agent`. Its implementation is in [`json-event.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/json-event.ts).\n\n## Example\n\nPrint completed messages from a one-shot run:\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```\n","packages/coding-agent/docs/keybindings.md":"# Keybindings Reference\n\nPi exposes named actions, such as `app.session.new`, that can be assigned keybindings. You can change default assignments or bind unassigned actions in Pi's [user configuration](configuration.md#agent-directory).\n\nRun `/hotkeys` to see the active shortcuts for the main editor and application.\n\n## Assign keybindings\n\nCreate `<agent-dir>/keybindings.json`. The agent directory defaults to `~/.pi/agent` and is described in [Agent directory](configuration.md#agent-directory).\n\nMap each action identifier to one key or a list of keys:\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/keybindings.schema.json\",\n  \"app.session.new\": \"ctrl+shift+n\",\n  \"app.session.tree\": [\"ctrl+shift+t\", \"alt+shift+t\"]\n}\n```\n\nA configured value replaces the default for that action. Use an empty list to disable an action's keybindings:\n\n```json\n{\n  \"tui.altScreen.pageUp\": []\n}\n```\n\nAfter editing the file, run `/reload` to apply the changes to the active session.\n\n## Key syntax\n\nWrite a key as `modifier+key`. Modifiers are `ctrl`, `shift`, `alt`, and `super`. You can combine modifiers. Valid keys are:\n\n- **Letters:** `a-z`\n- **Digits:** `0-9`\n- **Special:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Function:** `f1`-`f12`\n- **Symbols:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nExamples: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, and `ctrl+1`.\n\n`super` bindings require a terminal that reports the modifier separately, typically through the Kitty keyboard protocol. They may not work in terminals without that support.\n\n## Actions\n\n### Terminal UI\n\n#### Cursor movement\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.cursorUp` | `up` | Move cursor up, browsing older history at the top |\n| `tui.editor.cursorDown` | `down` | Move cursor down, browsing newer history at the bottom |\n| `tui.editor.historyPrevious` | None | Select the previous prompt history entry |\n| `tui.editor.historyNext` | None | Select the next prompt history entry |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Move cursor left |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Move cursor right |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Move cursor word left |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Move cursor word right |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+a` | Move to line start |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+e` | Move to line end |\n| `tui.editor.jumpForward` | `ctrl+]` | Jump forward to character |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Jump backward to character |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Scroll up by page |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Scroll down by page |\n\nThe dedicated history actions browse prompt history regardless of cursor position and take precedence over application actions using the same key.\n\n#### Text editing\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.deleteCharBackward` | `backspace` | Delete character backward |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Delete character forward |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Delete word backward |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Delete word forward |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Delete to line start |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Delete to line end |\n| `tui.editor.yank` | `ctrl+y` | Paste most recently deleted text |\n| `tui.editor.yankPop` | `alt+y` | Cycle through deleted text after yank |\n| `tui.editor.undo` | `ctrl+-` (`ctrl+z` on Windows; `alt+z` on WSL) | Undo last edit |\n\n#### Input and selection\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insert new line |\n| `tui.input.submit` | `enter` | Submit input |\n| `tui.input.tab` | `tab` | Tab or autocomplete |\n| `tui.input.copy` | `ctrl+c` | Copy selection |\n| `tui.select.up` | `up` | Move selection up |\n| `tui.select.down` | `down` | Move selection down |\n| `tui.select.pageUp` | `pageUp` | Page up in list |\n| `tui.select.pageDown` | `pageDown` | Page down in list |\n| `tui.select.confirm` | `enter` | Confirm selection |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Cancel selection |\n\n#### Fullscreen\n\nIn fullscreen mode, these actions control the transcript and take precedence over editor actions using the same key.\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.altScreen.pageUp` | `pageUp` | Scroll the transcript up by one page |\n| `tui.altScreen.pageDown` | `pageDown` | Scroll the transcript down by one page |\n| `tui.altScreen.halfPageUp` | None | Scroll the transcript up by half a page |\n| `tui.altScreen.halfPageDown` | None | Scroll the transcript down by half a page |\n| `tui.altScreen.lineUp` | None | Scroll the transcript up by one line |\n| `tui.altScreen.lineDown` | None | Scroll the transcript down by one line |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up`, `ctrl+up` (`ctrl+up` only on Windows and WSL) | Jump to the previous marked message |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down`, `ctrl+down` (`ctrl+down` only on Windows and WSL) | Jump to the next marked message |\n| `tui.altScreen.search` | `ctrl+shift+f` (`ctrl+f` on Windows and WSL) | Search the rendered transcript |\n| `tui.altScreen.searchNext` | `enter`, `ctrl+g` | Select the next search match while searching |\n| `tui.altScreen.searchPrevious` | `shift+enter`, `ctrl+shift+g` | Select the previous search match while searching |\n| `tui.altScreen.searchClose` | `escape` | Close transcript search |\n| `tui.altScreen.top` | `ctrl+home` | Scroll to the beginning of the transcript |\n| `tui.altScreen.bottom` | `ctrl+end` | Scroll to the transcript end and follow new output |\n\n### Application\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Cancel / abort |\n| `app.clear` | `ctrl+c` | Clear editor (first) / exit (second) |\n| `app.exit` | `ctrl+d` | Exit (when editor empty) |\n| `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background |\n| `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard |\n\nOn native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior.\n\n### Sessions\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.session.new` | None | Start a new session (`/new`) |\n| `app.session.tree` | None | Open session tree navigator (`/tree`) |\n| `app.session.fork` | None | Fork current session (`/fork`) |\n| `app.session.resume` | None | Open session resume picker (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Toggle path display |\n| `app.session.toggleSort` | `ctrl+s` | Toggle sort mode |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Toggle named-only filter |\n| `app.session.rename` | `ctrl+r` | Rename session |\n| `app.session.delete` | `ctrl+d` | Delete session |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Delete session when query is empty |\n\n### Models and Thinking\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Open model selector |\n| `app.model.cycleForward` | `ctrl+p` | Cycle to next model |\n| `app.model.cycleBackward` | `shift+ctrl+p` (`alt+p` on Windows and WSL) | Cycle to previous model |\n| `app.models.save` | `ctrl+s` | Save the selected default model or scoped model configuration to settings |\n| `app.thinking.cycle` | `shift+tab` | Cycle thinking level |\n| `app.thinking.save` | `ctrl+s` | Save current thinking level to settings |\n| `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks |\n\n### Display and Message Queue\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Collapse or expand tool output |\n| `app.message.copy` | `ctrl+x` | Copy the selected message in `/tree`; in fullscreen mode, copy the active selection when `fullscreenCopyOnSelect` is `false`; otherwise copy the last assistant message. On OAuth sign-in screens, copy the sign-in URL |\n| `app.message.followUp` | `alt+enter` (`ctrl+q` on Windows and WSL) | Queue follow-up message |\n| `app.message.dequeue` | `alt+up` (`alt+q` on Windows and WSL) | Restore queued messages to editor |\n\n### Tree Navigation\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Fold current branch segment, or jump to the previous segment start |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Unfold current branch segment, or jump to the next segment start or branch end |\n| `app.tree.editLabel` | `shift+l` | Edit the label on the selected tree node |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Toggle label timestamps in the tree |\n| `app.tree.filter.default` | `ctrl+d` | Set tree filter to default view |\n| `app.tree.filter.noTools` | `ctrl+t` | Toggle tree filter that hides tool results |\n| `app.tree.filter.userOnly` | `ctrl+u` | Toggle tree filter that shows only user messages |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Toggle tree filter that shows only labeled entries |\n| `app.tree.filter.all` | `ctrl+a` | Toggle tree filter that shows all entries |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Cycle tree filter forward |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Cycle tree filter backward |\n\n### Scoped Models Selector\n\nUsed inside the scoped models selector (opened via `/scoped-models`).\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.models.enableAll` | `ctrl+a` | Enable all models (or all matching the current search) |\n| `app.models.clearAll` | `ctrl+x` | Clear all models (or all matching the current search) |\n| `app.models.toggleProvider` | `ctrl+p` | Toggle all models for the current provider |\n| `app.models.reorderUp` | `alt+up` | Move the selected model up in the cycle order |\n| `app.models.reorderDown` | `alt+down` | Move the selected model down in the cycle order |\n","packages/coding-agent/docs/llama-cpp.md":"# Local Models with llama.cpp\n\nPi supports the [llama.cpp](https://github.com/ggml-org/llama.cpp) router server. The router discovers multiple GGUF models and loads or unloads them on demand.\n\nUse a current llama.cpp build with router support. Follow the [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) or install a [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) for your platform.\n\n## Start the router\n\nStart `llama-server` without `--model` or `-m`. Passing a model starts single-model mode instead of router mode.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\nImportant options:\n\n- `--models-dir ~/models` discovers local GGUF files.\n- `--no-models-autoload` keeps loading explicit through `/llama`.\n- `--jinja` enables compatible chat templates and tool calling.\n- `-ngl 999` offloads as many layers as possible to the GPU.\n- `-c 32768` sets the context window for each loaded model. Omit it to use the model's native context, which may require substantially more memory.\n\nA single-file model can sit directly in the model directory. Put multimodal and multi-shard models in separate subdirectories:\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\nRestart the router after manually adding files. For per-model context sizes and other options, use [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets). For settings tuned to a specific NVIDIA GPU and model, see [Run Pi with NVIDIA RTX and llama.cpp](nvidia-rtx-llama-cpp.md).\n\n## Configure Pi\n\nStart Pi and configure the provider:\n\n```text\n/login llama.cpp\n```\n\nEnter the router URL and optional API key. The default URL is `http://127.0.0.1:8080`.\n\nIf you start the router with `--no-models-autoload`, `/login llama.cpp` only stores the connection. Run `/llama` to load a model, then `/model` to select the loaded model for the current session.\n\nEnvironment variables can configure the same values without `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nIf the server uses an API key, start `llama-server` with the matching `--api-key` value. Keep `--host 127.0.0.1` for local-only access.\n\n## Manage models\n\nRun:\n\n```text\n/llama\n```\n\n- Select an unloaded model to load it.\n- Select a loaded model to unload it.\n- Select **Download model…**, search Hugging Face, then choose a repository and quantization. Exact `owner/repository[:quant]` values also work.\n- Press Escape during a load or download to confirm cancellation.\n\nHugging Face search uses `HF_TOKEN` when set, then checks `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token`, and `~/.cache/huggingface/token`. Search also works without authentication, subject to lower rate limits. Pi warns before downloading gated repositories and links to their access page. The llama.cpp server performs the download, so its process must also have `HF_TOKEN` when the selected repository requires access.\n\nIf other models are loaded, Pi asks whether to unload them first or keep them loaded. Pi does not silently unload models and never deletes model files. The router may be shared with other clients, so `/llama` always displays the router's current state.\n\nLoaded and sleeping models appear in `/model`. Sleeping models wake automatically when selected. With router autoload enabled, unloaded preset models also appear and load when selected. With `--no-models-autoload`, load a model through `/llama` before selecting it.\n\nIf the router disconnects, `/llama` shows **Retry** and **Close**. Retry reconnects and refreshes model state without replaying the interrupted operation.\n\n## Classification\n\nClassifier models answer typed `choice`, `bool`, and `score` questions about JSON state, like TypeSafe's Jev models. The model reaches them from [`codemode`](cli.md#enable-codemode) scripts, and extensions through `ctx.modelRegistry.classify()`; see [Classifier models](models.md#use-classifier-models). Pi lists llama.cpp models as classifiers in two ways:\n\n- **Decision models** such as [Julia-1, Laya, Kev, lev, and OpenJev](https://huggingface.co/collections/ggml-org/decision-models-6abf80cca3c83f127060a769) answer natively through llama.cpp's `/v1/systemone` endpoint. They appear only as classifiers, with the `typesafe-system-one` API, and not in `/model`.\n- **Chat models** are also listed as classifiers with the same ID and the `llama-cpp-classify` API, which reads answers from next-token probabilities as described below.\n\nllama.cpp 0.6.0 and later report decision models in the router's model list: their `architecture.output_modalities` contains `decisions`. The router reads this from the GGUF metadata without loading the model, so Pi recognizes unloaded and sleeping decision models too. Older llama.cpp builds do not report it, and Pi lists their decision models as chat models.\n\n### Chat models as classifiers\n\nThe model does not generate an answer. Each question becomes one chat prompt: the state, every question of the request, the state again, and then the question with its answers under single-token labels. Labels are letters for a choice (up to 62 options), `Yes`/`No` for a bool, and digits for a score (up to 10 levels). The second copy of the state is read with the questions in view, which improved accuracy on JevBench with small models. Pi reads the probabilities of the labels as the next token and normalizes them. A choice returns every option's probability and a confidence of `(n * peak - 1) / (n - 1)`; a score returns the expected level.\n\n- Raw label probabilities are usually overconfident. The per-request `temperature` option divides the label logits before normalizing; values above 1 soften the distribution. It changes no answer.\n- Questions run one after another. Everything before the final question is the same for all questions of a request, so the server's prompt cache evaluates it once. The state appears twice, so it needs twice its size in context.\n- Small models may follow instructions written inside the state. The prompt tells the model to judge the state as data, but that is not a guarantee.\n- Hybrid models such as Qwen3.5 cannot rewind a partially cached prompt without context checkpoints. If each question reprocesses the whole state, start the router with `--ctx-checkpoints 32 --checkpoint-min-step 0`.\n\n## Troubleshooting\n\nCheck that the router is reachable:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **No models in `/llama`:** Check `--models-dir`, the directory layout, and restart the router.\n- **Model missing from `/model` with `--no-models-autoload`:** Load it with `/llama` first.\n- **Load fails or uses too much memory:** Lower `-c` or unload another model.\n- **Server is not in router mode:** Start it without `--model`, `-m`, or `-hf`.\n\nTo remove the `llama.cpp` provider and `/llama`, disable `llama.cpp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:llama.cpp\"]` in [settings](settings.md#resources).\n","packages/coding-agent/docs/mcp.md":"# MCP Servers\n\nPi connects to [Model Context Protocol](https://modelcontextprotocol.io) servers over stdio or streamable HTTP and makes their tools and resources available to the model.\n\n## Quick setup\n\nAdd a local stdio server, check the connection, then start Pi:\n\n```bash\npi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .\npi mcp list\npi\n```\n\nFor a remote server:\n\n```bash\npi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN\npi mcp list\n```\n\nThese commands add user-level servers by default. Add `--local` or `-l` to write the project configuration instead:\n\n```bash\npi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp\n```\n\nUse `/mcp` inside an interactive session to inspect connections, sign in, reconnect, change exposure, or enable and disable servers. Run `/reload` after adding, removing, or changing a server outside the session.\n\n## Configure servers\n\nPi reads user-level servers from `~/.pi/agent/mcp.json` and project servers from `.pi/mcp.json`. Project configuration is read only after [project trust](security.md#understand-project-trust) is granted. A project entry replaces a user-level entry with the same name.\n\nA project entry without `command`, `url`, or `type` overrides only `enabled`, `exposure`, and `toolExposure` of the user-level server with the same name and keeps the rest, including `env`, `headers`, and `auth`. For example, this turns off a user-level server in one project:\n\n```json\n{\n  \"mcpServers\": {\n    \"internal-tools\": { \"enabled\": false }\n  }\n}\n```\n\nThe format matches other MCP clients:\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \".\"]\n    },\n    \"docs\": {\n      \"url\": \"https://example.com/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer ${DOCS_TOKEN}\" },\n      \"description\": \"Search and read the product documentation\"\n    }\n  }\n}\n```\n\nStdio servers use `command`, `args`, `env`, and `cwd`. Relative `cwd` values resolve against the session directory. A leading `~/` in `command`, an argument, or `cwd` names the home directory.\n\nHTTP servers use `url`, `headers`, and `oauth` (see [Authenticate with OAuth](#authenticate-with-oauth)). The legacy SSE transport is not supported.\n\nBoth server types support:\n\n- `timeout`: per-request timeout in seconds (default 60). Progress notifications reset it.\n- `enabled: false`: keep the entry without connecting to it.\n- `exposure` and `toolExposure`: control how tools reach the model (see [Control tool exposure](#control-tool-exposure)).\n- `description`: what the server offers, in a sentence. It lists the server in the system prompt (see [Control tool exposure](#control-tool-exposure)), tool search ranks the server's tools by it, and codemode's `describeNamespace()` returns it. Without it, the first line of the server instructions is used once the server connects.\n\nKeep personal servers and servers with credentials in the user-level file. Use the project file only for servers the project requires, and only in trusted projects.\n\n### Configuration rules\n\n- Server names may contain only letters, digits, `_`, and `-`. Tools are named `mcp__<server>__<tool>`, with every character other than letters, digits, and `_` replaced by `_`; tools of a server whose names then collide all get a hash suffix. Server names that differ only in `-` and `_` count as the same server: a second one is rejected, and a `mcp.json` server overrides a registered one.\n- `type` is optional. A `command` selects stdio and a `url` selects streamable HTTP. When present, `type` must be `stdio`, `http`, or `streamable-http`.\n- `sse` is rejected. Servers that document an SSE endpoint often also provide streamable HTTP, commonly at `/mcp` instead of `/sse`.\n- `command` is one executable and `args` contains its arguments. It is not a shell command string.\n- `env` and `headers` values can use environment variables such as `${GITHUB_TOKEN}`. They can also run a command with `!command`, but the command must make up the whole value, for example `\"Authorization\": \"!echo Bearer $(gh auth token)\"`.\n- Invalid entries are reported and skipped without preventing other servers from connecting.\n\n`pi mcp add` and `pi mcp remove` cover common changes from a shell. See [MCP commands](cli.md#mcp-commands) for their options.\n\n### Inspect or change a server\n\n`/mcp` lists configured servers with their state, tool count, exposure, and configuration source. Servers that need attention appear first. Select a server to inspect its tools and connection details, reconnect, sign in or out, change exposure, or enable and disable it.\n\nExposure and enabled-state changes are saved to the file that defines the server without replacing unrelated content. In a trusted project, \"Enable in this project\" and \"Disable in this project\" add a project override for a user-level server; later changes to that server are saved to the override. Disabled servers remain listed. Outside the interactive TUI, `/mcp` prints server status; `/mcp login <server>`, `/mcp logout <server>`, and `/mcp reconnect <server>` perform those actions directly.\n\nShell commands work without a session: `pi mcp add`, `pi mcp remove`, `pi mcp list`, `pi mcp login`, and `pi mcp logout`. Shell commands do not load extensions.\n\n### Diagnose connection problems\n\nRun `pi mcp list` to connect to every enabled server and print its state, tools, and errors. It exits with status 1 when an entry is invalid or an enabled server is not connected. `/mcp` shows the full connection error and the tail of stderr from a failed stdio server.\n\nPi reports configuration errors, failed connections, and required sign-ins once after startup. Server logging notifications are appended to `~/.pi/agent/mcp.log` as `<time> [<server>] <level> <logger>: <message>`. The file moves to `mcp.log.1` after it grows past 5 MB.\n\nPi connects every enabled server in the background when a session starts. A server's tools appear once it connects; the `codemode` description does not list them, so it does not change when servers connect. The first prompt waits up to 10 seconds only for servers with `direct` tools, which must be declared in its request. Other servers are waited for when they are needed: a codemode script waits for the servers it names (`mcp__<server>`) and, when it calls `searchTools()` or reads `ALL_TOOLS`, for all of them; `tool_search` and the resource tools also wait for all of them. HTTP network errors and transient statuses (408, 429, and 5xx) are retried twice. A dropped connection is shown as disconnected and reconnects on the next call. When a server announces a changed tool list, new tools are added and withdrawn tools become unreachable.\n\nStopping a stdio server closes its stdin, sends SIGTERM, then sends SIGKILL to its process group. This also stops servers launched through wrappers such as `npx` or `uvx`.\n\n## Migrate configuration from another client\n\nMove the converted entry under `mcpServers` in `mcp.json`, then run `pi mcp list` to validate it.\n\n| Client | Conversion |\n|---|---|\n| Claude Desktop, Claude Code, or Cursor | Copy the existing `mcpServers` entry. |\n| VS Code | Move an entry from the top-level `servers` object and replace `${input:...}` prompts with `${NAME}` environment variables. |\n| Codex | Convert `[mcp_servers.<name>]` TOML fields such as `command`, `args`, `env`, and `url` to JSON. |\n| OpenCode | Convert `\"type\": \"local\"` to a stdio entry, split its `command` array into `command` and `args`, rename `environment` to `env`, and replace `{env:NAME}` with `${NAME}`. Convert `\"type\": \"remote\"` to a URL entry. |\n\n## Authenticate with OAuth\n\nRemote servers that use OAuth, such as Sentry, need no credentials in `mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"sentry\": { \"url\": \"https://mcp.sentry.dev/mcp\" }\n  }\n}\n```\n\nWhen the server rejects an unauthenticated connection, `/mcp` shows that it needs sign-in. Select \"Sign in\", run `/mcp login sentry`, or run `pi mcp login sentry`. Pi opens the authorization page and waits for approval. If the browser runs on another machine, such as over SSH, paste its redirected URL into the sign-in screen. A running session uses the new credentials on its next turn.\n\nPi registers itself with the authorization server, stores tokens in `~/.pi/agent/mcp-auth.json`, and refreshes access tokens when they expire or the server rejects them. If a server later requests additional scope, Pi asks for sign-in again. Signing out deletes the stored credentials.\n\nCredentials belong to a server name and URL. Servers with the same URL under different names, such as one per account, sign in separately; servers with the same name and URL in different `mcp.json` files share one sign-in.\n\nOAuth applies to HTTP servers without an `Authorization` header. For a server that does not support dynamic client registration, configure a registered client:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"clientId\": \"my-client\", \"clientSecret\": \"${EXAMPLE_SECRET}\", \"callbackPort\": 8765 }\n    }\n  }\n}\n```\n\nThe redirect URI must match the registered URI. `callbackPort` uses `http://127.0.0.1:<port>/callback`. To use another URI, set `callbackUrl`; it must use HTTP on `localhost`, `127.0.0.1`, or `[::1]`. Pi sends it exactly as written. When `callbackUrl` omits a port, Pi uses `callbackPort` or a free port and adds it to the URI, as allowed for loopback redirects by RFC 8252. `clientSecret` is optional and can use an environment variable or command.\n\nSet `scope` to a space-separated list for servers that do not advertise their required scopes. Otherwise, Pi requests the advertised scopes. Later scope requests are added to the configured value.\n\nPi registers as `pi`. Some servers only accept registrations from known clients. Set `clientName` to send another name:\n\n```json\n{\n  \"mcpServers\": {\n    \"figma\": { \"url\": \"https://mcp.figma.com/mcp\", \"oauth\": { \"clientName\": \"Claude Code\" } }\n  }\n}\n```\n\nThe name is only sent when Pi registers a client. To register again under a new name, sign out first.\n\nSome authorization servers allow clients by their Client ID Metadata Document URL instead of registering them. Set `clientRegistration` to `cimd` to identify as Pi's document on pi.dev instead of registering:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": { \"url\": \"https://mcp.example.com/mcp\", \"oauth\": { \"clientRegistration\": \"cimd\" } }\n  }\n}\n```\n\nThe client ID is `https://pi.dev/oauth/client.json` with the redirect URI `http://127.0.0.1:<port>/callback`. If the authorization server does not send the `iss` parameter in authorization responses (RFC 9207), Pi uses a document and redirect path specific to the MCP server instead: `https://pi.dev/oauth/<id>/client.json` with `http://127.0.0.1:<port>/callback/<id>`. The authorization server must advertise Client ID Metadata Document support and public clients, or sign-in fails. `cimd` cannot be combined with `clientId` or `clientName`, and a `callbackUrl` must use `localhost` or `127.0.0.1` with the path `/callback`.\n\nPi finds the authorization server through the server's protected resource metadata (RFC 9728) and checks that the authorization server's metadata names the expected issuer (RFC 8414). Some servers advertise the wrong authorization server or none, so sign-in opens a page that does not exist. Set `authServerMetadataUrl` to the metadata document of the right authorization server:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"authServerMetadataUrl\": \"https://example.okta.com/.well-known/openid-configuration\" }\n    }\n  }\n}\n```\n\nPi uses that document instead of discovery and trusts it as configured, so only point it at a document you trust. The URL must use HTTPS, except on `localhost`, `127.0.0.1`, or `[::1]`.\n\n## Control tool exposure\n\nEach server tool is registered as `mcp__<server>__<tool>`. The server's `exposure` determines how the model reaches it:\n\n| Exposure | Behavior | Typical use |\n|---|---|---|\n| `codemode` (default) | Callable from [`codemode`](cli.md#tools) scripts, but neither declared to the model nor listed in the codemode description. Scripts find tools with `searchTools()`, `describeTool()`, or `ALL_TOOLS`. | General MCP servers, especially when scripts should combine or filter calls. |\n| `deferred` | Not declared until [`tool_search`](cli.md#tools) loads a match for the next model call. | Large servers whose tools should be called directly after discovery. |\n| `direct` | Declared to the model like a built-in tool and also callable from codemode. | Small, frequently used tool sets. |\n| `hidden` | Registered but unreachable. | Servers or tools that should remain unavailable. |\n\n`codemode-deferred` is accepted as an alias for `codemode`.\n\nServers with `codemode` or `deferred` tools are listed in the `mcp_servers` section of the system prompt, with how their tools are reached and one line from the configured `description` or, once connected, from the server instructions. Pi updates the section when a prompt starts, after waiting for servers with `direct` tools. When it changed, for example because a server connected and its summary became available, Pi appends the new section to the conversation instead of changing tool declarations, so earlier messages stay cached. `describeNamespace()` and the `namespace` option of `searchTools()` accept `mcp__dev-radius`, `mcp__dev_radius`, `dev-radius`, or `dev_radius`.\n\nPi activates `codemode` when a server with `codemode` exposure connects. It activates `tool_search` for a server with `deferred` exposure. To make the model see a tool without searching, give it `direct` exposure with `toolExposure`.\n\n`toolExposure` overrides the server exposure for individual tools. Keys are exact server tool names or patterns where `*` matches any characters. Exact names win over patterns; among patterns, the first match wins. A server with `hidden` exposure can expose only selected tools:\n\n```json\n{\n  \"mcpServers\": {\n    \"github\": {\n      \"url\": \"https://api.githubcopilot.com/mcp/\",\n      \"exposure\": \"deferred\",\n      \"toolExposure\": {\n        \"search_code\": \"direct\",\n        \"get_*\": \"codemode\",\n        \"delete_*\": \"hidden\"\n      }\n    }\n  }\n}\n```\n\n`pi mcp list` marks tools whose exposure differs from their server. The Tools view in `/mcp` also shows the effective exposure.\n\nTools with `codemode` or `deferred` exposure can be reached through either indirect mechanism: codemode scripts can call them, and `tool_search` can load them. Codemode calls do not depend on the active tool set, so they remain available after `/tree`, resume, and fork. Tools loaded by `tool_search` are recorded in the transcript and remain declared on that branch.\n\n`--tools` does not remove MCP tools unless one of its entries starts with `mcp__`; `pi --tools read,codemode,'mcp__radius__*'` keeps only the tools of `radius`. `--exclude-tools` accepts the same patterns, and `--no-mcp` disables MCP for one run (see [Tools](cli.md#mcp-tools)).\n\nTo keep `codemode` active without MCP servers, add `\"defaultTools\": [\"+codemode\"]` to [settings](settings.md#tools). To prevent automatic codemode activation, set `\"autoEnableCodemode\": false` beside `mcpServers`. A project value overrides the user-level value. Pi warns once when neither `codemode` nor `tool_search` is active and non-direct tools cannot be called.\n\nText results over 20 KB reach the model with their middle removed around a `…N chars truncated…` marker. The full text is saved to a temporary file named in the result. Codemode scripts receive the complete result and can reduce it before returning output to the model.\n\nCodemode scripts receive the complete MCP `CallToolResult`, including `content`, `structuredContent`, and `isError`. A result with `isError` resolves inside scripts but is reported as an error for direct calls. `image(result.content[0])` forwards an image block. Server instructions are not part of any tool description; scripts read them with `describeNamespace(\"mcp__<server>\")`, which also returns the server's tool names.\n\n## Use resources\n\nWhen a connected server offers [resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources), Pi adds the resource tools used by Codex and OpenCode:\n\n- `list_mcp_resources` lists resources as JSON: `{ server?, resources: [{ server, uri, name, ... }], nextCursor? }`. With `server`, it lists one page; `cursor` continues with the next page. Without `server`, it lists every resource from every server.\n- `list_mcp_resource_templates` lists URI templates for resources the servers do not list directly.\n- `read_mcp_resource` reads a resource by `server` and `uri`. Text reaches the model as text and images as images. Other binary resources are saved to temporary files, and the model receives the path. Scripts receive `{ server, uri, contents }`.\n\nThese tools reach every enabled, non-hidden server with resources. Their exposure is the widest exposure among those servers: `direct`, then `codemode` or `deferred`. Resource links in tool results identify `read_mcp_resource` and the server.\n\nResources for MCP Apps, identified by `ui://` URIs or `text/html;profile=mcp-app`, are omitted because Pi does not render them. Resource icons are also omitted.\n\nReading and listing resources is retried once after a transient HTTP error (408, 429, or 5xx). Tool calls are not retried because the server may already have performed them.\n\n## Permissions\n\nEvery MCP call passes through Pi's tool pipeline. Extension `tool_call` and `tool_result` handlers, including permission gates, therefore apply to MCP tools. Calls made from codemode scripts carry the codemode call ID as `parentToolCallId`.\n\n`pi.getAllTools()` reports the annotations declared by each server: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. Permission extensions can use these hints to decide which calls require confirmation (see [Tool exposure](extensions.md#tool-exposure)). Resource tools are marked read-only.\n\n## Extensions and SDK\n\n### Add servers from extensions\n\nExtensions can add servers for the current session with `pi.registerMcpServer(name, config)`, using the same shape as an `mcpServers` entry (see [MCP servers in extensions](extensions.md#mcp-servers)). Registered servers connect like configured servers and appear in `/mcp` with the extension as their source.\n\nChanges to enabled state or exposure apply only to the current session. A file-configured server with the same name takes precedence, and `/mcp` lists the overridden registration. `pi mcp` shell commands do not load extensions and only see file-configured servers.\n\n### Replace the built-in MCP support\n\nAn installed extension that registers `/mcp`, such as `pi-mcp-adapter`, replaces the built-in MCP support for sessions. Pi then does not read `mcp.json` or connect its servers in a session, and `/mcp` belongs to the extension. Remove the extension to restore the built-in behavior. To disable built-in MCP support without a replacement, disable `mcp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:mcp\"]` in [settings](settings.md#resources). `--no-mcp` disables it for one run.\n\nAn extension that registers `codemode` or `tool_search` similarly replaces the built-in tool with that name. Pi activates the replacement for MCP servers and waits for the servers its calls need only when it declares [`reachesTools`](extensions.md#tool-exposure). Shell-level `pi mcp` commands always use the built-in implementation.\n\n### Use MCP from the SDK\n\nSDK sessions do not load built-in extensions. Add the MCP extension, the codemode extension for `codemode` servers, and the tool-search extension for `deferred` servers to the resource loader. See [Codemode and MCP](sdk.md#codemode-mcp).\n","packages/coding-agent/docs/message-types.md":"# Message Types\n\nPi uses `AgentMessage` values in SDK state, lifecycle events, RPC responses, and persisted session message entries. This page defines those shared messages and their content blocks.\n\nMessage timestamps are Unix timestamps in milliseconds. They are different from the ISO 8601 timestamps on [session entries](session-format.md#entry-base).\n\nSource definitions:\n\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/ai/src/types.ts) defines provider-facing messages and content blocks.\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts) defines the extensible `AgentMessage` union.\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/messages.ts) adds coding-agent message roles.\n\n## Content blocks\n\n### TextContent\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n  textSignature?: string;\n}\n```\n\n`textSignature` contains provider-specific message metadata. Treat it as opaque.\n\n### ImageContent\n\n```typescript\ninterface ImageContent {\n  type: \"image\";\n  data: string;\n  mimeType: string;\n}\n```\n\n`data` is base64-encoded image data. `mimeType` identifies its media type, such as `image/png` or `image/jpeg`.\n\n### ThinkingContent\n\n```typescript\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n  thinkingSignature?: string;\n  redacted?: boolean;\n}\n```\n\nThinking signatures contain provider-specific replay data. Treat them as opaque. A redacted block can have no visible thinking text while retaining an encrypted payload in `thinkingSignature`.\n\n### ToolCall\n\n```typescript\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n  thoughtSignature?: string;\n  namespace?: string;\n}\n```\n\n`thoughtSignature` is provider-specific. `namespace` identifies an OpenAI Responses namespace for dynamically loaded or namespaced tools.\n\n## Usage\n\nAssistant messages always contain usage. Tool results can contain usage when the tool performed nested model work.\n\n```typescript\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  cacheWrite1h?: number;\n  reasoning?: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\nWhen present, `reasoning` is already included in `output`; do not add it again. `cacheWrite1h` is the subset of `cacheWrite` written with one-hour retention.\n\n## Base messages\n\n### SystemMessage\n\n```typescript\ninterface SystemMessage {\n  role: \"system\";\n  content: string | TextContent[];\n  sections?: Record<string, string | null>;\n  toolsAdded?: Tool[];\n  toolsRemoved?: ToolReference[];\n  timestamp: number;\n}\n```\n\nThe leading system message declares the initial prompt and tools. Later system messages can append instructions, replace or remove named prompt sections, and add or remove tools. Replaying them in order yields the current state.\n\n### UserMessage\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;\n}\n```\n\n### AssistantMessage\n\n```typescript\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  responseModel?: string;\n  responseId?: string;\n  providerThinkingLevel?: string;\n  thinkingLevel?: ModelThinkingLevel;\n  diagnostics?: AssistantMessageDiagnostic[];\n  usage: Usage;\n  stopReason: \"pending\" | \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\" | \"deferred\";\n  deferred?: DeferredHandle;\n  errorMessage?: string;\n  rawStopReason?: string;\n  endTurn?: boolean;\n  timestamp: number;\n}\n```\n\n`responseModel` records a concrete provider response model when it differs from the requested model. `responseId`, `providerThinkingLevel`, `thinkingLevel`, `diagnostics`, and `rawStopReason` preserve provider or runtime details.\n\n`\"pending\"` is used for a partial assistant message while it streams. The completed message in `message_end` has a terminal stop reason, and Pi does not persist `\"pending\"` assistant messages in session JSONL.\n\nA `\"deferred\"` response has a `DeferredHandle` with the provider data needed to retrieve it:\n\n```typescript\ninterface DeferredHandle {\n  provider: string;\n  modelId: string;\n  api: string;\n  id: string;\n  expiresAt?: number;\n  pollAfterMs?: number;\n  data?: JsonValue;\n}\n```\n\n### ToolResultMessage\n\n```typescript\ninterface ToolResultMessage<TDetails = any> {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: TDetails;\n  usage?: Usage;\n  nestedCalls?: NestedToolCalls;\n  isError: boolean;\n  timestamp: number;\n}\n```\n\n`details` is tool-specific. Optional `usage` reports nested model work performed by the tool and contributes to full-session statistics, but it is not part of the main model-call usage. `nestedCalls` records bounded metadata about calls this tool made to other tools:\n\n```typescript\ninterface NestedToolCalls {\n  calls: NestedToolCallRecord[];\n  complete: boolean;\n}\n\ninterface NestedToolCallRecord {\n  id: string;\n  name: string;\n  arguments?: JsonObject;\n  argumentsBytes?: number;\n  status: \"ok\" | \"error\" | \"unfinished\";\n  durationMs?: number;\n  error?: string;\n}\n```\n\n## Coding-agent messages\n\nThe coding-agent package extends `AgentMessage` with four roles.\n\n### BashExecutionMessage\n\nCreated by direct shell commands, including the RPC [`bash`](rpc-commands.md#bash) command. It is not an LLM tool result.\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;\n  timestamp: number;\n}\n```\n\nUnless `excludeFromContext` is true, Pi converts this message to user-role text before the next model request.\n\n### CustomMessage\n\nCreated when an extension sends a context message.\n\n```typescript\ninterface CustomMessage<T = unknown> {\n  role: \"custom\";\n  customType: string;\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;\n  details?: T;\n  timestamp: number;\n}\n```\n\nPi converts its content to a user message for model requests. `display` controls terminal rendering; `details` is not sent to the model.\n\n### BranchSummaryMessage\n\n```typescript\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string | null;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `branch_summary` entry.\n\n### CompactionSummaryMessage\n\n```typescript\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `compaction` entry.\n\n## AgentMessage union\n\nIn the coding agent, the union is equivalent to:\n\n```typescript\ntype AgentMessage =\n  | SystemMessage\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\nAt the lower-level agent package, `AgentMessage` is `Message | CustomAgentMessages[keyof CustomAgentMessages]`. Applications can add roles through TypeScript declaration merging, so consumers should tolerate unknown custom roles when they accept messages from an augmented host.\n","packages/coding-agent/docs/models-configuration.md":"# Models Configuration\n\n<!-- This file is generated from TypeScript source. Do not edit it manually; run npm run generate:references. -->\n\nThis reference lists the fields of `models.json`. See [Configure a compatible endpoint](models.md#configure-a-compatible-endpoint) for examples and [Configuration](configuration.md#agent-directory) for the file location.\n\nAdd `\"$schema\": \"https://pi.dev/schemas/models.schema.json\"` to `models.json` for editor validation and completion.\n\n## Providers\n\nEach key under `providers` is a provider ID. A built-in provider ID changes that provider. Any other ID adds a provider, which needs `baseUrl` and an `api` at provider or model level.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name. |\n| `baseUrl` | string |  | API base URL. Also applies to the provider's built-in models. |\n| `apiKey` | string |  | Literal key, \"$NAME\" environment interpolation, or \"!command\" output. |\n| `api` | string |  | API for the provider's models, such as \"openai-completions\", \"openai-responses\", \"anthropic-messages\", or \"google-generative-ai\". |\n| `oauth` | `\"radius\"` |  | OAuth login flow. Requires baseUrl. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n| `authHeader` | boolean | `false` | Whether to send the resolved API key as Authorization: Bearer. |\n| `models` | [`object[]`](#models) |  |  |\n| `modelOverrides` | [object](#model-overrides) |  |  |\n\n## Models\n\nEach `models` entry adds a model to the provider. An entry whose `id` matches an existing model replaces it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `id` | string |  | Required. Model ID sent to the API. |\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `api` | string |  | Overrides the provider api. |\n| `baseUrl` | string |  | Overrides the provider baseUrl. |\n| `reasoning` | boolean | `false` | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` | `[\"text\"]` |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number | `128000` | Context window in tokens. |\n| `maxTokens` | number | `16384` | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Model overrides\n\nEach `modelOverrides` key is the ID of a built-in or extension model. Unknown IDs are ignored. Object fields, such as `cost`, merge with the model's value field by field; other fields replace it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `reasoning` | boolean |  | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` |  |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Input cost in USD per million tokens. |\n| `cost.output` | number |  | Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number |  | Context window in tokens. |\n| `maxTokens` | number |  | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Input limits\n\nSee [Describe model input and caching](models.md#describe-model-input-and-caching).\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `maxRequestBytes` | integer |  | Maximum serialized provider request size in bytes. |\n| `images` | object |  |  |\n| `images.resize` | object |  | Cache-safe resize profile applied before a new image enters conversation history. |\n| `images.resize.maxWidth` | integer |  |  |\n| `images.resize.maxHeight` | integer |  |  |\n| `images.resize.maxBytes` | integer |  | Maximum base64-encoded payload size in bytes. |\n| `images.resize.jpegQuality` | integer |  |  |\n| `images.maxPerMessage` | integer |  | Maximum images accepted in one provider message. |\n| `images.maxPerRequest` | integer |  | Maximum images accepted across one provider request. |\n\n## Compatibility\n\n`compat` describes verified differences from an API's standard request and response behavior. Model values override provider values per field. The fields that apply depend on the model's `api`.\n\n### `openai-completions`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStore` | boolean |  | Whether the provider supports the store field. Default: auto-detected from URL. |\n| `supportsDeveloperRole` | boolean |  | Whether the provider supports the developer role instead of system. Default: auto-detected from URL. |\n| `supportsReasoningEffort` | boolean |  | Whether the provider supports reasoning_effort. Default: auto-detected from URL. |\n| `supportsUsageInStreaming` | boolean | `true` | Whether the provider supports stream_options.include_usage for token usage in streaming responses. |\n| `supportsFinishReason` | boolean | `true` | Whether streamed responses include finish_reason. When false, pi infers stop or toolUse when the stream ends. |\n| `maxTokensField` | `\"max_completion_tokens\" \\| \"max_tokens\"` |  | Which field to use for max tokens. Default: auto-detected from URL. |\n| `requiresToolResultName` | boolean |  | Whether tool results require the name field. Default: auto-detected from URL. |\n| `requiresAssistantAfterToolResult` | boolean |  | Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. |\n| `requiresThinkingAsText` | boolean |  | Whether thinking blocks must be converted to text blocks with &lt;thinking> delimiters. Default: auto-detected from URL. |\n| `requiresReasoningContentOnAssistantMessages` | boolean |  | Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. |\n| `thinkingFormat` | `\"openai\" \\| \"openrouter\" \\| \"deepseek\" \\| \"together\" \\| \"baseten\" \\| \"zai\" \\| \"qwen\" \\| \"chat-template\" \\| \"qwen-chat-template\" \\| \"string-thinking\" \\| \"ant-ling\"` |  | Format for reasoning or thinking parameters. When omitted, Pi auto-detects the format from the provider URL. \"openai\" uses reasoning_effort, \"openrouter\" uses reasoning.effort, \"deepseek\" uses thinking.type plus reasoning_effort when supported, \"together\" uses reasoning.enabled plus reasoning_effort when supported, \"baseten\" uses configurable chat_template_args plus reasoning_effort when supported, \"zai\" uses thinking.type, \"qwen\" uses top-level enable_thinking, \"qwen-chat-template\" uses chat_template_kwargs.enable_thinking and preserve_thinking, \"chat-template\" uses configurable chat_template_kwargs, \"string-thinking\" uses top-level thinking, and \"ant-ling\" uses reasoning.effort only when the mapped effort is non-null. |\n| `chatTemplateKwargs` | object |  | Kwargs sent as chat_template_kwargs when thinkingFormat is \"chat-template\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `chatTemplateArgs` | object |  | Arguments sent as chat_template_args when thinkingFormat is \"baseten\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `openRouterRouting` | object |  | OpenRouter provider routing preferences. Controls which upstream providers OpenRouter routes requests to. Sent as the provider field in the OpenRouter API request body. See https://openrouter.ai/docs/guides/routing/provider-selection. |\n| `vercelGatewayRouting` | object |  | Vercel AI Gateway routing preferences. Controls which upstream providers the gateway routes requests to. See https://vercel.com/docs/ai-gateway/models-and-providers/provider-options. |\n| `zaiToolStream` | boolean | `false` | Whether z.ai supports top-level tool_stream for streaming tool call deltas. |\n| `thinkingTokenBudgetField` | `\"thinking_token_budget\" \\| \"thinking_budget\" \\| \"thinking_budget_tokens\"` |  | Top-level request field used to cap reasoning tokens from thinkingBudgets. Reasoning and the answer share max_tokens on these endpoints. \"thinking_token_budget\" is vLLM, \"thinking_budget\" is Qwen, DashScope, or SGLang, and \"thinking_budget_tokens\" is llama.cpp. Off by default and not set on the generated catalog. |\n| `supportsThinkingTokenBudget` | boolean | `false` | Alias for thinkingTokenBudgetField: \"thinking_token_budget\" (vLLM). Prefer thinkingTokenBudgetField. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether the provider supports OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system or developer messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `supportsMidConvoToolAdditions` | boolean | `false` | Whether system messages can introduce additional tools mid-conversation. Requires supportsMidConvoSystemMessages. The generated catalog enables this for capable models. |\n| `supportsStrictMode` | boolean | `false` | Whether the provider supports the strict field in tool definitions. Generated capable models enable it explicitly. |\n| `cacheControlFormat` | `\"anthropic\"` |  | Cache control convention for prompt caching. Anthropic applies cache_control markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send session-affinity data from options.sessionId. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id, x-client-request-id, and x-session-affinity; openai-nosession sends x-client-request-id and x-session-affinity; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean |  | Whether the provider supports long prompt cache retention (prompt_cache_retention: \"24h\" or Anthropic-style cache_control.ttl: \"1h\", depending on format). Default: auto-detected from provider and URL. |\n| `vllmPriority` | number |  | vLLM scheduler priority sent as the top-level priority request field. Lower values are handled earlier and the server default is 0. Only meaningful with --scheduling-policy priority. Off by default and not set on the generated catalog. |\n\n### `openai-responses`\n\nAlso applies to `azure-openai-responses` and `openai-codex-responses`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsDeveloperRole` | boolean | `true` | Whether the provider supports the developer role instead of system. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts developer or system messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id and x-client-request-id; openai-nosession sends x-client-request-id; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports long prompt cache retention. This uses prompt_cache_options.ttl: \"30m\" on GPT-5.6+ and prompt_cache_retention: \"24h\" on earlier models. |\n| `supportsStrictMode` | boolean |  | Whether the provider supports strict JSON-schema function tools. Defaults are API-specific; generated OpenAI models enable it explicitly. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether to emit OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsAdditionalTools` | boolean | `false` | Whether the model supports message-anchored additional_tools input items. |\n| `supportsToolSearch` | boolean | `false` | Whether the model supports client-executed tool search for transcript-anchored additions. |\n| `supportsExplicitPromptCacheMode` | boolean | `false` | Whether the model accepts prompt_cache_options. Older OpenAI models reject the parameter. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether thinking level changes are sent as configuration_update input items instead of changing reasoning.effort, so a change keeps the prompt cache. Only used by the openai-responses API, and ignored with Sign in with ChatGPT on the openai provider. |\n| `supportsMaxOutputTokens` | boolean | `true` | Whether the provider accepts max_output_tokens. Some Codex-protocol gateways reject it. |\n\n### `anthropic-messages`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsEagerToolInputStreaming` | boolean | `true` | Whether the provider accepts per-tool eager_input_streaming. When false, the Anthropic provider omits tools[].eager_input_streaming and sends the legacy fine-grained-tool-streaming-2025-05-14 beta header for tool-enabled requests. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports Anthropic long cache retention through cache_control.ttl. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send x-session-affinity from options.sessionId when caching is enabled. Required for providers like Fireworks that use session affinity for prompt cache routing; requests to the same replica maximize cache hits. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openrouter\"` |  | Session-affinity format. openrouter sends x-session-id; when unset, sends x-session-affinity. |\n| `supportsCacheControlOnTools` | boolean | `true` | Whether the provider supports Anthropic-style cache_control markers on tool definitions. When false, cache_control is omitted from tool parameters. Some Anthropic-compatible providers, such as Fireworks, do not support this field on tools and may reject or ignore it. |\n| `supportsTemperature` | boolean | `true` | Whether the model accepts the Anthropic temperature request field. Claude Opus 4.7+ rejects non-default values. |\n| `forceAdaptiveThinking` | boolean | `false` | Whether to force adaptive thinking (thinking.type: adaptive plus output_config.effort) regardless of model ID. Built-in models that require adaptive thinking set this in generated metadata. Custom Anthropic-compatible providers can set this to true for any model whose upstream requires the adaptive format. Set false to opt out on overridden built-in models. |\n| `allowEmptySignature` | boolean | `false` | Whether to replay empty thinking signatures instead of converting thinking to text. |\n| `supportsStrictTools` | boolean | `false` | Whether the provider supports Anthropic strict tool schemas. Generated Anthropic models enable it explicitly. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether the exact model transport supports effort-only system messages and thinking binding controls. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system-role messages inside the conversation. When false, later system messages are folded into the top-level system prompt. |\n| `supportsMidConvoToolChanges` | boolean | `false` | Whether the exact model accepts mid-conversation tool_addition and tool_removal blocks. Requires supportsMidConvoSystemMessages. |\n| `allowedFallbackModels` | `object[]` |  | Models Anthropic accepts for server-side refusal fallback, with local pricing metadata for returned fallback responses. When absent or empty, callers must omit fallbacks; Anthropic rejects the field for models with no permitted fallback targets. |\n\n### `bedrock-converse-stream`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStrictMode` | boolean | `false` | Whether the model supports Bedrock strict tool schemas. |\n\n### `mistral-conversations`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system messages after the conversation has started. When false, later system messages are folded into the leading system message. |\n\n### `typesafe-system-one`\n\nAlso applies to `cloudflare-workers-ai-system-one`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `imageInput` | `\"assets\" \\| \"images\" \\| \"state-image-urls\"` |  | How the service takes classifier images. \"assets\" sends TypeSafe's assets map (PNG and JPEG) and references the images from the state, as TypeSafe and Jev on Workers AI expect. \"images\" sends Clef's images list of data URLs (PNG, JPEG, and WebP, at most four), as Clef on Workers AI expects. \"state-image-urls\" places image_url parts before the state, as OpenRouter expects; OpenRouter drops assets without an error. Auto-detected from the provider, baseUrl, and model ID. Classifier models read it from the provider-level compat; models and modelOverrides entries apply to chat models only. |\n| `sendImageOptInHeader` | boolean |  | Whether requests with images send the TypeSafe-Danger-Zone-Images: true header, which TypeSafe's own API requires for its experimental image input. Defaults to true for the typesafe-system-one API unless the model is detected as OpenRouter. |\n","packages/coding-agent/docs/models.md":"# Choose a Model\n\nFor a built-in provider, start with `/login`, then choose a model with `/model`. Use custom model configuration only when Pi does not already include the provider or endpoint you need.\n\n## Choose a connection\n\n| What you have | Recommended setup |\n|---|---|\n| A supported subscription | Sign in through `/login` |\n| A provider API key | Store it through `/login` or set its environment variable |\n| A local GGUF model | Connect Pi to the llama.cpp router |\n| An OpenAI-, Anthropic-, or Google-compatible endpoint | Add it to `models.json` |\n| A provider with a custom protocol or authentication flow | Build or install a provider extension |\n\nBrowse the [model catalog](https://pi.dev/models) for current providers, model IDs, capabilities, context limits, and pricing. Pi starts with its bundled catalog and can overlay newer catalog data from pi.dev. Cached catalog data remains available offline; run `pi update --models` to force a refresh.\n\n## Authenticate\n\nRun `/login` and select a provider. Pi stores credentials in [`auth.json`](configuration.md#agent-directory). Run `/logout` to remove stored credentials for a provider.\n\nYou can instead provide an API key through the provider's environment variable. This is useful in CI and other environments where Pi should not write credentials. [Providers](providers.md) lists the variables and provider-specific setup.\n\nWhen several credential sources are configured, Pi uses a runtime `--api-key` first, then a stored `auth.json` credential, an `apiKey` from `models.json`, and finally the provider's environment variables or ambient cloud credentials. Provider extensions can define their own authentication behavior.\n\nKeep `auth.json` and any credential commands private. Project settings and extensions can execute inside the Pi process after you trust a project. Review [Security](security.md) before loading configuration from an untrusted directory.\n\n## Select a model\n\nRun `/model` to search available models. The picker shows models whose providers have usable authentication. Press `Ctrl+S` on a model to save it as the default for new sessions.\n\nRun `/thinking` to select the thinking level for the current model. Press `Ctrl+S` there to save the startup level. Pi limits the choices to levels supported by the selected model.\n\n`Ctrl+P` cycles through available models. Use `/scoped-models` to control that cycle and save the selection, or configure model patterns through [Settings](settings.md#model-cycling).\n\nA session records model and thinking-level changes. Resuming the session restores them without changing defaults for new sessions.\n\n## Connect local models\n\nPi integrates directly with the llama.cpp router. The router discovers GGUF files and loads models on demand. Pi's `/llama` command manages the router, while `/model` selects one of its loaded models.\n\nFollow [Local Models with llama.cpp](llama-cpp.md) for server startup, model layout, downloads, and connection troubleshooting.\n\nFor Ollama, LM Studio, vLLM, SGLang, and other compatible servers, [configure a compatible endpoint](#configure-a-compatible-endpoint) in `models.json`.\n\n## Configure a compatible endpoint\n\nUse [`models.json`](configuration.md#agent-directory) when an endpoint speaks an API Pi already supports. This includes most Ollama, LM Studio, vLLM, SGLang, and proxy deployments.\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/models.schema.json\",\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\nThe dummy key makes the model available to Pi; Ollama ignores it. For an authenticated endpoint, `apiKey` and header values can use `$NAME` or `${NAME}` environment interpolation, a literal value, or a leading `!command`. `$$` and `$!` produce a literal `$` and `!`. Commands in `models.json` run at request time and are not cached by Pi.\n\nOpening `/model` reloads the file. A `models` entry adds or replaces a model with the same ID on that provider. Use `modelOverrides` to change metadata for an existing built-in or extension-provided model without replacing the provider's model list. Unknown override IDs are ignored.\n\nTo route a built-in provider through a proxy, set only its `baseUrl`. Its models and authentication stay unchanged:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": { \"baseUrl\": \"https://proxy.example.com/v1\" }\n  }\n}\n```\n\n[Models Configuration](models-configuration.md) lists every field.\n\n### Describe model input and caching\n\nUse `inputLimits.images.resize` to control how Pi encodes new image attachments, `read` results, and tool-result images before storing them in conversation history:\n\n```json\n{\n  \"id\": \"vision-model\",\n  \"input\": [\"text\", \"image\"],\n  \"inputLimits\": {\n    \"images\": {\n      \"resize\": {\n        \"maxWidth\": 1568,\n        \"maxHeight\": 1568,\n        \"maxBytes\": 524288,\n        \"jpegQuality\": 75\n      }\n    }\n  }\n}\n```\n\n`maxBytes` limits the base64-encoded payload. Omitted resize fields use conservative defaults of 2000 by 2000 pixels, 4.5 MiB encoded, and JPEG quality 80. Images are encoded once; changing models does not rewrite historical images. The catalog can also describe hard request limits with `inputLimits.maxRequestBytes`, `images.maxPerMessage`, and `images.maxPerRequest`, but Pi does not yet rewrite or reject history based on them.\n\n<a id=\"prompt-cache-lifetimes\"></a>\n\nUse `promptCache` to declare the provider's best-effort cache lifetime in seconds for the `short` or `long` retention tier:\n\n```json\n{ \"id\": \"claude-sonnet-5\", \"promptCache\": { \"short\": 300, \"long\": 3600 } }\n```\n\nChoose the conservative end of any published range. A model without a lifetime for the active tier is not eligible for cache warming. A `modelOverrides` entry can set `inputLimits` or `promptCache` for a built-in or extension model, including a model accessed through a validated proxy. See [`cacheWarming`](settings.md#model-and-thinking).\n\n### Configure sampling by thinking level\n\nOpenAI-compatible APIs support free-form `samplingParams` model defaults and `samplingParamsByThinkingLevel` overrides. The latter uses Pi thinking-level keys (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`), not provider values from `thinkingLevelMap`:\n\n```json\n{\n  \"id\": \"qwen-thinking-model\",\n  \"reasoning\": true,\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95\n  },\n  \"samplingParamsByThinkingLevel\": {\n    \"off\": {\n      \"temperature\": 0.7,\n      \"top_p\": 0.8\n    },\n    \"high\": {\n      \"top_k\": 20\n    }\n  }\n}\n```\n\nPi first clamps unsupported thinking levels, then merges model `samplingParams`, the effective level's override, and request-level `samplingParams` in that order. Later values win per key. Missing levels inherit the model defaults. `modelOverrides` merges per-level entries per key with the base model. These fields apply only to `openai-completions`, `openai-responses`, and `azure-openai-responses`; other APIs ignore them.\n\nCompatibility settings should describe verified differences in the endpoint's request or response behavior. Do not enable them based only on an endpoint advertising OpenAI or Anthropic compatibility.\n\n## Use classifier models\n\nClassifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef, Clef Flash, and Clef Omni models from Workers AI and OpenRouter, and OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions):\n\n| Provider | Model IDs | Authentication |\n|---|---|---|\n| `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |\n| `openrouter` | `typesafe/jev-1.13`, `typesafe/jev-1.14`, `~typesafe/jev-latest`, `cloudflare/clef`, `cloudflare/clef-flash`, `cloudflare/clef-omni` | `OPENROUTER_API_KEY` or `/login` |\n| `cloudflare-workers-ai` | `typesafe/jev`, `typesafe/jev-1.14.0`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`, `@cf/cloudflare/clef-omni` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |\n| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |\n| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |\n| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |\n\nChat models on a [llama.cpp router](llama-cpp.md#classification) are also listed as classifier models.\n\nOpenAI's Decisions API needs an API key. Sign in with ChatGPT credentials do not work with it, so `gpt-6-luna` is not listed as available while `openai` is logged in through `/login`, even when `OPENAI_API_KEY` is set; log out of `openai` to use the key. The API rejects inputs above 922K tokens, but requests that run longer than about five seconds, currently above roughly 600K input tokens, fail with a gateway timeout.\n\nClassifier models do not appear in `/model`. The model reaches them through the [`codemode`](cli.md#enable-codemode) tool, which is off unless an MCP server turned it on. Enable it with `\"defaultTools\": [\"+codemode\"]` in [settings](settings.md#tools). Scripts then list classifier models with `models.getAvailableOfType(\"classifier\")` and call `models.classify(model, { state, questions })`:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst result = await models.classify(jev, {\n  state: { message: \"The change works, thanks.\" },\n  questions: {\n    approved: {\n      type: \"bool\",\n      instructions: \"Does the user approve of the result?\",\n      criteria: { true: \"Approval\", false: \"No approval\" },\n    },\n  },\n});\nreturn result.answers;\n```\n\n[Codemode](codemode.md#classify) describes the question and answer types.\n\nClassifier models whose `input` includes `\"image\"` also judge images passed in `images`; other classifier models return an error for them. Image input works with GPT-6 Luna, with Jev from `typesafe` and `cloudflare-workers-ai`, with Clef from `cloudflare-workers-ai`, and with the OpenRouter models whose catalog lists image input. Jev takes PNG and JPEG images, and image input is experimental on TypeSafe's own API. Clef takes PNG, JPEG, and WebP images, at most four per request. Jev from `vercel-ai-gateway` and `opencode` takes text only.\n\nEach System One service takes images in its own request format, set by the model's `compat.imageInput`. Built-in models set it, so overriding a provider's `baseUrl` keeps it. A custom provider that proxies another service's classifiers sets it in its provider-level `compat` in `models.json` (see [System One compatibility](models-configuration.md#typesafe-system-one)). OpenRouter in particular answers requests in TypeSafe's format without an error but drops the images.\n\nWhen the service reports token counts, as all System One services do, `result.usage` carries them with their cost. Pi adds the usage of a script's classifier calls to the `codemode` tool result, so it counts toward the session cost in the footer and `/session`. The cost uses the model's catalog price; models without one, such as TypeSafe's direct `jev-latest`, report tokens at no cost.\n\nExtensions call classifiers through `ctx.modelRegistry.classify()`, without codemode. [Virtual models](virtual-models.md#route-requests) can use them to route requests; see the `jev-router.ts` example.\n\n## Use image models\n\nImage models generate images from a prompt and optional input images. Pi lists OpenRouter's image models, such as `google/gemini-2.5-flash-image` and `black-forest-labs/flux.2-pro`, under the `openrouter` provider; they use the same `OPENROUTER_API_KEY` or `/login` credential as its chat models.\n\nLike classifier models, image models do not appear in `/model`; the model reaches them through the [`codemode`](cli.md#enable-codemode) tool. Scripts list them with `models.getAvailableOfType(\"image\")` and call `models.generateImages(model, { input })`. The result's `output` holds base64 image blocks, which `image()` attaches to the `codemode` result so the model sees them:\n\n```js\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) if (block.type === \"image\") image(block);\n```\n\n`input` can also contain `{ type: \"image\", data, mimeType }` blocks to edit or use as references. Pi adds the usage of a script's image calls to the `codemode` tool result, like classifier calls. Generated images are not saved to disk. [Codemode](codemode.md#generate-images) describes the full API.\n\nExtensions generate images through `ctx.modelRegistry.generateImages()`, without codemode.\n\n## Add a custom provider\n\nUse an extension when the provider needs custom streaming, model discovery, or authentication behavior. See [Custom Providers](custom-provider.md) for the extension workflow.\n\n## Troubleshooting\n\n### A model does not appear\n\nConfirm that its provider has usable authentication. Custom models can load from `models.json` but remain unavailable in `/model` until Pi can resolve credentials. For llama.cpp, only models currently loaded by the router appear.\n\n### Authentication works in one shell only\n\nCheck whether the key came from an environment variable rather than `auth.json`. Environment variables must be present in the process that starts Pi.\n\n### Sign-in opens a browser on a remote machine\n\nComplete the provider's headless authentication flow when available. Some providers let you paste the final redirect URL or authorization code back into Pi. See [Authenticate interactively](providers.md#authenticate-interactively).\n\n### A compatible endpoint rejects requests\n\nCheck its API type and compatibility settings in `models.json`. The upstream server must support the corresponding request fields and behavior.\n","packages/coding-agent/docs/nvidia-rtx-llama-cpp.md":"# Run Pi with NVIDIA RTX and llama.cpp\n\n[NVIDIA Local AI Inference Recipes](https://nvidia.github.io/local-ai-inference-recipes/index.html) provides llama.cpp settings tuned for specific NVIDIA hardware and models. Each recipe gives a single-model `llama-server` command. Pi works with the llama.cpp router, which can serve and manage several models, so this guide converts the recipe command into a router preset.\n\nThe examples below use Windows PowerShell. See [Local Models with llama.cpp](llama-cpp.md) for Pi's complete router and model-management reference.\n\n## Before you begin\n\n1. Open the [recipe catalog](https://nvidia.github.io/local-ai-inference-recipes/index.html), select your hardware, operating system, and model, then choose **llama.cpp**. Check the prerequisites and memory requirements. Keep the recipe's launch command for the conversion below.\n2. Install llama.cpp with the recipe's setup instructions. The recipe's settings depend on its build, for example CUDA support or speculative decoding options.\n3. Run `llama-server --help` and check that it lists `--models-preset`. Without it, the build cannot load the recipe as a router preset. Use a newer build that still meets the recipe's requirements.\n4. Install Pi with the [Quickstart](quickstart.md). On native Windows, also see [Run Pi on Windows](windows.md).\n\n## Convert the recipe to a preset\n\nA preset lists the settings for one or more models. Each section name becomes the model ID shown in Pi. Save the preset next to `llama-server`, or pass its full path when starting the router.\n\nConvert each option from the recipe command into one preset line:\n\n- Remove the leading dashes: `--ctx-size 65536` becomes `ctx-size = 65536`.\n- Write options without a value as `true`: `--jinja` becomes `jinja = true`.\n- Remove quotes and PowerShell line-continuation backticks around values.\n- Leave out `--host` and `--port`. They belong on the router command line.\n- Add `load-on-startup = true` so the router loads the model when it starts.\n- Replace short model-file options with their long preset keys:\n\n| Recipe option | Preset key |\n|---|---|\n| `-hf` | `hf-repo` |\n| `-hff` | `hf-file` |\n| `-hfrd` | `hf-repo-draft` |\n| `--spec-draft-model` | `spec-draft-model` |\n\nFor example, this recipe command:\n\n```powershell\nllama-server.exe -hf \"unsloth/Qwen3.8-27B-GGUF\" `\n  -hff \"Qwen3.8-27B-UD-Q4_K_M.gguf\" `\n  -hfrd \"z-lab/Qwen3.8-27B-DFlash2-GGUF\" `\n  --spec-draft-model \"Qwen3.8-27B-DFlash2-Q4_K_M.gguf\" `\n  --no-mmproj `\n  --host 127.0.0.1 `\n  --port 9931 `\n  --batch-size 4096 `\n  --ubatch-size 1024 `\n  --ctx-size 65536 `\n  --parallel 1 `\n  -ngl 999 `\n  --jinja `\n  --chat-template-kwargs '{\"preserve_thinking\":true}' `\n  --spec-type draft-dflash `\n  --spec-draft-n-max 7 `\n  --spec-draft-backend-sampling\n```\n\nbecomes `qwen3.8-27b.ini`:\n\n```ini\nversion = 1\n\n[qwen3.8-27b]\nload-on-startup = true\nhf-repo = unsloth/Qwen3.8-27B-GGUF\nhf-file = Qwen3.8-27B-UD-Q4_K_M.gguf\nhf-repo-draft = z-lab/Qwen3.8-27B-DFlash2-GGUF\nspec-draft-model = Qwen3.8-27B-DFlash2-Q4_K_M.gguf\nno-mmproj = true\nbatch-size = 4096\nubatch-size = 1024\nctx-size = 65536\nparallel = 1\nngl = 999\njinja = true\nchat-template-kwargs = {\"preserve_thinking\":true}\nspec-type = draft-dflash\nspec-draft-n-max = 7\nspec-draft-backend-sampling = true\n```\n\n## Start the router\n\nOpen PowerShell in the llama.cpp folder and run:\n\n```powershell\n.\\llama-server.exe --models-preset qwen3.8-27b.ini --host 127.0.0.1 --port 8080\n```\n\nIf the recipe requires a Hugging Face token, set `$env:HF_TOKEN` in the same PowerShell window before starting the router. On its first start, the router downloads the model files.\n\nUse the [router health checks](llama-cpp.md#troubleshooting) to confirm that the server is ready. In Windows PowerShell, use `curl.exe` rather than `curl`, which PowerShell maps to a different command. The model is ready when `/models` lists `qwen3.8-27b` with the status `loaded`.\n\n## Use the model in Pi\n\nFollow [Configure Pi](llama-cpp.md#configure-pi) and connect to `http://127.0.0.1:8080`. The preset section name is the model ID, so select `qwen3.8-27b` in `/model` for this example.\n\nNVIDIA recipe memory settings assume that only the selected model uses the GPU. If `/llama` shows other loaded models, unload them before using the recipe model.\n","packages/coding-agent/docs/packages.md":"# Pi Packages\n\nPi packages install and distribute extensions, skills, prompt templates, and themes as one unit. Use a package when a customization should be shared through npm or git, or when several resources belong together.\n\nA package is an ordinary directory or npm package. It can expose conventional resource directories, declare explicit paths under the `pi` key in `package.json`, and carry its own runtime dependencies.\n\n## Install and manage packages\n\nInstall from npm, git, or a local path:\n\n```bash\npi install npm:@example/pi-tools@1.0.0\npi install git:github.com/example/pi-tools@v1\npi install ./local-package\n```\n\n`pi list` shows configured packages. Use `pi remove <source>` to remove one and `pi update --extensions` to reconcile package installations. See [Command Line](cli.md#package-commands) for every package command and option.\n\nPersonal installs are written to `~/.pi/agent/settings.json`. Add `--local` or `-l` to write the package declaration to `.pi/settings.json`. Pi reads declarations from that file only after project trust is granted.\n\nProject packages are installed and loaded only after project trust is resolved. Packages can execute extension code and can include skills that instruct the model to run programs. Review third-party package source before installing it. Review project package declarations before granting project trust.\n\nUse `--extension` or `-e` to try a package for one invocation without adding it to settings:\n\n```bash\npi -e npm:@example/pi-tools\n```\n\n## Choose a source\n\n| Source | Example | Behavior |\n|---|---|---|\n| npm | `npm:@example/pi-tools@1.0.0` | Installed under the Pi npm directory |\n| git | `git:github.com/example/pi-tools@v1` | Cloned and reconciled to the selected ref |\n| URL | `https://github.com/example/pi-tools` | Treated as a git source |\n| Local | `./pi-tools` | Loaded from the resolved path without copying |\n\nVersioned npm specifications are pinned. Git tags and commits are also pinned; package updates reconcile the checkout but do not move a configured ref.\n\nRelative local paths resolve from the settings file that contains them. A file path loads one extension. A directory follows normal package discovery rules.\n\n## Create a package\n\nThe simplest package uses conventional directories:\n\n```text\nmy-pi-package/\n├── package.json\n├── extensions/\n├── skills/\n├── prompts/\n└── themes/\n```\n\nWithout a `pi` manifest, Pi discovers TypeScript and JavaScript extensions, skill directories, Markdown prompts, and JSON themes from those directories.\n\nUse an explicit manifest when resources live elsewhere or need filtering:\n\n```json\n{\n  \"name\": \"my-pi-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./src/extension.ts\"],\n    \"skills\": [\"./resources/skills\"],\n    \"prompts\": [\"./resources/prompts/*.md\"],\n    \"themes\": [\"./resources/themes/*.json\"]\n  }\n}\n```\n\nPaths are relative to the package root. Arrays accept glob patterns and exclusions. List dot-prefixed or symlinked resource roots directly when traversal through a glob would not discover them.\n\nThe `pi-package` keyword makes an npm package eligible for discovery in the [Pi package gallery](https://pi.dev/packages). Optional `pi.image` and `pi.video` fields add gallery previews.\n\n## Declare dependencies\n\nPut runtime packages imported by extensions in `dependencies`. Pi installs package dependencies when it installs an npm or git source.\n\nPi supplies these packages to extensions and skills:\n\n- `@earendil-works/pi-ai`\n- `@earendil-works/pi-agent-core`\n- `@earendil-works/pi-coding-agent`\n- `@earendil-works/pi-tui`\n- `typebox`\n\nDeclare the host-provided packages listed above in `peerDependencies` with a `\"*\"` range and do not bundle them. Pi suppresses automatic peer installation for managed npm packages and git packages installed with npm, pnpm, or Bun. Local packages are not installed or modified, so their dependency tree remains the package author's responsibility.\n\nDo not list host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping in compiled ESM and create duplicate classes, registries, and initialization work. Pi reports an extension warning when it detects this manifest configuration. Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths.\n\nInstalled packages load with separate module roots. Do not rely on two packages sharing one dependency instance or one package resolving another package’s undeclared dependency.\n\n## Select package resources\n\nThe object form in settings narrows which resources load from a package:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"npm:@example/pi-tools\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"]\n    }\n  ]\n}\n```\n\nFor each resource type:\n\n- Omit the property to load everything allowed by the package.\n- Use `[]` to load none of that type.\n- Use `!pattern` to exclude glob matches.\n- Use `+path` to include one exact allowed path.\n- Use `-path` to exclude one exact path.\n\nFilters narrow the package manifest. They do not expose resources that the package itself did not declare.\n\nRun `pi config` to enable or disable discovered resources and pi's built-in extensions. It starts with personal configuration; press Tab to switch scope, or run `pi config --local` to start with project overrides.\n\n## Understand scope and identity\n\nThe same package can appear in personal and project settings. A project entry normally replaces the personal entry. With `autoload: false`, the project entry instead acts as a filtering delta over the personal package.\n\nPi identifies npm packages by package name, git packages by repository URL without the ref, and local packages by resolved absolute path. This prevents the same package from loading twice through equivalent declarations.\n\nUse [Extensions](extensions.md), [Skills](skills.md), [Prompt Templates](prompt-templates.md), and [Themes](themes.md) to design each resource before packaging it.\n","packages/coding-agent/docs/prompt-templates.md":"# Prompt Templates\n\nPrompt templates turn Markdown files into reusable `/` commands. Use one when you want to reuse the same prompt without adding executable behavior or a larger set of supporting instructions.\n\nA template can accept arguments and appear in command completion. Pi can load templates from personal configuration, project configuration, an explicit path, or a Pi package. Project configuration loads only after project trust is granted.\n\n## Create a template\n\nCreate `~/.pi/agent/prompts/review.md`:\n\n```markdown\n---\ndescription: Review staged git changes\nargument-hint: \"[focus]\"\n---\nReview the staged changes. Focus on ${1:-correctness, security, and error handling}.\n```\n\nThe filename becomes the command name, so this template is available as `/review`. The `description` appears in command completion. If it is omitted, Pi uses the first non-empty line.\n\n`argument-hint` is optional. Use `<angle brackets>` for required arguments and `[square brackets]` for optional arguments.\n\nRun `/reload` after adding or changing a template in an active session.\n\n<a id=\"invoke-a-template\"></a>\n\n## Use a template\n\nType the template command in the editor:\n\n```text\n/review\n/review concurrency\n```\n\nPi expands the template before the resulting text enters the agent. Extensions receive the raw input first through the `input` event unless an extension command with the same name handles it.\n\nTemplates support these substitutions:\n\n| Syntax | Result |\n|---|---|\n| `$1`, `$2`, … | One positional argument |\n| `$@` or `$ARGUMENTS` | All arguments joined with spaces |\n| `${1:-default}` | First argument, or a default value |\n| `${@:-default}` | All arguments, or a default value |\n| `${@:N}` | Arguments starting at position `N` |\n| `${@:N:L}` | `L` arguments starting at position `N` |\n\nArguments follow shell-like quoting, so `/review \"API compatibility\"` supplies one argument containing a space.\n\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the template in your user or project prompt directory. Conventional prompt directories load direct `.md` children only.\n\nSettings and packages can select nested Markdown files; a package manifest can narrow discovery with explicit paths and globs. See [Settings](settings.md#resources) and [Pi Packages](packages.md) for these options.\n\nProject templates become commands in the editor after trust is granted. Review their content before trusting an unfamiliar project. See [Security](security.md#understand-project-trust).\n","packages/coding-agent/docs/providers.md":"# Providers\n\nMost hosted providers support one or both of these authentication methods:\n\n- Sign in through a browser or device flow backed by OAuth.\n- Provide an API key.\n\nUse `/login [provider]` to see the methods supported by a provider. Amazon Bedrock and Google Vertex AI can also use ambient cloud credentials.\n\n## Authenticate interactively\n\nRun `/login` and select a provider. Pi guides you through its OAuth or API-key flow and saves the resulting credential in [`auth.json`](configuration.md#agent-directory).\n\nOn a remote or headless machine, an OAuth callback may not reach the local process. When prompted, paste the final redirect URL or authorization code back into Pi.\n\nRun `/logout` and select a provider to remove its stored credential. This does not unset environment variables, remove authentication from `models.json`, or revoke the credential at the provider.\n\n`auth.json` can contain API keys and OAuth tokens. Keep it private and do not commit it.\n\n## Use an API key from the environment\n\nEnvironment variables are useful in CI and anywhere Pi should not store the key. Set the variable before starting Pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nThis table covers providers with a single primary API-key variable. Providers that need additional configuration or support ambient credentials are covered under [Provider Specific Config](#provider-specific-config).\n\n| Provider | Environment variable |\n|---|---|\n| Anthropic | `ANTHROPIC_API_KEY` |\n| Ant Ling | `ANT_LING_API_KEY` |\n| OpenAI | `OPENAI_API_KEY` |\n| DeepSeek | `DEEPSEEK_API_KEY` |\n| NVIDIA NIM | `NVIDIA_API_KEY` |\n| Google Gemini | `GEMINI_API_KEY` |\n| GitHub Copilot | `COPILOT_GITHUB_TOKEN` |\n| Mistral | `MISTRAL_API_KEY` |\n| Groq | `GROQ_API_KEY` |\n| Cerebras | `CEREBRAS_API_KEY` |\n| xAI | `XAI_API_KEY` |\n| OpenRouter | `OPENROUTER_API_KEY` |\n| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |\n| ZAI Coding Plan (Global) | `ZAI_API_KEY` |\n| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` |\n| OpenCode Zen and Go | `OPENCODE_API_KEY` |\n| Radius | `RADIUS_API_KEY` |\n| TypeSafe ([classifier models](models.md#use-classifier-models)) | `TYPESAFE_API_KEY` |\n| Hugging Face | `HF_TOKEN` |\n| Fireworks | `FIREWORKS_API_KEY` |\n| Together AI | `TOGETHER_API_KEY` |\n| Baseten | `BASETEN_API_KEY` |\n| Kimi For Coding | `KIMI_API_KEY` |\n| Meta | `META_API_KEY` |\n| MiniMax | `MINIMAX_API_KEY` |\n| MiniMax (China) | `MINIMAX_CN_API_KEY` |\n| Moonshot AI (Global and China) | `MOONSHOT_API_KEY` |\n| Qwen Token Plan and Individual | `QWEN_TOKEN_PLAN_API_KEY` |\n| Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |\n| Xiaomi MiMo | `XIAOMI_API_KEY` |\n| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |\n| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |\n| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` |\n\nAnthropic also recognizes `ANTHROPIC_OAUTH_TOKEN` as an API credential and `ANTHROPIC_AUTH_TOKEN` as bearer authentication.\n\nWith no key or token set, Anthropic uses workload identity federation when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` and `ANTHROPIC_IDENTITY_TOKEN_FILE` are set: the Anthropic SDK exchanges the identity token for a short-lived access token and refreshes it itself (re-reading the identity token file, so keep that file fresh for long sessions). `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID` are passed through when set.\n\n## Load an API key from a command\n\nTo use a secret manager without writing the resolved key to disk, set a provider's `key` in `auth.json` to a command prefixed with `!`:\n\n```json\n{\n  \"anthropic\": {\n    \"type\": \"api_key\",\n    \"key\": \"!security find-generic-password -ws 'anthropic'\"\n  }\n}\n```\n\nPi runs the command when the key is first needed and caches its standard output for the process lifetime. Empty output, a timeout, or a nonzero exit leaves the key unresolved until Pi restarts.\n\n## Provider Specific Config\n\nThe providers below have additional setup, need additional settings, or can use credentials supplied by their platform.\n\nA stored API-key credential can include an `env` object. Its values take priority over the process environment for that provider:\n\n```json\n{\n  \"cloudflare-workers-ai\": {\n    \"type\": \"api_key\",\n    \"key\": \"...\",\n    \"env\": {\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\"\n    }\n  }\n}\n```\n\n### Radius\n\nRadius is a service crafted for Pi by the builders of Pi, Earendil Works. It provides a customizable AI gateway with organization-level controls and analytics built in, and artifacts for sharing what you create with Pi.\n\nTo get started, run `/login radius` in Pi. This adds Radius as a provider, and its models appear in `/model` like any other provider's.\n\nRadius also has an MCP server, so Pi can manage Radius for you.\n\nRadius is currently in early alpha and evolving quickly. See [radius.earendil.com](https://radius.earendil.com) for more.\n\nRadius authentication uses its gateway catalog and caches refreshed model metadata for later offline startup. A custom Radius gateway configured in `models.json` uses its own catalog rather than inheriting the public `radius.pi.dev` catalog.\n\n### OpenRouter\n\nAuthenticate with `/login openrouter` or `OPENROUTER_API_KEY`.\n\nWith a key, Pi lists only the OpenRouter chat models that key may use, with the key's context size, max output, and prices. These come from OpenRouter's `/models/user` endpoint and reflect guardrails and provider restrictions set in OpenRouter. Image and classifier models are not filtered.\n\nPi refreshes the list in the background at startup in interactive mode, when `/model` opens, after `/login`, and on `pi update --models`. The last list is saved and used offline and in `pi -p` and RPC mode. After switching keys outside `/login`, the previous key's list stays until the next refresh.\n\nRegional endpoints filter models to their region. To use one, set the base URL in [`models.json`](models.md#configure-a-compatible-endpoint):\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": { \"baseUrl\": \"https://us.openrouter.ai/api/v1\" }\n  }\n}\n```\n\nIf the base URL rejects the key at `/models/user` or does not have that endpoint, for example a proxy configured as the OpenRouter base URL, Pi lists every OpenRouter model.\n\n### Azure OpenAI\n\nThe provider ID is `azure` (formerly `azure-openai-responses`). Use it as the key in `auth.json`, `models.json`, and `settings.json`, and in model references such as `--model azure/gpt-5.4`.\n\nThe `azure` provider serves OpenAI models through the Responses API and Microsoft Foundry models through Chat Completions, such as `azure/deepseek-v4-pro`.\n\nSet an API key plus either a base URL or resource name:\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# Or:\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n```\n\nResource root URLs under `ai.azure.com`, `cognitiveservices.azure.com`, and `openai.azure.com` are normalized to the OpenAI API path.\n\nPi sends the model ID as the deployment name. If a deployment has a different name, map it with `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`:\n\n```bash\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-5.4=my-gpt-deployment,deepseek-v4-pro=my-deepseek\n```\n\n`AZURE_OPENAI_API_VERSION` overrides the API version for OpenAI models (default `v1`).\n\nTo use a Foundry model that Pi does not include, add it under `azure` in [`models.json`](models.md#configure-a-compatible-endpoint) with `api: \"openai-completions\"`. Custom models require a `baseUrl`; `AZURE_OPENAI_BASE_URL` and `AZURE_OPENAI_RESOURCE_NAME` take priority over it when set:\n\n```json\n{\n  \"providers\": {\n    \"azure\": {\n      \"baseUrl\": \"https://your-resource.services.ai.azure.com\",\n      \"models\": [\n        { \"id\": \"your-deployment\", \"api\": \"openai-completions\" }\n      ]\n    }\n  }\n}\n```\n\n### Amazon Bedrock\n\nBedrock can use a bearer token or an ambient AWS credential source:\n\n```bash\n# Named profile\nexport AWS_PROFILE=your-profile\n\n# IAM keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n# Required for temporary credentials\nexport AWS_SESSION_TOKEN=...\n\n# Bedrock bearer token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Region, when not supplied by the profile or AWS SDK configuration\nexport AWS_REGION=us-west-2\n# AWS_DEFAULT_REGION is also supported\n```\n\nPi also supports ECS task credentials and IRSA through the standard `AWS_CONTAINER_CREDENTIALS_*` and `AWS_WEB_IDENTITY_TOKEN_FILE` variables.\n\n### Cloudflare AI Gateway\n\nThe gateway requires a token, account ID, and gateway ID:\n\n```bash\nexport CLOUDFLARE_API_KEY=...\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...\n```\n\nThe account and gateway IDs can come from the process environment or the credential's `env` object in `auth.json`.\n\n`CLOUDFLARE_API_KEY` authenticates Pi to the gateway. Upstream access can use Cloudflare unified billing, credentials stored in the gateway, or an `Authorization` header configured for the provider in `models.json`.\n\n### Cloudflare Workers AI\n\nWorkers AI requires a token and account ID:\n\n```bash\nexport CLOUDFLARE_API_KEY=...\nexport CLOUDFLARE_ACCOUNT_ID=...\n```\n\nThe account ID can also be stored in the credential's `env` object.\n\n### Google Vertex AI\n\nUse a Google Cloud API key:\n\n```bash\nexport GOOGLE_CLOUD_API_KEY=...\n```\n\nTo use Application Default Credentials, configure a project and location:\n\n```bash\nexport GOOGLE_CLOUD_PROJECT=your-project\n# GCLOUD_PROJECT is also supported\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nThen authenticate:\n\n```bash\ngcloud auth application-default login\n```\n\nTo use a service-account key file instead, set `GOOGLE_APPLICATION_CREDENTIALS` along with the project and location.\n","packages/coding-agent/docs/quickstart.md":"# Quickstart\n\nPi runs in your terminal and works with files on your machine. To use it, you need access to a model through a supported provider. This can be a subscription, an API key, or a local model.\n\nFor native Windows setup, read [Windows Setup](windows.md). For Android, read [Termux Setup](termux.md).\n\n## 1. Install Pi\n\nOn macOS or Linux, you can use the installer:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nThe installer pins all dependencies and updates Pi with `pi update`. Alternatively, install Pi from npm, which does not pin transitive dependencies. This requires Node.js 22.19 or newer:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\nPi does not require dependency lifecycle scripts for a normal npm installation.\n\nWith Nix on macOS or Linux, install the latest release from Pi's flake. Nix builds Pi from source:\n\n```bash\nnix profile add github:earendil-works/pi/stable\n```\n\nOlder Nix versions use `nix profile install` instead. Update with `nix profile upgrade pi`; `pi update` cannot update a Nix installation. To pin a release, use a tag such as `github:earendil-works/pi/v1.0.0`.\n\nVerify the installation:\n\n```bash\npi --version\n```\n\n## 2. Start Pi\n\nChange to the folder you want Pi to work with, then start it:\n\n```bash\ncd /path/to/folder\npi\n```\n\nThe working folder helps Pi discover relevant files, instructions, and configuration. Pi also uses it to group saved sessions.\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Pi running in a terminal with a conversation, input editor, and status footer\" width=\"750\"></p>\n\nThe interface shows your conversation, an editor for prompts and commands, and a footer with the current folder, model, and session status. See [Use Pi in the terminal](usage.md) to learn how to add files, run commands, direct ongoing work, and manage results.\n\n## 3. Choose a model\n\nA **model** generates Pi's responses. A **provider** is the service or account Pi uses to access that model.\n\nIn Pi, run:\n\n```text\n/login\n```\n\nChoose a provider, then follow the prompts to use a subscription or store an API key. Run `/model` afterward if you want to select a different available model.\n\nSee [Choose a model and provider](models.md) for supported providers, environment-variable authentication, local models, and custom endpoints.\n\n## 4. Give Pi a task\n\nPi shows each file read, search, command, and edit it performs. It does not ask before every tool call.\n\nEnter a task that matches your work, for example:\n\n```text\nSummarize @meeting-notes.md and save the action items to action-items.md.\n```\n\n```text\nExplain how this repository is structured and how to run its checks.\n```\n\n```text\nCompare @previous.csv with @current.csv and summarize the important changes.\n```\n\nType `@` in the editor to search for a file instead of entering its full path. When Pi finishes, review its response and any changed files. Use version control or backups for important work. For untrusted or unattended work, use a container or another sandbox. See [Security](security.md).\n\n## Continue later\n\nPi saves sessions automatically. Exit Pi, then resume the most recent session for the same working folder with:\n\n```bash\npi --continue\n```\n\nUse `/resume` to choose another saved session. See [Continue or branch a session](sessions.md) for session naming, branching, compaction, export, and sharing.\n\n## Next steps\n\n- [Use Pi interactively](usage.md) to learn input, commands, shortcuts, and queued messages.\n- [Add instructions](configuration.md#context-files) that Pi should follow whenever it works in a folder.\n- [Choose a model and provider](models.md).\n\n### Choose how to customize Pi\n\nStart with the least powerful mechanism that meets your need:\n\n| Need | Start with |\n|---|---|\n| Give Pi persistent instructions for a folder | [`AGENTS.md`](configuration.md#context-files) |\n| Reuse a prompt from the `/` menu | [Prompt template](prompt-templates.md) |\n| Add task-specific instructions and supporting files | [Skill](skills.md) |\n| Add executable tools, commands, or event handlers | [Extension](extensions.md) |\n| Build a custom terminal component | [Terminal UI](tui.md) |\n| Connect an unsupported model service | [Custom provider](custom-provider.md) |\n| Install or distribute several resources | [Pi package](packages.md) |\n\n## Uninstall Pi\n\nIf you installed Pi with npm, run:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nIf you used the installer, run it again and choose **Uninstall Pi**:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nIf you installed Pi with Nix, run:\n\n```bash\nnix profile remove pi\n```\n\nNone of these methods removes configuration, credentials, sessions, or installed Pi packages from `~/.pi/agent/`.\n","packages/coding-agent/docs/rpc-commands.md":"# RPC Commands\n\nThis reference lists commands accepted on stdin in [RPC mode](rpc.md). Each command and response is one JSON object. Shared message values use the [message types](message-types.md).\n\n## Prompting\n\n### prompt\n\nSend a user prompt to the agent. The command response is emitted after the prompt is accepted, queued, or handled. Events continue streaming asynchronously after acceptance.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nWith images:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**During streaming**: If the agent is already streaming, you must specify `streamingBehavior` to queue the message:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: Queue the message while the agent is running. It is delivered after the current assistant turn finishes executing its tool calls, before the next LLM call.\n- `\"followUp\"`: Wait until the agent finishes. Message is delivered only when agent stops.\n\nIf the agent is streaming and no `streamingBehavior` is specified, the command returns an error.\n\n**Extension commands**: If the message is an extension command (e.g., `/mycommand`), it executes immediately even during streaming. Extension commands manage their own LLM interaction via `pi.sendMessage()`.\n\n**Input expansion**: Skill commands (`/skill:name`) and prompt templates (`/template`) are expanded before sending/queueing.\n\nResponse:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true, \"data\": {\"disposition\": \"started\"}}\n```\n\n`data.disposition` is `\"handled\"` if an extension command or input handler consumed the prompt, `\"queued\"` if Pi queued it during a run, or `\"started\"` if Pi accepted it to start a run. This describes the submitted prompt, not independent work started by an extension or a guarantee of completion.\n\n`success: true` means the prompt was accepted, queued, or handled immediately. `success: false` means the prompt was rejected before acceptance. Failures after acceptance are reported through the normal event and message stream, not as a second `response` for the same request id.\n\nThe `images` field is optional. Each image uses `ImageContent` format: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n### steer\n\nQueue a steering message while the agent is running. It is delivered after the current assistant turn finishes executing its tool calls, before the next LLM call. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nWith images:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nThe `images` field is optional. Each image uses `ImageContent` format (same as `prompt`).\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true, \"data\": {\"disposition\": \"queued\"}}\n```\n\n`data.disposition` is `\"handled\"` if an input handler consumed this steer, or `\"queued\"` if Pi queued it (including after a handler transformed it). It does not guarantee this message remains queued.\n\nSee [set_steering_mode](#set_steering_mode) for controlling how steering messages are processed.\n\n### follow_up\n\nQueue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nWith images:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nThe `images` field is optional. Each image uses `ImageContent` format (same as `prompt`).\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true, \"data\": {\"disposition\": \"queued\"}}\n```\n\n`data.disposition` has the same `\"handled\"` or `\"queued\"` meaning as for `steer`, applied to this follow-up.\n\nSee [set_follow_up_mode](#set_follow_up_mode) for controlling how follow-up messages are processed.\n\n### abort\n\nAbort the current operation and wait for the session to become idle before responding.\n\n```json\n{\"type\": \"abort\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n### clear_queue\n\nRemove queued steering and follow-up messages and return their text.\n\n```json\n{\"type\": \"clear_queue\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clear_queue\",\n  \"success\": true,\n  \"data\": {\n    \"steering\": [\"Change direction\"],\n    \"followUp\": [\"Summarize when finished\"]\n  }\n}\n```\n\nTo implement interactive Esc behavior, send `clear_queue` before `abort`, then restore the returned text in the client editor. `abort` continues queued messages when they remain in the session.\n\n### new_session\n\nStart a fresh session. Can be canceled by a `session_before_switch` extension event handler.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nWith optional parent session tracking:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nIf an extension canceled:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n## State\n\n### get_state\n\nGet current session state.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\nThe `model` field is a full [Model](#model-object) object, or omitted when no model is selected. The `sessionName` field is the display name set via `set_session_name`, or omitted if not set.\n\n### get_messages\n\nGet all messages in the conversation.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nMessages are `AgentMessage` objects (see [Message Types](message-types.md)).\n\n## Model\n\n### set_model\n\nSwitch to a specific model.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nResponse contains the full [Model](#model-object) object:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n### cycle_model\n\nCycle to the next available model. Returns `null` data if only one model available.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\nThe `model` field is a full [Model](#model-object) object.\n\n### get_available_models\n\nList all configured models.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nResponse contains an array of full [Model](#model-object) objects:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n## Thinking\n\n### set_thinking_level\n\nSet the reasoning/thinking level for models that support it.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nLevels: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` and `\"max\"` are exposed only when supported by the selected model. Some models, including GPT-5.6, expose both.\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n### cycle_thinking_level\n\nCycle through available thinking levels. Returns `null` data if model doesn't support thinking.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n### get_available_thinking_levels\n\nList the thinking levels supported by the current model. Returns `[\"off\"]` for a model without reasoning support.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n## Queue modes\n\n### set_steering_mode\n\nControl how steering messages (from `steer`) are delivered.\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: Deliver all steering messages after the current assistant turn finishes executing its tool calls\n- `\"one-at-a-time\"`: Deliver one steering message per completed assistant turn (default)\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n### set_follow_up_mode\n\nControl how follow-up messages (from `follow_up`) are delivered.\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: Deliver all follow-up messages when agent finishes\n- `\"one-at-a-time\"`: Deliver one follow-up message per agent completion (default)\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n## Compaction\n\n### compact\n\nManually compact conversation context to reduce token usage.\n\n```json\n{\"type\": \"compact\"}\n```\n\nWith custom instructions:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter` is a heuristic estimate over the rebuilt message context immediately after compaction, not a provider-exact token count. `usage` reports the LLM call or calls that generated the summary and may be omitted by custom compaction handlers.\n\n### set_auto_compaction\n\nEnable or disable automatic compaction when context is nearly full.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n## Retry\n\n### set_auto_retry\n\nEnable or disable automatic retry on transient errors (overloaded, rate limit, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n### abort_retry\n\nAbort an in-progress retry (cancel the delay and stop retrying).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n## Bash\n\n### bash\n\nExecute a shell command and add output to conversation context. Output streams as `bash_execution_update` events while the command runs; the response contains the final result.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nSet `excludeFromContext` to `true` when the command output should be stored in the session but omitted from the model context on the next prompt.\n\nInclude an `id` to associate streamed `bash_execution_update` events with this command.\n\nResponse:\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\nIf output was truncated, includes `fullOutputPath`:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**How bash results reach the LLM:**\n\nThe `bash` command executes immediately and returns a `BashResult`. Internally, a `BashExecutionMessage` is created and stored in the agent's message state.\n\nWhen the next `prompt` command is sent, Pi transforms context messages before sending them to the model. Unless `excludeFromContext` is true, the `BashExecutionMessage` becomes a `UserMessage` with this format:\n\n````\nRan `ls -la`\n```\ntotal 48\ndrwxr-xr-x ...\n```\n````\n\nThis means:\n1. Included bash output reaches the model on the **next prompt**, not immediately.\n2. Multiple bash commands can run before a prompt; Pi includes each output that does not set `excludeFromContext`.\n\n### abort_bash\n\nAbort a running bash command.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n## Session\n\n### get_session_stats\n\nGet token usage, cost statistics, and current context window usage.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens` and `cost` include assistant messages, usage reported by tools, and compaction/branch-summary generation across the full session. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.\n\n`contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.\n\n### export_html\n\nExport session to an HTML file.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nWith custom path:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n### switch_session\n\nLoad a different session file. Can be canceled by a `session_before_switch` extension event handler.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nIf an extension canceled the switch:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### fork\n\nCreate a new fork from a previous user message on the active branch. Can be canceled by a `session_before_fork` extension event handler. Returns the text of the message being forked from.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nIf an extension canceled the fork:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n### clone\n\nDuplicate the current active branch into a new session at the current position. Can be canceled by a `session_before_fork` extension event handler.\n\n```json\n{\"type\": \"clone\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nIf an extension canceled the clone:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n### get_fork_messages\n\nGet user messages available for forking.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n### get_entries\n\nGet all session entries in append order (excluding the session header). The session is an append-only tree of entries with stable ids, so an entry id works as a durable cursor: pass the last entry id you have seen as `since` to get only entries strictly after it, even across client restarts. Unlike `get_messages`, this includes pre-compaction history and abandoned branches.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nWith a cursor:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId` is the id of the current leaf entry (`null` for an empty session), so a client can tell in one round trip whether the active branch moved. If `since` does not match any entry id, the response is `success: false`.\n\n### get_tree\n\nGet the session as a tree of entries. Each node is `{entry, children, label?, labelTimestamp?}`. The result is an array because navigation APIs can create multiple roots; orphaned entries with broken parent chains also appear as roots.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n### get_last_assistant_text\n\nGet the text content of the last assistant message.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\nThe `text` value is `null` if no assistant text exists.\n\n### set_session_name\n\nSet a display name for the current session. The name appears in session listings and helps identify sessions.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nThe current session name is available via `get_state` in the `sessionName` field. To set the initial name when starting RPC mode, pass `--name <name>` or `-n <name>` to the `pi --mode rpc` process.\n\n## Discoverable commands\n\n### get_commands\n\nGet available commands (extension commands, prompt templates, and skills). Run one through the `prompt` command by prefixing its name with `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\n        \"name\": \"fix-tests\",\n        \"description\": \"Fix failing tests\",\n        \"source\": \"prompt\",\n        \"sourceInfo\": {\n          \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\",\n          \"source\": \"local\",\n          \"scope\": \"project\",\n          \"origin\": \"top-level\"\n        }\n      }\n    ]\n  }\n}\n```\n\nEach command has:\n- `name`: Command name (use `/name`)\n- `description`: Human-readable description (optional for extension commands)\n- `source`: What kind of command:\n  - `\"extension\"`: Registered via `pi.registerCommand()` in an extension\n  - `\"prompt\"`: Loaded from a prompt template `.md` file\n  - `\"skill\"`: Loaded from a skill directory (name is prefixed with `skill:`)\n- `sourceInfo`: Metadata for the resource that registered the command:\n  - `path`: Absolute path to the resource\n  - `source`: How Pi discovered it, such as `\"local\"`, `\"auto\"`, or `\"cli\"`\n  - `scope`: `\"user\"`, `\"project\"`, or `\"temporary\"`\n  - `origin`: `\"top-level\"` for a directly loaded resource or `\"package\"` for a package resource\n  - `baseDir`: Package base directory, when applicable\n\n**Note**: Built-in TUI commands (`/settings`, `/hotkeys`, etc.) are not included. They are handled only in interactive mode and would not execute if sent via `prompt`.\n\n## Model object\n\nModel commands return the complete configured model definition. Costs are in US dollars per million tokens.\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\nFor model configuration, see [Configure a compatible endpoint](models.md#configure-a-compatible-endpoint). For TypeScript, use the exported `Model` type from `@earendil-works/pi-ai`.\n","packages/coding-agent/docs/rpc-extension-ui.md":"# RPC Extension UI\n\nExtensions can request user interaction through `ctx.ui`. In RPC mode, supported calls become a request/response subprotocol alongside normal [RPC commands](rpc-commands.md) and [session events](json.md).\n\nThere are two categories of extension UI methods:\n\n- **Dialog methods** (`select`, `confirm`, `input`, `editor`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.\n- **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): emit an `extension_ui_request` on stdout but do not expect a response. The client can display the information or ignore it.\n\nIf a dialog method includes a `timeout` field, the agent-side will auto-resolve with a default value when the timeout expires. The client does not need to track timeouts.\n\n## Limitations\n\nSome `ExtensionUIContext` methods are not supported or degraded in RPC mode because they require direct terminal UI access:\n\n- `custom()` returns `undefined`.\n- `onTerminalInput()` returns a no-op unsubscribe function.\n- `setWorkingMessage()`, `setWorkingVisible()`, `setWorkingIndicator()`, `setHiddenThinkingLabel()`, `setFooter()`, `setHeader()`, `addAutocompleteProvider()`, `setEditorComponent()`, and `setToolsExpanded()` are no-ops.\n- `getEditorText()` returns `\"\"` and `getEditorComponent()` returns `undefined`.\n- `getToolsExpanded()` returns `false`.\n- `pasteToEditor()` delegates to `setEditorText()` without terminal paste handling.\n- `getAllThemes()` returns `[]`, and `getTheme()` returns `undefined`.\n- `setTheme()` returns `{ success: false, error: \"Theme switching not supported in RPC mode\" }`.\n\nNote: `ctx.mode` is `\"rpc\"` and `ctx.hasUI` is `true` in RPC mode because the dialog and fire-and-forget methods are functional via the extension UI sub-protocol. Use `ctx.mode === \"tui\"` to guard TUI-specific features like `custom()` that require a real terminal.\n\n## Requests from Pi\n\nAll requests have `type: \"extension_ui_request\"`, a unique `id`, and a `method` field.\n\n### select\n\nPrompt the user to choose from a list. Dialog methods with a `timeout` field include the timeout in milliseconds; the agent auto-resolves with `undefined` if the client doesn't respond in time.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the selected option string) or `cancelled: true`.\n\n### confirm\n\nPrompt the user for yes/no confirmation.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\nExpected response: `extension_ui_response` with `confirmed: true/false` or `cancelled: true`.\n\n### input\n\nPrompt the user for free-form text.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the entered text) or `cancelled: true`.\n\n### editor\n\nOpen a multi-line text editor with optional prefilled content.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the edited text) or `cancelled: true`.\n\n### notify\n\nDisplay a notification. Fire-and-forget, no response expected.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\nThe `notifyType` field is `\"info\"`, `\"warning\"`, or `\"error\"`. Defaults to `\"info\"` if omitted.\n\n### setStatus\n\nSet or clear a status entry in the footer/status bar. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\nSend `statusText: undefined` (or omit it) to clear the status entry for that key.\n\n### setWidget\n\nSet or clear a widget (block of text lines) displayed above or below the editor. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\nSend `widgetLines: undefined` (or omit it) to clear the widget. The `widgetPlacement` field is `\"aboveEditor\"` (default) or `\"belowEditor\"`. Only string arrays are supported in RPC mode; component factories are ignored.\n\n### setTitle\n\nSet the terminal window/tab title. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n### set_editor_text\n\nSet the text in the input editor. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n## Responses to Pi\n\nResponses are sent for dialog methods only (`select`, `confirm`, `input`, `editor`). The `id` must match the request.\n\n### Value response (select, input, editor)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n### Confirmation response (confirm)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n### Cancellation response (any dialog)\n\nDismiss any dialog method. The extension receives `undefined` (for select/input/editor) or `false` (for confirm).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Example\n\nSee the checked [RPC extension UI client](../examples/rpc-extension-ui.ts) and its [demo extension](../examples/extensions/rpc-demo.ts).\n\nThe exported request and response unions are defined in [`rpc-types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-types.ts). See [Extensions](extensions.md#ui-and-modes) for mode-independent extension guidance.\n","packages/coding-agent/docs/rpc.md":"# RPC Mode\n\nRPC mode runs Pi as a long-lived subprocess controlled through JSON records on stdin and stdout. Use it for language-independent integrations, process isolation, IDEs, and custom user interfaces.\n\nFor an in-process Node.js or Bun integration, prefer the [SDK](sdk.md). For a subprocess-based TypeScript integration, prefer the exported `RpcClient`, which starts Pi, correlates responses, exposes typed command methods, and delivers events to listeners.\n\n| Interface | Process boundary | Control model | Best fit |\n|---|---|---|---|\n| [SDK](sdk.md) | In process | Direct TypeScript methods and events | Node.js or Bun hosts that want complete API access |\n| RPC | Child process | JSONL commands, responses, and events | Other languages, isolated processes, IDEs, or custom clients |\n\n## Start RPC mode\n\n```bash\npi --mode rpc --no-session\n```\n\nNormal CLI options still select the working folder, model, tools, resources, and session behavior. Common choices include `--provider`, `--model`, `--name`, `--no-session`, and `--session-dir`. See [Command Line](cli.md) for the complete, version-specific interface; `pi --help` is authoritative for the installed version.\n\nRPC mode rejects `@file` prompt arguments. Send prompts through the [`prompt`](rpc-commands.md#prompt) command instead.\n\n## Protocol records\n\nThe protocol has four record families:\n\n| Direction | Record | Purpose |\n|---|---|---|\n| stdin | Command | Ask Pi to prompt, inspect state, change configuration, or manage the session |\n| stdout | `response` | Report whether one command succeeded and return any command data |\n| stdout | Session event | Stream run, message, tool, queue, compaction, and retry activity |\n| Both | Extension UI record | Forward supported extension interactions between Pi and the client |\n\nSee [RPC Commands](rpc-commands.md), [JSON Event Stream](json.md), and [RPC Extension UI](rpc-extension-ui.md) for the canonical record definitions.\n\n### Correlate commands and responses\n\nEvery command accepts an optional string `id`. A matching response repeats it:\n\n```json\n{\"id\":\"req-1\",\"type\":\"get_state\"}\n{\"id\":\"req-1\",\"type\":\"response\",\"command\":\"get_state\",\"success\":true,\"data\":{\"...\":\"...\"}}\n```\n\nUse unique IDs whenever more than one command can be outstanding. Command handling is asynchronous, so clients should correlate by ID rather than response order.\n\nSession events generally have no command ID because they describe session activity. `bash_execution_update` is the exception: when the originating [`bash`](rpc-commands.md#bash) command has an ID, its output events repeat that ID.\n\nAn `extension_ui_response` uses the ID supplied by its `extension_ui_request`. It does not produce a normal command response.\n\n## Framing\n\nRPC uses strict JSONL framing. Write one complete JSON object per record and terminate it with LF (`\\n`). Read stdout as a byte or UTF-8 stream and split records only on LF. Strip an optional preceding carriage return to accept CRLF input.\n\nDo not use a generic line reader that treats Unicode line or paragraph separators as record boundaries. In particular, Node.js `readline` also splits on `U+2028` and `U+2029`, which are valid inside JSON strings.\n\nRead stdout continuously. Pi honors stdout backpressure, but a client that stops reading can stall the process. Honor stdin backpressure when writing commands. Stdout is reserved for protocol records; diagnostics and application logging go to stderr.\n\n## Run lifecycle\n\nA successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean model work completed:\n\n```json\n{\"id\":\"req-2\",\"type\":\"prompt\",\"message\":\"Review this repository\"}\n{\"id\":\"req-2\",\"type\":\"response\",\"command\":\"prompt\",\"success\":true,\"data\":{\"disposition\":\"started\"}}\n```\n\n`data.disposition` reports what happened to the prompt. If it is `\"handled\"`, no run started for this prompt, so don't wait for `agent_settled`. See [RPC Commands](rpc-commands.md#prompt) for all values.\n\nContinue consuming [events](json.md) after that response. `agent_end` marks the end of one low-level agent run, but retries, overflow recovery, compaction, steering, or follow-up work can still follow. Wait for `agent_settled` when the client needs to know Pi will not continue automatically.\n\nSubscribe before sending a prompt to avoid missing a fast completion. `RpcClient.promptAndWait()` does this internally. If using separate `RpcClient` calls, install the event listener before `prompt()` and call `waitForIdle()` only while a run is active.\n\n## Errors\n\nA failed command returns one response with `success: false`:\n\n```json\n{\"id\":\"req-3\",\"type\":\"response\",\"command\":\"set_model\",\"success\":false,\"error\":\"Model not found: invalid/model\"}\n```\n\nMalformed JSON produces a parse response without a request ID:\n\n```json\n{\"type\":\"response\",\"command\":\"parse\",\"success\":false,\"error\":\"Failed to parse command: Unexpected token...\"}\n```\n\nA success response only covers command handling. Provider failures and aborts after a prompt is accepted appear in the message and event stream.\n\nClients must also handle child-process startup failures, unexpected exits, stderr diagnostics, cancellation, and their own deadlines. Do not parse stderr as protocol data.\n\n## Shutdown\n\nClose the child's stdin to request an orderly shutdown. Pi disposes the active runtime before exiting. Clients should still handle process signals and unexpected exits.\n\nAn extension can also request shutdown through its extension context. Pi completes shutdown after the current command or after the active run emits `agent_settled`.\n\n## Minimal client\n\nThis Python example uses a binary pipe reader, which splits on LF without treating Unicode separators as protocol boundaries:\n\n```python\nimport json\nimport subprocess\n\nprocess = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n)\n\nassert process.stdin is not None\nassert process.stdout is not None\n\ncommand = {\"id\": \"prompt-1\", \"type\": \"prompt\", \"message\": \"Hello\"}\nprocess.stdin.write(json.dumps(command).encode(\"utf-8\") + b\"\\n\")\nprocess.stdin.flush()\n\nwhile line := process.stdout.readline():\n    record = json.loads(line)\n    if record.get(\"type\") == \"message_update\":\n        update = record[\"assistantMessageEvent\"]\n        if update[\"type\"] == \"text_delta\":\n            print(update[\"delta\"], end=\"\", flush=True)\n    elif record.get(\"type\") == \"agent_settled\":\n        print()\n        break\n\nprocess.stdin.close()\nprocess.wait()\n```\n\nFor maintained TypeScript clients, use the checked [RPC client example](../examples/rpc-client.ts). It requires a built Pi CLI because the repository example points to `dist/cli.js`.\n\n## Reference\n\n- [RPC Commands](rpc-commands.md): every stdin command and response\n- [JSON Event Stream](json.md): shared stdout session events and streaming reconstruction\n- [RPC Extension UI](rpc-extension-ui.md): dialogs, notifications, responses, and limitations\n- [Message Types](message-types.md): messages and content blocks used by responses and events\n- [Session File Format](session-format.md): entries returned by session commands\n- [`rpc-types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-types.ts): exported TypeScript protocol definitions\n- [`RpcClient`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-client.ts): subprocess client implementation\n\n## Moved reference anchors\n\nThe detailed references formerly on this page now have dedicated pages. These anchors preserve existing links.\n\n<a id=\"prompt\"></a>\n<a id=\"steer\"></a>\n<a id=\"follow_up\"></a>\n<a id=\"abort\"></a>\n<a id=\"clear_queue\"></a>\n<a id=\"new_session\"></a>\n<a id=\"get_state\"></a>\n<a id=\"get_messages\"></a>\n<a id=\"set_model\"></a>\n<a id=\"cycle_model\"></a>\n<a id=\"get_available_models\"></a>\n<a id=\"set_thinking_level\"></a>\n<a id=\"cycle_thinking_level\"></a>\n<a id=\"get_available_thinking_levels\"></a>\n<a id=\"set_steering_mode\"></a>\n<a id=\"set_follow_up_mode\"></a>\n<a id=\"compact\"></a>\n<a id=\"set_auto_compaction\"></a>\n<a id=\"set_auto_retry\"></a>\n<a id=\"abort_retry\"></a>\n<a id=\"bash\"></a>\n<a id=\"abort_bash\"></a>\n<a id=\"get_session_stats\"></a>\n<a id=\"export_html\"></a>\n<a id=\"switch_session\"></a>\n<a id=\"fork\"></a>\n<a id=\"clone\"></a>\n<a id=\"get_fork_messages\"></a>\n<a id=\"get_entries\"></a>\n<a id=\"get_tree\"></a>\n<a id=\"get_last_assistant_text\"></a>\n<a id=\"set_session_name\"></a>\n<a id=\"get_commands\"></a>\n\nCommand details moved to [RPC Commands](rpc-commands.md).\n\n<a id=\"message_update-streaming\"></a>\n<a id=\"bash_execution_update\"></a>\n<a id=\"compaction_start--compaction_end\"></a>\n<a id=\"summarization_retry_scheduled--summarization_retry_attempt_start--summarization_retry_finished\"></a>\n\nEvent details moved to [JSON Event Stream](json.md).\n\n<a id=\"extension-ui-protocol\"></a>\n\nExtension interaction details moved to [RPC Extension UI](rpc-extension-ui.md).\n","packages/coding-agent/docs/sdk.md":"# SDK\n\n`@earendil-works/pi-coding-agent` embeds Pi in a Node.js or Bun process. It provides direct TypeScript access to the agent, sessions, tools, models, and resources used by the command-line application.\n\nUse the SDK for in-process TypeScript integration. For a language-independent or isolated subprocess, see [CLI Integration](cli-integration.md).\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\nconst { session } = await createAgentSession();\n\ntry {\n  await session.prompt(\"What files are in the current directory?\");\n  console.log(session.getLastAssistantText());\n} finally {\n  session.dispose();\n}\n```\n\nThis uses the working directory, discovered resources, stored settings, and configured credentials. `prompt()` resolves when the run finishes.\n\nThe [complete minimal example](../examples/sdk/01-minimal.ts) also streams text events. All [SDK examples](../examples/sdk/) are typechecked with the repository.\n\n<a id=\"session-management\"></a>\n\n## Session lifecycle\n\n`createAgentSession()` creates an `AgentSession`. The session owns one conversation, its model and tools, queued messages, compaction state, and extension runtime.\n\nRead current state through `session.messages`, `session.model`, `session.thinkingLevel`, `session.systemPrompt`, and `session.getActiveToolNames()`.\n\n`session.systemPrompt` is read-only and returns the current effective system prompt, including changes that have not yet been sent to the model. Tool changes are declared to the model before the next request.\n\n<a id=\"sessionmanager-api\"></a>\n\n### Session storage\n\nSessions are persistent by default. `SessionManager` owns the persisted or in-memory entry tree and tracks its active leaf. Branching changes that leaf without deleting abandoned branches. When Pi reconstructs model context, the manager selects the active branch and applies compaction.\n\n`SessionManager` is authoritative for finalized model context. Restore external history by constructing the session with a manager containing those entries. Assigning `session.agent.state.messages` does not replace persisted context.\n\nUse an in-memory manager when the host does not want session files:\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\nSee the checked [sessions example](../examples/sdk/11-sessions.ts) for creating, opening, continuing, listing, and forking sessions. [Session File Format](session-format.md) defines the persisted JSONL contract, and [Message Types](message-types.md) defines transcript values. For exact methods and signatures, use the exported TypeScript declarations or [`session-manager.ts`](../src/core/session-manager.ts).\n\n`cwd` selects the workspace used for project resource discovery, context files, session grouping, and built-in tool paths. Pass it explicitly when the target differs from `process.cwd()`.\n\n`session.dispose()` aborts active work, invalidates extension contexts, disconnects from the agent, and removes event listeners. Call it when the session is no longer needed.\n\n`AgentSessionRuntime` adds `newSession()`, `switchSession()`, `fork()`, and `importFromJsonl()`. Each operation replaces the active `AgentSession` and recreates services for the target working directory.\n\nAfter a runtime replacement, subscriptions belong to the old `AgentSession` and must be rebound. See the [session runtime example](../examples/sdk/13-session-runtime.ts).\n\n## Prompting\n\n`prompt()` handles extension commands and expands file-based prompt templates before ordinary user messages enter the agent. For an accepted agent run, it resolves after the run finishes, including automatic retries.\n\nA prompt sent while the session is already streaming must specify whether it should steer the current run or follow it. Calling `prompt()` without that choice rejects rather than guessing.\n\nA steering message enters after the current assistant turn and its tool calls. A follow-up enters after the current run finishes its pending work. `steer()` and `followUp()` expose those behaviors directly and return `\"queued\"` if the input was queued (including after an extension transformed it), or `\"handled\"` if an extension consumed it.\n\n`abort()` stops the active operation and waits for the session to become idle. `waitForIdle()` waits without aborting it.\n\n## Subscribing to events\n\nSubscribe before prompting when the host needs streamed output:\n\n```typescript\nconst unsubscribe = session.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\ntry {\n  await session.prompt(\"Explain this repository\");\n} finally {\n  unsubscribe();\n}\n```\n\nSession events report message updates, tool execution, queues, compaction, retries, and run lifecycle changes.\n\n`message_end` contains the authoritative completed message. `agent_end` marks the end of one low-level agent run, but automatic recovery or queued work can still follow.\n\nUse `agent_settled` when the host needs to know that Pi will not continue automatically.\n\n## Configuring a session\n\nWithout overrides, the factory creates a `ModelRuntime`, file-backed `SettingsManager`, persistent `SessionManager`, `DefaultResourceLoader`, and the configured default tools.\n\nEach boundary can be supplied explicitly:\n\n- `modelRuntime`, `model`, `thinkingLevel`, and `scopedModels` control model access and selection.\n- `settingsManager` supplies merged settings or an in-memory configuration.\n- `sessionManager` supplies persistent or in-memory conversation history.\n- `resourceLoader` supplies extensions, skills, prompt templates, themes, and context files.\n- `tools`, `noTools`, `excludeTools`, and `customTools` control the active tool set.\n\nUse `DefaultResourceLoader` when you want standard discovery with selected overrides. Supply a custom `ResourceLoader` when the host owns resource storage and discovery completely.\n\n<a id=\"inlineextension\"></a>\n\nInline extension factories can be supplied through `DefaultResourceLoader`. Give one an `InlineExtension` name only when it needs a stable name in diagnostics and startup output. A named inline extension with `replaceable: true` is left out when another extension registers a tool, command, or flag with a name it registers during loading, instead of both loading with a conflict. The CLI's built-in codemode, tool search, and MCP extensions are replaceable. A named entry with `builtin: true` is not an inline extension: it supplies the code of the `builtin:<name>` extension, which loads like a configured extension file. It loads by default, is listed in `pi config`, and is disabled by `-builtin:<name>` in the `extensions` setting or by `noExtensions`; `additionalExtensionPaths: [\"builtin:<name>\"]` loads it explicitly. It loads after project trust is resolved, so it cannot handle `project_trust`. The CLI's built-in extensions use it.\n\n<a id=\"codemode-mcp\"></a>\n\nThe CLI loads `codemode`, `tool_search`, and MCP as built-in extensions. SDK sessions do not; add `createCodemodeExtension()`, `createToolSearchExtension()`, and `createMcpExtension()` to the `extensionFactories` of `DefaultResourceLoader`. `codemode` and `tool_search` are registered inactive: enable them through the `defaultTools` setting (`[\"+codemode\", \"+tool_search\"]` keeps the other default tools), or let the MCP extension activate them: `codemode` for servers with `codemode` exposure, `tool_search` for servers with `deferred` exposure. The MCP extension connects its servers on `session_start`, so call `session.bindExtensions()`. See [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts).\n\nSee the focused examples for [models](../examples/sdk/02-custom-model.ts), [tools](../examples/sdk/05-tools.ts), [extensions](../examples/sdk/06-extensions.ts), and [full control](../examples/sdk/12-full-control.ts).\n\n## Examples\n\n| Example | Purpose |\n|---|---|\n| [Minimal](../examples/sdk/01-minimal.ts) | Create, prompt, observe, and dispose a session |\n| [Custom model](../examples/sdk/02-custom-model.ts) | Select a model and thinking level |\n| [System prompt](../examples/sdk/03-custom-prompt.ts) | Replace or append to the system prompt |\n| [Skills](../examples/sdk/04-skills.ts) | Discover, filter, and add skills |\n| [Tools](../examples/sdk/05-tools.ts) | Select built-in tools and their working directory |\n| [Extensions](../examples/sdk/06-extensions.ts) | Load file-based and inline extensions |\n| [Context files](../examples/sdk/07-context-files.ts) | Add or replace project instructions |\n| [Prompt templates](../examples/sdk/08-prompt-templates.ts) | Add file-style prompt templates |\n| [Credentials](../examples/sdk/09-api-keys-and-oauth.ts) | Configure credential and model storage |\n| [Settings](../examples/sdk/10-settings.ts) | Supply file-backed or in-memory settings |\n| [Sessions](../examples/sdk/11-sessions.ts) | Control session persistence and restoration |\n| [Full control](../examples/sdk/12-full-control.ts) | Replace default discovery and state services |\n| [Session runtime](../examples/sdk/13-session-runtime.ts) | Replace the active session safely |\n| [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts) | Add the `codemode`, `tool_search`, and MCP extensions |\n\n<a id=\"exports\"></a>\n\n## Resources\n\n- [Choose a Model](models.md) covers model selection and compatible endpoints; [Providers](providers.md) covers credentials and provider-specific setup.\n- [Configuration](configuration.md) explains normal discovery and settings; [Settings](settings.md) lists every setting.\n- [Sessions and Context](sessions.md) explains session behavior; [Session Format](session-format.md) defines persisted entries; [Message Types](message-types.md) defines shared transcript values.\n- [Extensions](extensions.md), [Skills](skills.md), and [Prompt Templates](prompt-templates.md) document resources supplied through a `ResourceLoader`.\n- [CLI Integration](cli-integration.md) covers print, JSON, and RPC alternatives to an in-process SDK integration.\n","packages/coding-agent/docs/security.md":"# Run Pi safely\n\nTreat model-generated commands and code as untrusted. Pi can read, change, and execute files with the permissions of the account that started it, and it does not ask for approval before every tool call. Extensions, package installers, language servers, and other child processes run with those same permissions unless an operating-system or virtualization boundary restricts them.\n\nFiles, comments, instructions, command output, and model responses can steer the model through prompt injection. Project trust controls which project resources load at startup, but it does not make that content or the resulting actions safe.\n\nSafety comes from limiting the files, credentials, processes, and network services Pi can access and affect if a generated action is wrong or hostile. Watching the transcript, using project trust, and reviewing changes do not create a security boundary.\n\n## Choose how to run Pi\n\nDifferent ways of running Pi place different limits on what generated commands can access:\n\n| How Pi runs | What remains protected |\n|---|---|\n| Directly, with the permissions of its operating-system user | Anything that user cannot access. A dedicated user account can narrow those permissions, but Pi still shares the operating system and network with other users. |\n| Entirely inside a container, virtual machine, or sandbox | Host files and processes that you do not expose to the environment. Credentials and network services remain accessible if you make them available inside it. This is usually the strongest practical option. |\n| Outside the isolated environment, with only its built-in tools running inside | Host resources are protected from actions performed through those tools. Pi itself and other extensions remain outside the boundary, so this is a narrower form of isolation. |\n\nThe working folder controls resource discovery and the default location for tools, but it does not prevent commands from accessing other paths available to the Pi process.\n\nWhichever option you choose, only provide the files and services required for the task. Keep credentials outside the environment where possible, or use narrowly scoped, short-lived credentials. Restrict network access when commands do not need it.\n\nFor setup instructions and the limitations of each isolation method, see [Run Pi in an isolated environment](containerization.md).\n\n<a id=\"project-trust\"></a>\n\n## Understand project trust\n\nProject trust controls whether Pi loads most settings and resources supplied by a working folder. It prevents a folder from silently loading executable extensions before you approve it.\n\nProject trust is not a complete startup boundary. Pi reads the project `sessionDir` setting while selecting or creating a session, before it resolves project trust. Declining trust prevents the remaining project settings and protected resources from loading, but it cannot undo that initial session-directory lookup.\n\nProject trust does not limit what tool calls can access or affect. After Pi starts, enabled tools still use the operating-system permissions of the Pi process. Instructions and other content in the folder can also influence the model.\n\n### Resources protected by project trust\n\nPi requires a project-trust decision when it finds any of these resources from the current working directory:\n\n- `.pi/settings.json`\n- `.pi/mcp.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts`, or `.pi/themes`\n- `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md`\n- project `.agents/skills` in the current directory or an ancestor directory\n\nA bare `.pi` directory does not require project trust.\n\nGranting project trust allows Pi to load:\n\n- project settings\n- project MCP servers from `.pi/mcp.json`\n- extensions, skills, prompt templates, themes, and system-prompt files under `.pi`\n- missing packages configured through project settings\n- project-local and project-package extensions\n\nDeclining project trust skips those protected resources, except for the initial `sessionDir` lookup described above.\n\nContext files such as `AGENTS.override.md`, `AGENTS.md`, and `CLAUDE.md` load regardless of project trust unless you disable context loading. Treat instructions in a folder as untrusted input even when you decline project trust.\n\n### How Pi chooses a trust decision\n\nA command-line `--approve` or `--no-approve` override applies first. When protected resources exist and there is no command-line override:\n\n1. User-level and command-line extensions can handle the `project_trust` event. The first extension that returns yes or no owns the decision.\n2. If no extension decides, Pi looks for a saved decision for the current directory or one of its parents. The closest decision applies.\n3. If no saved decision applies, Pi follows the global `defaultProjectTrust` setting, whose default is `\"ask\"`.\n\nSaved decisions use canonical directory paths and live in:\n\n```text\n~/.pi/agent/trust.json\n```\n\nUse `/trust` to save a decision for future Pi processes.\n\n### Project trust without an interactive prompt\n\nPrint, JSON, and RPC modes cannot show the built-in trust prompt. If no command-line override, extension, or saved decision applies:\n\n- `defaultProjectTrust: \"always\"` loads protected project resources.\n- `defaultProjectTrust: \"ask\"` or `\"never\"` skips them.\n\nUse `--approve` or `--no-approve` when an automated run needs an explicit one-time decision.\n\n## Reduce impact and improve recovery\n\nThese practices do not replace isolation, but they reduce exposure or make recovery easier:\n\n- Give Pi access only to files and services required for the task.\n- Use snapshots, backups, or version control before substantial changes.\n- Review extensions and packages before loading them. Extensions execute inside the Pi process.\n- Prefer narrowly scoped, short-lived credentials.\n- Review diffs and generated output before applying results to another system.\n- Review sessions before exporting or sharing them. They can contain prompts, tool arguments, command output, file contents, and credentials exposed during the conversation.\n\n## Report a security issue\n\nFollow the repository [Security Policy](https://github.com/earendil-works/pi/blob/main/SECURITY.md). Do not open a public issue for a security-sensitive report.\n\nExpected local-agent behavior, prompt injection from untrusted content, lack of a built-in sandbox, and behavior from user-installed extensions or skills are generally outside the security boundary unless the report demonstrates a privilege-boundary bypass or access that the local user did not already have.\n","packages/coding-agent/docs/session-format.md":"# Session File Format\n\nSessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field. Session entries form a tree structure via `id`/`parentId` fields, enabling in-place branching without creating new files.\n\nFor programmatic creation, persistence, and tree navigation, see the [`SessionManager` API](sdk.md#sessionmanager-api).\n\n\n## File Location\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<session-id>.jsonl\n```\n\nBy default, `<session-id>` is a UUID. Callers can supply a custom ID through the SDK or `--session-id`. For `<path>`, Pi removes the leading path separator and replaces `/`, `\\\\`, and `:` with `-`.\n\n## Deleting Sessions\n\nSessions can be removed by deleting their `.jsonl` files under `~/.pi/agent/sessions/`.\n\nPi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.\n\n## Session Version\n\nSessions have a version field in the header:\n\n- **Version 1**: Linear entry sequence (legacy, auto-migrated on load)\n- **Version 2**: Tree structure with `id`/`parentId` linking\n- **Version 3**: Renamed `hookMessage` role to `custom` (extensions unification)\n\nExisting sessions are automatically migrated to the current version (v3) when loaded.\n\n## Source Files\n\nSource on GitHub ([pi](https://github.com/earendil-works/pi)):\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts) - Session entry types and SessionManager\n- [Message Types](message-types.md) - Shared message and content-block reference\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/messages.ts) - Extended message types\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/ai/src/types.ts) - Base message and content-block types\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts) - Extensible `AgentMessage` union\n\nFor TypeScript definitions in your project, inspect `node_modules/@earendil-works/pi-coding-agent/dist/` and `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Messages\n\nA `message` entry stores an [`AgentMessage`](message-types.md). Message content blocks, roles, usage, and message timestamps are defined in [Message Types](message-types.md).\n\nSession entry timestamps are ISO 8601 strings. The nested message timestamp is a Unix timestamp in milliseconds.\n\n## Entry Base\n\nAll entries (except `SessionHeader`) extend `SessionEntryBase`:\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // Usually an 8-char hex ID; may fall back to a full UUID\n  parentId: string | null;  // Parent entry ID (null for a root entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## Entry Types\n\n### SessionHeader\n\nFirst line of the file. Metadata only, not part of the tree (no `id`/`parentId`).\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\nFor sessions with a parent (created via `/fork`, `/clone`, or `newSession({ parentSession })`):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### SessionMessageEntry\n\nA message in the conversation. The `message` field contains an `AgentMessage`. System messages carry the prompt and tool loadout: the first request of a session persists one with every prompt section and tool declaration, and later changes persist as system messages that patch `sections` by name (`null` removes one) and list `toolsAdded`/`toolsRemoved`. Replaying them in order yields the current prompt and tools; there is no separate prompt state entry.\n\n```json\n{\"type\":\"message\",\"id\":\"a0b1c2d3\",\"parentId\":null,\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"message\":{\"role\":\"system\",\"content\":\"\",\"sections\":{\"preamble\":\"You are an expert coding assistant...\",\"tools\":\"<tools>\\n- read: ...\\n</tools>\",\"cwd\":\"/project\"},\"toolsAdded\":[{\"name\":\"read\",\"description\":\"...\",\"parameters\":{}}],\"timestamp\":1733234400000}}\n{\"type\":\"message\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:04:00.000Z\",\"message\":{\"role\":\"system\",\"content\":\"\",\"sections\":{\"skills\":\"<skills>...</skills>\"},\"toolsRemoved\":[{\"name\":\"write\"}],\"timestamp\":1733234640000}}\n```\n\nSessions created before system messages existed have no leading system message; the first request declares the current prompt as a later system message, which replays the same way.\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\",\"timestamp\":1733234401000}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"api\":\"anthropic-messages\",\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\",\"timestamp\":1733234402000}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false,\"timestamp\":1733234403000}}\n```\n\nAssistant messages name the model that produced them. Newer messages also record `thinkingLevel`, the Pi thinking level requested for that response.\n\n### ModelChangeEntry\n\nEmitted when the user switches models mid-session. The latest entry is the selected model, which may be a [virtual model](virtual-models.md); assistant messages then name the physical model that answered.\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### ThinkingLevelChangeEntry\n\nEmitted when the user changes the thinking/reasoning level.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### UsageEntry\n\nRecords model-attributed usage that is not an assistant message and does not participate in LLM context. `kind` is an arbitrary string identifying the operation; for example, cache warming uses `\"cache_warm\"`.\n\n```json\n{\"type\":\"usage\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:08:00.000Z\",\"kind\":\"cache_warm\",\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{\"input\":0,\"output\":0,\"cacheRead\":50000,\"cacheWrite\":0,\"totalTokens\":50000,\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0.015,\"cacheWrite\":0,\"total\":0.015}}}\n```\n\nUsage entries contribute to session token and cost totals. Pi hides them from the conversation tree. Consumers should treat unknown `kind` values as normal usage rather than rejecting them.\n\n### CompactionEntry\n\nCreated when context is compacted. Stores a summary of earlier messages and a complete system prompt/tool checkpoint.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000,\"systemMessage\":{\"role\":\"system\",\"content\":\"You are a coding assistant.\",\"toolsAdded\":[],\"timestamp\":1733235000000}}\n```\n\n`firstKeptEntryId` is required. It identifies the first entry retained from before the compaction entry. When rebuilding context, Pi replaces older summarized entries with the compaction summary and keeps the range beginning at this entry. A retain-none compaction stores its own ID in this field, so no preceding entries are retained.\n\nOptional fields:\n- `systemMessage`: The replayed prompt sections and tool declarations at the compaction boundary; it becomes the leading system message of the compacted context, and system messages among the kept entries are dropped in its favor. It is absent on older session entries.\n- `usage`: LLM usage from generating the summary; included in session token and cost totals\n- `details`: Implementation-specific data (e.g., `{ readFiles: string[], modifiedFiles: string[] }` for default, or custom data for extensions)\n- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)\n\n### ContextEditEntry\n\nAppend-only edit of one earlier context-producing entry. It changes only future model context; the target entry and its metadata remain unchanged in raw history, UI, exports, and session accounting.\n\n```json\n{\"type\":\"context_edit\",\"id\":\"g6h7i8j9\",\"parentId\":\"f6g7h8i9\",\"timestamp\":\"2024-12-03T14:11:00.000Z\",\"targetId\":\"c3d4e5f6\",\"replacement\":null}\n```\n\nTargets may be user, assistant, tool-result, or custom-message entries. `replacement: null` omits the target from model context. A non-null `replacement` replaces only the target message content. String replacements for assistant and tool-result entries are normalized to one text block because those roles require content arrays. If several edits target the same entry, the latest edit on the active branch wins. Edits are branch-relative: navigating to a point before the edit reveals the target's original contribution again.\n\n### BranchSummaryEntry\n\nCreated when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\n`parentId` is the entry from which the new branch continues. `fromId` is the previous leaf whose abandoned path was summarized.\n\nOptional fields:\n- `usage`: LLM usage from generating the summary; included in session token and cost totals\n- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default, or custom data for extensions\n- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)\n\n### CustomEntry\n\nExtension state persistence. Does NOT participate in LLM context.\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\nUse `customType` to identify your extension's entries on reload. Interactive mode can render custom entries via `pi.registerEntryRenderer(customType, renderer)`, but they still do not participate in LLM context.\n\nPi stores [virtual model](virtual-models.md) router state as custom entries with `customType` `pi.virtual-model-state` and `data` `{ provider, modelId, state }`.\n\n### CustomMessageEntry\n\nExtension-injected messages that DO participate in LLM context.\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\nFields:\n- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)\n- `display`: `true` = show in TUI with distinct styling, `false` = hidden\n- `details`: Optional extension-specific metadata (not sent to LLM)\n\n### LabelEntry\n\nUser-defined bookmark/marker on an entry.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\nSet `label` to `undefined` to clear a label.\n\n### SessionInfoEntry\n\nSession metadata (e.g., user-defined display name). Set via `/name`, `--name` / `-n`, or `pi.setSessionName()` in extensions.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nThe session name is displayed in the session selector (`/resume`) instead of the first message when set.\n\n## Tree Structure\n\nEntries normally form one tree, but navigation APIs can create multiple roots:\n- A root entry has `parentId: null`; the first entry is initially the root\n- Each non-root entry points to its parent via `parentId`\n- Branching creates new children from an earlier entry\n- The \"leaf\" is the current position in the tree\n- Calling `resetLeaf()` or `branchWithSummary(null, ...)` allows a later entry to become another root\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Context Building\n\n`buildContextEntries()` walks from the current leaf to the root, producing the active entry list while honoring compaction:\n\n1. Collects all entries on the path\n2. If one or more `CompactionEntry` values are on the path, uses the latest one:\n   - Includes the compaction entry first\n   - Includes non-system entries from `firstKeptEntryId` up to, but not including, the compaction entry\n   - Includes entries after the compaction entry\n3. Preserves non-message entries in the selected range so interactive mode can render them\n\n`buildSessionProjection()` then applies the latest `context_edit` for each selected target. It returns the model-visible messages together with their source entries. Omitted targets produce no message; replacements retain the source entry's role and metadata while changing only content. The raw selected entries are not modified.\n\n`buildSessionContext()` builds on that projection to produce the message list for the LLM:\n\n1. Extracts current model and thinking level settings from the full path\n2. Converts selected entries to messages:\n   - `message` -> stored `AgentMessage`\n   - `compaction` -> complete system checkpoint followed by `compactionSummary`\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `context_edit` -> no context message of its own\n   - `usage` and `custom` -> no context message\n\nThe compaction summary replaces entries before `firstKeptEntryId`. Pre-compaction system messages are folded into the complete checkpoint rather than replayed from the retained range. Retained non-system entries and all entries after the compaction remain available to the LLM.\n\n## Parsing Example\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"usage\":\n      console.log(`[${entry.id}] Usage (${entry.kind}): ${entry.usage.totalTokens} tokens`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n","packages/coding-agent/docs/sessions.md":"# Sessions and Context\n\nPi saves a conversation as a session. The active branch of that session supplies conversation history for the next model request. Use session commands to continue work, explore another branch, or reduce the amount of history sent to the model.\n\n## Continue or switch sessions\n\nPi saves sessions automatically unless you start it with `--no-session`.\n\n```bash\npi --continue\npi --resume\n```\n\n`--continue` opens the most recent session for the current working directory. `--resume` opens the session picker. In interactive mode, `/resume` opens the same picker and `/new` starts a new session.\n\nUse `/name` or `--name` to assign a recognizable session name. Run `/session` to verify the current session file, ID, message count, token usage, and cost.\n\nThe session picker lets you search, rename, and delete sessions. It can also show paths, change sorting, and limit results to named sessions. See [Keybindings](keybindings.md#sessions) for its shortcuts.\n\n## Choose how to branch\n\nPi stores entries as a tree, so returning to an earlier point does not erase the branch you leave.\n\n| Action | Result | Use it when |\n|---|---|---|\n| `/tree` | Moves within the current session file | Related alternatives should stay together |\n| `/fork` | Creates a new session from an earlier user message | The alternative should become separate work |\n| `/clone` | Copies the active branch into a new session | You want a separate copy of the current state |\n\nIn `/tree`, select a user message to put its text back in the editor. Edit and submit it to create another branch. Selecting an assistant response or another entry continues after that entry with an empty editor.\n\nWhen you leave a branch, Pi can summarize it and attach that summary to the branch you enter. This preserves relevant work from the abandoned path without including every message from it.\n\nFor the persisted tree and entry types, see [Session Format](session-format.md).\n\n## Manage conversation context\n\nThe model receives the active branch, not every branch in the session file. Pi combines that history with the system prompt, discovered context files, available tools, and loaded skill descriptions. [How Pi Works](how-pi-works.md#context) describes how those inputs are assembled.\n\nThe footer shows current context usage. When the active context approaches the model's limit, Pi normally compacts older history automatically. Compaction adds a summary and keeps recent messages. It does not delete the original session entries.\n\nRun `/compact` to compact manually. You can add instructions when the summary should preserve a particular topic or decision. Configure automatic compaction and retained history through [Settings](settings.md#compaction).\n\nCompaction can fail if the provider is unavailable or cannot accept the summarization request. Correct the provider problem and run `/compact` again. Disabling automatic compaction does not disable the manual command.\n\nSee [Compaction Reference](compaction.md) for thresholds, retained boundaries, branch-summary behavior, and extension hooks.\n\n## Control session storage\n\nBy default, Pi stores sessions under `~/.pi/agent/sessions/`, grouped by working directory. Use `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, or the `sessionDir` setting to choose another location. The CLI option has highest precedence.\n\nUse `--no-session` for an ephemeral run. An ephemeral session cannot be resumed after Pi exits.\n\nUse `--session` when you already know the session path or ID. Use `--fork` to create a new session from an existing session before interactive mode starts.\n\n## Export or share a session\n\nUse `/export` to write the current session as HTML or JSONL. Use `/share` to upload it and get a viewer link. By default, Pi uses a Radius artifact when Radius authentication is configured; otherwise, it uses a private GitHub gist. Set [`sessionShareDestination`](settings.md#sessions-and-context) to `gist` or `radius` to force one destination.\n\nReview exported or shared sessions first. They can contain prompts, model responses, tool arguments, command output, file contents, and extension messages.\n\n## Report a bug\n\nRun `/bug [description]` to prepare a private report for the Pi developers. You can include the session transcript, omit it, or ask the current model to summarize the problem. Review any transcript or generated summary because it can contain sensitive conversation data.\n\nThe report includes environment and provider configuration without credential values, plus recorded error diagnostics. Upload it through `radius.pi.dev` or export the same report as a zip to inspect and share yourself. Uploads do not require a login; Radius authentication attributes the report to your account so the developers can follow up. If an upload fails, Pi offers to export the zip.\n","packages/coding-agent/docs/settings.md":"# Settings Reference\n\nThis reference lists user-configurable settings, their types, defaults, and purposes. Project settings override agent-directory settings. Resource lists are combined. See [Configuration](configuration.md) for file locations and trust behavior.\n\nAdd `\"$schema\": \"https://pi.dev/schemas/settings.schema.json\"` to `settings.json` for editor validation and completion.\n\n## Model and thinking\n\n<a id=\"model-cycling\"></a>\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `defaultProvider` | string | Automatic | Startup AI provider. |\n| `defaultModel` | string | Automatic | Startup model ID. |\n| `defaultThinkingLevel` | `\"off\" \\| \"minimal\" \\| \"low\" \\| \"medium\" \\| \"high\" \\| \"xhigh\" \\| \"max\"` | `\"medium\"` | Startup thinking level. |\n| `modelThinkingLevels` | object | None | Per-model startup thinking levels keyed by exact `provider/modelId`. |\n| `thinkingBudgets` | object | Built-in budgets | Token budgets for `minimal`, `low`, `medium`, and `high` thinking levels. |\n| `enabledModels` | `string[]` | All available models | Model patterns used for startup selection and model cycling. Uses the same format as `--models`. |\n| `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in the transcript. |\n| `showCacheMissNotices` | boolean | `false` | Show notices for significant cache misses, successful cache warming, compaction usage, and provider recovery. |\n| `cacheWarming` | `\"off\" \\| \"streaming\" \\| \"idle\"` | `\"streaming\"` | Keep eligible provider prompt caches warm during active runs or, with `\"idle\"`, between runs. Global setting only. |\n\nCache warming runs only when the model declares a cache lifetime and Pi estimates at least $0.05 in avoided cache-miss cost. Refresh usage counts toward session totals but does not enter model context. `/session` shows the next decision; extensions can override it with `cache_warming_decision`. See [Prompt Cache Lifetimes](models.md#prompt-cache-lifetimes).\n\nSee [Choose a Model](models.md) for model selection and thinking controls.\n\n## Interaction\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `steeringMode` | `\"all\" \\| \"one-at-a-time\"` | `\"one-at-a-time\"` | How queued steering messages are delivered. |\n| `followUpMode` | `\"all\" \\| \"one-at-a-time\"` | `\"one-at-a-time\"` | How queued follow-up messages are delivered. |\n| `externalEditor` | string | `$VISUAL`, `$EDITOR`, then platform default | Command opened by the external-editor keybinding. |\n| `doubleEscapeAction` | `\"tree\" \\| \"fork\" \\| \"none\"` | `\"tree\"` | Action for double Escape with an empty editor. |\n| `treeFilterMode` | `\"default\" \\| \"no-tools\" \\| \"user-only\" \\| \"labeled-only\" \\| \"all\"` | `\"default\"` | Initial filter used by `/tree`. |\n| `defaultProjectTrust` | `\"ask\" \\| \"always\" \\| \"never\"` | `\"ask\"` | Fallback project-trust behavior. **Can only be set in agent-directory settings.** |\n\n## Tools\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `defaultTools` | `string[]` | `read`, `bash`, `edit`, `write` | Tools enabled at startup. Plain names replace the defaults; `+name` adds a tool and `-name` removes one. An empty array disables all built-in tools but not extension or SDK tools. |\n| `codemode.mode` | `\"on\"` \\| `\"only\"` | `\"on\"` | How the `codemode` tool presents tools while it is active. `on`: declared tools get a note on calling them from scripts appended to their description, and `codemode` lists only tools that are not declared. `only`: `codemode` lists every tool scripts can call, and active built-in and extension tools are hidden from the model, so it reaches them through `codemode`. |\n| `codemode.inlineBudget` | number | `3000` | Estimated tokens (characters / 4) the `codemode` tool's description may spend on tool declarations. Tools that do not fit are left out and found with `searchTools()`. `0` lists only namespaces. |\n\nAvailable built-in tools are `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`. `defaultTools` can also name `codemode` and `tool_search`, which built-in extensions register inactive, and other extension tools registered inactive.\n\nA list of only `+name` and `-name` entries changes the inherited selection instead of replacing it. For example, this enables `codemode` next to the default tools:\n\n```json\n{\n  \"defaultTools\": [\"+codemode\"]\n}\n```\n\nThis replaces `bash` with `powershell` and enables `grep`: `[\"-bash\", \"+powershell\", \"+grep\"]`. Project settings apply on top of user settings: a project list with only `+name` and `-name` entries changes the user's selection, and a project list with a plain name replaces it. In one list, plain names form the selection, and `+name` and `-name` then apply in order.\n\n`/reload` enables tools newly added to `defaultTools`. It does not disable tools removed from it or re-enable unchanged tools you turned off. `--tools` with plain names, `--no-tools`, and `--no-builtin-tools` override `defaultTools`, also on reload.\n\nCLI tool options override this setting for one invocation. `--tools` with only `+name` and `-name` entries changes the resolved `defaultTools` selection instead, for example `pi --tools +codemode`. On `/reload`, these entries apply to the reloaded setting too, so a tool removed with `-name` stays removed. See [Command Line](cli.md#tools).\n\n## Sessions and context\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `sessionDir` | string | Agent session directory | Session storage directory. Relative paths resolve from the working directory. `PI_CODING_AGENT_SESSION_DIR` and `--session-dir` override this setting. |\n| `sessionShareDestination` | `\"auto\" \\| \"gist\" \\| \"radius\"` | `\"auto\"` | Destination used by `/share`. `auto` uses Radius when authenticated and otherwise creates a GitHub gist. `gist` always uses a gist. `radius` requires Radius authentication and never falls back to a gist. |\n\n### Compaction\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `compaction.enabled` | boolean | `true` | Enable automatic compaction. |\n| `compaction.reserveTokens` | number | `16384` | Free context tokens below which automatic compaction starts. |\n| `compaction.keepRecentTokens` | number | `20000` | Recent tokens retained without summarization. |\n| `compaction.modelOverrides` | object | None | Per-model token settings keyed by exact `provider/modelId`. |\n\n<a id=\"per-model-compaction-overrides\"></a>\n\nCompaction token values must be non-negative safe integers. Each value resolves independently from the matching model override, then the ordinary compaction setting, then the built-in default. Project and user objects merge before model lookup.\n\nSee [Compaction Reference](compaction.md) for trigger, summarization, and validation behavior.\n\n### Branch summaries\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `branchSummary.reserveTokens` | number | `16384` | Tokens reserved when summarizing branch history. |\n| `branchSummary.skipPrompt` | boolean | `false` | Skip the branch-summary prompt and default to no summary. |\n\n## Terminal and display\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `theme` | string | `\"system\"` | Built-in or custom theme name. `system` derives colors from the terminal theme. |\n| `quietStartup` | boolean \\| `\"header\"` | `false` | `true` hides the startup header and loaded-resource listing. `\"header\"` keeps the header (version and key hints) but hides the model scope line and loaded-resource listing. |\n| `tuiMode` | `\"regular\" \\| \"fullscreen\"` | `\"fullscreen\"` | Interactive terminal UI mode. |\n| `fullscreenExitOutput` | `\"transcript\" \\| \"resume-hint\"` | `\"transcript\"` | Output printed when fullscreen mode exits. |\n| `fullscreenScrollbar` | `\"auto\" \\| \"always\" \\| \"hidden\"` | `\"auto\"` | Fullscreen transcript scrollbar behavior. |\n| `fullscreenCopyOnSelect` | boolean | `true` | Copy selected text automatically in fullscreen mode. |\n| `fullscreenWheelScrollLines` | `\"auto\"` \\| number | `\"auto\"` | Lines per mouse-wheel event in fullscreen mode, from 1 to 100. `\"auto\"` moves one line per event in local macOS terminals, which already accelerate wheel and trackpad input; elsewhere, and over SSH, it speeds up fast wheel spins to at most 6 lines per event. Alt+wheel moves five times as far. |\n| `editorPaddingX` | number | `0` | Horizontal editor padding from 0 to 3 cells. |\n| `outputPad` | `0 \\| 1` | `1` | Horizontal transcript padding for messages, tool output, `!` command output, and summary blocks. |\n| `autocompleteMaxVisible` | number | `5` | Visible autocomplete entries, from 3 to 20. |\n| `showHardwareCursor` | boolean | `false` | Use the terminal cursor instead of Pi's drawn cursor. Pi still positions it for input methods. |\n| `terminal.showImages` | boolean | `true` | Display inline images when supported. |\n| `terminal.imageWidthCells` | number | `60` | Preferred inline image width in terminal cells. |\n| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when rendered content shrinks. |\n| `terminal.showTerminalProgress` | boolean | `false` | Show OSC 9;4 progress in the terminal tab. |\n| `terminal.hyperlinks` | `boolean \\| \"auto\"` | `\"auto\"` | Override OSC 8 hyperlink detection. |\n| `terminal.images` | `\"kitty\" \\| \"iterm2\" \\| \"auto\" \\| false` | `\"auto\"` | Override inline-image protocol detection. |\n| `terminal.trueColor` | `boolean \\| \"auto\"` | `\"auto\"` | Override true-color detection. |\n| `images.autoResize` | boolean | `true` | Resize images to at most 2000 by 2000 pixels before sending them to a model. |\n| `images.blockImages` | boolean | `false` | Prevent images from being sent to models. |\n| `markdown.codeBlockIndent` | string | `\"  \"` | Prefix used to indent rendered code blocks. |\n| `markdown.mermaid` | `\"off\" \\| \"final\" \\| \"streaming\"` | `\"streaming\"` | Mermaid rendering mode. |\n\nSee [Themes](themes.md) and [Terminal Setup](terminal-setup.md) for format and platform details.\n\n## Network and retries\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `transport` | `\"auto\" \\| \"sse\" \\| \"websocket\" \\| \"websocket-cached\"` | `\"auto\"` | Preferred transport for AI providers that support multiple transports. |\n| `httpProxy` | string | None | Proxy URL applied as `HTTP_PROXY` and `HTTPS_PROXY` for Pi-managed HTTP clients. **Can only be set in agent-directory settings.** |\n| `httpIdleTimeoutMs` | number | `300000` | HTTP header and body idle timeout in milliseconds. Set to `0` to disable. |\n| `websocketConnectTimeoutMs` | number | `15000` | WebSocket connection timeout in milliseconds. Set to `0` to disable. |\n| `retry.enabled` | boolean | `true` | Enable automatic agent-level retry for transient failures. |\n| `retry.maxRetries` | number | `3` | Maximum agent-level retry attempts. |\n| `retry.baseDelayMs` | number | `2000` | Initial exponential-backoff delay in milliseconds. |\n| `retry.maxAgentDelayMs` | number | `60000` | Maximum agent-level retry delay in milliseconds. |\n| `retry.provider.timeoutMs` | number | `httpIdleTimeoutMs` | Provider request timeout in milliseconds. |\n| `retry.provider.maxRetries` | number | `0` | Provider-level retry attempts. |\n| `retry.provider.maxRetryDelayMs` | number | `60000` | Maximum server-requested delay in milliseconds. Set to `0` to disable the limit. |\n\nKeep `retry.provider.maxRetries` at `0` unless provider-level retries are required. Provider retries can delay Pi from handling quota and usage-limit errors itself.\n\n## Shell\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `shellPath` | string | Platform default | Custom shell executable path. Supports a leading `~`. |\n| `shellCommandPrefix` | string | None | Prefix prepended to every shell command. |\n| `npmCommand` | `string[]` | `npm` | Command and arguments used for npm package lookup and installation. |\n\nSee [Shell aliases](shell-aliases.md) for shell setup and [Pi Packages](packages.md) for package-manager behavior.\n\n## Resources\n\nResource paths in user settings resolve from the agent directory. Paths in project settings resolve from the project `.pi` directory. Absolute paths and `~` are supported.\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `packages` | array | `[]` | npm, git, or local Pi package sources. See [Pi Packages](packages.md). |\n| `extensions` | `string[]` | `[]` | Extension files or directories. |\n| `skills` | `string[]` | `[]` | Skill files or directories. |\n| `prompts` | `string[]` | `[]` | Prompt-template files or directories. |\n| `themes` | `string[]` | `[]` | Theme files or directories. |\n| `enableSkillCommands` | boolean | `true` | Register skills as `/skill:name` commands. |\n\nResource arrays support glob exclusions with `!pattern`, exact inclusion with `+path`, and exact exclusion with `-path`. Pi loads resources listed in both user-level and project settings.\n\nThe built-in extensions are named `builtin:mcp`, `builtin:llama.cpp`, `builtin:codemode`, and `builtin:tool-search` in `extensions`. They load by default; `-builtin:mcp` disables one. A `+builtin:<name>` or `-builtin:<name>` entry in project settings overrides the user setting. `pi config` lists them under Built-in. `--no-extensions` disables them too, and `-e builtin:<name>` loads one explicitly.\n\n## Updates, telemetry, and warnings\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `collapseChangelog` | boolean | `false` | Show a condensed changelog after an update. |\n| `enableInstallTelemetry` | boolean | `true` | Enable anonymous install/update reporting and selected provider attribution headers. Does not control update checks. |\n| `enableAnalytics` | boolean | `false` | Opt in to analytics data sharing. Currently used only by the experimental first-run setup. |\n| `warnings.anthropicExtraUsage` | boolean | `true` | Warn when Anthropic subscription authentication may use paid extra usage. |\n","packages/coding-agent/docs/shell-aliases.md":"# Configure shell commands\n\nPi starts a separate non-interactive shell process for each Bash command. Non-interactive Bash does not expand aliases by default and usually does not load the same startup files as an interactive terminal.\n\nUse `shellPath` to choose the Bash executable and `shellCommandPrefix` to run setup before each command.\n\n## Understand which shell Pi uses\n\n| Command source | Shell |\n|---|---|\n| Model calls the built-in `bash` tool | Pi's resolved Bash executable |\n| You enter `!command` or `!!command` | The same resolved Bash executable |\n| Model calls the optional `powershell` tool | PowerShell 7 (`pwsh.exe`) or Windows PowerShell |\n| An extension provides or replaces a shell tool | The operations implemented by that extension |\n\nPi normally invokes Bash with `bash -c`. On Unix systems, it uses `/bin/bash`, then `bash` on `PATH`, and finally `sh` when Bash is unavailable. Native Windows first checks the configured path, then Git Bash, then `bash.exe` on `PATH`.\n\n## Choose a Bash executable\n\nSet `shellPath` in `~/.pi/agent/settings.json` when Pi should use a specific executable:\n\n```json\n{\n  \"shellPath\": \"~/.local/bin/bash\"\n}\n```\n\nOn Windows, use forward slashes or escape backslashes:\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```\n\nRun `/reload` after changing the setting. See [Run Pi on Windows](windows.md) for the native Windows defaults.\n\n## Run setup before every Bash command\n\nSet `shellCommandPrefix` to prepend shell setup to both the built-in `bash` tool and user-entered `!` or `!!` commands:\n\n```json\n{\n  \"shellCommandPrefix\": \"export CI=1\"\n}\n```\n\nPi joins the prefix and requested command with a newline. The prefix runs again for every command, so keep it fast and free of interactive prompts.\n\n## Enable Bash aliases\n\nStore aliases needed by Pi in a Bash-compatible file instead of parsing an entire interactive shell configuration.\n\nCreate `~/.bash_aliases`:\n\n```bash\nalias ll='ls -la'\nalias gs='git status --short'\n```\n\nThen configure Pi to enable alias expansion and load the file:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\nsource ~/.bash_aliases\"\n}\n```\n\nRun `/reload`, then verify the alias through Pi:\n\n```text\n!ll\n```\n\nThe command should produce the same listing as `ls -la`.\n\nAliases must use Bash-compatible syntax. Do not source an arbitrary `.zshrc` into Bash because zsh options, functions, and plugins may not parse or behave correctly there.\n\n## Troubleshooting\n\n### The prefix works for `!` but not for an extension tool\n\n`shellCommandPrefix` configures Pi's built-in Bash execution. An extension that replaces the `bash` tool or provides its own shell operations controls its own setup. Check that extension's documentation.\n\n### `shopt` is not found\n\nPi has fallen back to `sh` or `shellPath` points to a non-Bash shell. Install Bash or set `shellPath` to a Bash executable before using Bash-specific setup such as `shopt`.\n\n### A setup command waits for input\n\nRemove interactive commands from `shellCommandPrefix`. The prefix runs in a non-interactive process before every Bash command.\n\nFor the complete setting definitions, see [Shell settings](settings.md#shell).\n","packages/coding-agent/docs/skills.md":"# Skills\n\nSkills give Pi specialized instructions and supporting files for a particular kind of work. Pi advertises each available skill by name and description, then loads its full instructions only when the task calls for them.\n\nUse a skill when a workflow needs more context than a prompt template but does not need a new executable integration point. Skills can bundle scripts, references, and assets alongside their instructions.\n\nPi implements the [Agent Skills specification](https://agentskills.io/specification). Most invalid fields produce warnings rather than stopping startup.\n\n## Create a skill\n\nA skill is a directory containing `SKILL.md`:\n\n```text\npdf-tools/\n├── SKILL.md\n├── scripts/\n│   └── extract.sh\n├── references/\n│   └── formats.md\n└── assets/\n    └── template.json\n```\n\nStart `SKILL.md` with frontmatter followed by direct instructions:\n\n```markdown\n---\nname: pdf-tools\ndescription: Extract text and tables from PDF files. Use when reading, converting, or inspecting PDFs.\n---\n\n# PDF tools\n\nRead `references/formats.md` before converting a document. Run scripts relative to this skill directory.\n```\n\nThe description determines when the model considers loading the skill. State both what the skill does and when it applies. Avoid descriptions such as “Helps with PDFs,” which do not provide enough routing information.\n\nUse relative paths from the skill directory when referring to bundled files. Pi tells the model where the skill lives so it can resolve those paths.\n\n## Understand how skills load\n\nAt startup, Pi scans configured skill locations and adds each skill’s name, description, and path to the system prompt. It does not add the full instructions.\n\nWhen a task matches, the model reads `SKILL.md` and follows its instructions. This keeps detailed guidance out of context until it is needed. A model might fail to load a relevant skill, so use `/skill:name` when you need to force it.\n\nArguments after `/skill:name` are appended to the loaded instructions as a user request:\n\n```text\n/skill:pdf-tools extract report.pdf\n```\n\nSet `disable-model-invocation: true` in frontmatter when a skill should be available only through its explicit command. The `enableSkillCommands` [setting](settings.md) controls whether skill commands appear in interactive command discovery; manually entered `/skill:name` commands still work.\n\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the skill in your user or project skills directory. Directories containing `SKILL.md` are discovered recursively.\n\nPi also supports the Agent Skills locations `~/.agents/skills/` and `.agents/skills/`. Project `.agents/skills/` directories are discovered from the working directory through its ancestors, stopping at the repository root when one exists.\n\nPi accepts some standalone Markdown skills, but a directory containing `SKILL.md` is the portable form and should be preferred. See [Settings](settings.md#resources) and [Pi Packages](packages.md) for additional locations.\n\nProject skills can instruct the model to run scripts or modify files. Review unfamiliar skills and their supporting files before granting project trust.\n\n## Write portable frontmatter\n\nThe Agent Skills specification defines these fields:\n\n| Field | Purpose |\n|---|---|\n| `name` | Command and display name |\n| `description` | Routing description shown to the model |\n| `license` | License name or bundled license file |\n| `compatibility` | Environment requirements |\n| `metadata` | Additional key-value metadata |\n| `allowed-tools` | Experimental pre-approved tool list |\n| `disable-model-invocation` | Hide the skill from automatic model selection |\n\nNames use lowercase letters, numbers, and hyphens, with no leading, trailing, or consecutive hyphens. They can contain at most 64 characters; descriptions can contain at most 1024.\n\nPi neither requires nor warns when the declared name differs from the parent directory. Other Agent Skills implementations may enforce that requirement, so matching names remain the portable choice.\n\nMalformed `SKILL.md` files and declared skills without descriptions are not loaded. Name collisions keep the first discovered skill and produce a warning.\n\n## Validate and share a skill\n\nRun Pi from a location where the skill is discoverable, then inspect the startup diagnostics and `/skill:name` command. Run `/reload` after editing a skill during an active session.\n\nUse a [Pi package](packages.md) to distribute one or more skills through npm or git. Keep environment setup inside the skill and declare any required runtime dependencies in the package.\n\nFor examples, see the [Anthropic skills collection](https://github.com/anthropics/skills) and [Pi skills collection](https://github.com/badlogic/pi-skills).\n","packages/coding-agent/docs/slash-commands.md":"# Slash commands\n\nType `/` in Pi's terminal editor to search the commands available in the current session. This page lists the built-in commands in the current Pi release.\n\nExtensions, prompt templates, and skills can add commands. The command menu in Pi is therefore the exact reference for the resources loaded in your session.\n\n## Models and settings\n\n| Command | Description |\n|---|---|\n| `/settings` | Open settings |\n| `/model [provider/model]` | Select a model |\n| `/thinking [level]` | Set the thinking level |\n| `/scoped-models` | Configure the models used by interactive cycling |\n| `/login [provider]` | Add provider authentication |\n| `/logout` | Remove provider authentication |\n| `/llama` | Manage models on the configured llama.cpp router |\n\n## Sessions and context\n\n| Command | Description |\n|---|---|\n| `/new` | Start a new session |\n| `/resume` | Switch to another saved session |\n| `/name [name]` | Set the session display name, or show the current name when omitted |\n| `/session` | Show current session information and statistics |\n| `/tree` | Navigate the session tree |\n| `/fork` | Create a new session from an earlier user message |\n| `/clone` | Duplicate the current session at its current position |\n| `/compact [instructions]` | Compact the current context, optionally with custom instructions |\n| `/import <path>` | Import and resume a JSONL session |\n\n## Export and share\n\n| Command | Description |\n|---|---|\n| `/copy` | Copy the last assistant message |\n| `/export [path]` | Export the session as HTML or JSONL |\n| `/share` | Upload the session and return a viewer link |\n| `/bug [description]` | Prepare a private bug report for the Pi developers |\n\nReview a session before exporting or sharing it. Sessions can contain prompts, tool arguments, command output, file contents, and credentials exposed during the conversation.\n\n## Runtime and project\n\n| Command | Description |\n|---|---|\n| `/trust` | Save a project trust decision for future Pi processes |\n| `/reload` | Reload keybindings, extensions, skills, templates, themes, and context files |\n| `/hotkeys` | Show active keyboard shortcuts |\n| `/changelog` | Show changelog entries |\n| `/quit` | Quit Pi |\n\n## Commands added by resources\n\n- Extensions can register commands with their own arguments and completion behavior.\n- Each prompt template is available under its template name.\n- Skills are available as `/skill:name` when skill commands are enabled.\n\nUse `/reload` after adding or changing a discovered command resource. See [Extensions](extensions.md), [Prompt Templates](prompt-templates.md), and [Skills](skills.md) for their loading and naming rules.\n","packages/coding-agent/docs/terminal-setup.md":"# Configure your terminal\n\nMost modern terminals work with Pi without additional setup. Use this page when modified keys, scrolling, links, images, colors, or input-method editor (IME) positioning do not behave as expected.\n\nPi uses extended-key protocols so terminals can distinguish combinations such as `Shift+Enter` and `Alt+Enter` from plain `Enter`. Terminal proxies, multiplexers, and built-in IDE terminals can change or discard that information.\n\n## Troubleshooting\n\n| Symptom | Start here |\n|---|---|\n| `Shift+Enter` submits instead of inserting a line | Your terminal's section below; for tmux, see [Run Pi in tmux](tmux.md) |\n| `Alt+Enter` does not queue a follow-up | [WezTerm](#wezterm), [Alacritty](#alacritty), or [Windows Terminal](#windows-terminal) |\n| Fullscreen scrolling is unusually slow | [iTerm2](#iterm2) |\n| Links work but show no hover preview | [Ghostty](#ghostty) |\n| Inline images or colors are not detected | [Override detected capabilities](#override-detected-capabilities) |\n| An IME candidate window appears in the wrong place | [WezTerm](#wezterm) or [IntelliJ IDEA](#intellij-idea-integrated-terminal) |\n| Modified keys fail only inside tmux | [Run Pi in tmux](tmux.md) |\n\nUse `/hotkeys` to inspect Pi's active shortcuts. See [Keybindings](keybindings.md) to change them.\n\n## Kitty\n\nKitty supports the required keyboard protocol without additional configuration.\n\n## iTerm2\n\nRegular terminal mode works without additional configuration.\n\n### Fix slow fullscreen scrolling\n\nIn fullscreen mode, Pi owns the viewport, so iTerm2 sends mouse-wheel reports instead of scrolling native terminal history. Fast trackpad gestures can then move only about one line at a time.\n\nTo change this behavior:\n\n1. Open **iTerm2 > Settings > Advanced**.\n2. Search for **Trackpad scrolls fast?**.\n3. Set it to **No**.\n\nThis is an iTerm2-wide setting and can also change native trackpad scrolling. The underlying behavior is tracked in [iTerm2 issue 9619](https://gitlab.com/gnachman/iterm2/-/work_items/9619).\n\n## Apple Terminal\n\nPi enables enhanced key reporting when available. If Terminal.app still sends plain Return for `Shift+Enter`, Pi uses a local macOS modifier fallback and treats it as `Shift+Enter`.\n\nThe fallback works only when Pi runs on the same Mac as Terminal.app. It cannot inspect the local modifier state when Pi runs on another machine over SSH.\n\n## Ghostty\n\nAdd this mapping to Ghostty's configuration if `Alt+Backspace` does not work:\n\n```text\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nThe configuration file is `~/Library/Application Support/com.mitchellh.ghostty/config` on macOS and `~/.config/ghostty/config` on Linux.\n\nOlder Claude Code configurations may contain:\n\n```text\nkeybind = shift+enter=text:\\n\n```\n\nThis sends a raw linefeed, which Pi cannot distinguish from `Ctrl+J`. Remove the mapping if an older Claude Code installation is the only reason you added it. Pi already binds `Ctrl+J` as a newline alternative, so the mapping may appear to work while still preventing Pi and tmux from receiving a real `Shift+Enter` event.\n\n### Open links in fullscreen mode\n\nLinks remain clickable in fullscreen mode, but Ghostty does not show its normal hover underline or URL preview while Pi captures mouse input. Hold `Shift+Command` on macOS or `Shift+Ctrl` on Linux to use Ghostty's native link handling.\n\n## WezTerm\n\nWezTerm normally reports `Shift+Enter` through xterm extended keys. To enable the Kitty keyboard protocol explicitly, create `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\n### Forward Alt+Enter on macOS\n\nWezTerm binds `Option+Enter` to fullscreen by default on macOS. To use it for Pi's follow-up queue, add this entry to your `config.keys` table:\n\n```lua\n{\n  key = 'Enter',\n  mods = 'ALT',\n  action = wezterm.action.SendString('\\x1b[13;3u'),\n}\n```\n\nA complete minimal configuration is:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\n### Position an IME candidate window in WSL\n\nIf CJK IME candidates do not follow Pi's text cursor in WSL, show the hardware cursor:\n\n```bash\nexport PI_HARDWARE_CURSOR=1\npi\n```\n\nYou can instead set `showHardwareCursor` to `true` in Pi settings.\n\n## Alacritty\n\nAlacritty normally reports `Shift+Enter`. On macOS, `Option+Enter` can arrive as plain `Enter`. Add this to `~/.config/alacritty/alacritty.toml` to forward it to Pi:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nRestart Alacritty after changing the file.\n\n## VS Code integrated terminal\n\nVS Code 1.109.5 and newer enable the Kitty keyboard protocol in the integrated terminal by default.\n\nFor an older version, add a `Shift+Enter` terminal binding to `keybindings.json`:\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\nThe user `keybindings.json` file is normally located at:\n\n- macOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Linux: `~/.config/Code/User/keybindings.json`\n- Windows: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\n## Zed integrated terminal\n\nAdd these bindings to Zed's `keymap.json`:\n\n```json\n{\n  \"context\": \"Terminal\",\n  \"bindings\": {\n    \"shift-enter\": [\"terminal::SendText\", \"\\u001b[13;2u\"],\n    \"ctrl--\": [\"terminal::SendText\", \"\\u001b[45;5u\"],\n    \"ctrl-alt-]\": [\"terminal::SendText\", \"\\u001b[93;7u\"]\n  }\n}\n```\n\n## Windows Terminal\n\nWindows Terminal uses Pi's Windows and WSL shortcut defaults. See [Keybindings](keybindings.md) for the complete list.\n\n### Forward Shift+Enter\n\nOpen Windows Terminal's `settings.json` with `Ctrl+Shift+,` or **Settings > Open JSON file**. Add this object to its `actions` array:\n\n```json\n{\n  \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n  \"keys\": \"shift+enter\"\n}\n```\n\nFully close and reopen Windows Terminal, then verify that `Shift+Enter` inserts a new line in Pi.\n\n### Use Alt+Enter for follow-ups\n\nWindows Terminal binds `Alt+Enter` to fullscreen by default. Pi therefore uses `Ctrl+Q` for follow-ups on Windows and WSL.\n\nTo use `Alt+Enter` instead, configure Windows Terminal to forward the key and bind `app.message.followUp` to `alt+enter` in Pi's `keybindings.json`. See [Keybindings](keybindings.md#assign-keybindings).\n\n## xfce4-terminal and Terminator\n\nThese terminals cannot reliably distinguish modified Enter keys from plain `Enter`. Custom bindings such as `Ctrl+Enter` or `Shift+Enter` therefore may not work.\n\nUse a terminal with modern extended-key support when you need those shortcuts, such as Kitty, Ghostty, WezTerm, iTerm2, Windows Terminal, or a compatible Alacritty build.\n\n## IntelliJ IDEA integrated terminal\n\nIntelliJ IDEA's built-in terminal cannot reliably distinguish `Shift+Enter` from plain `Enter`. Use `Ctrl+J` for a newline or run Pi in a terminal with modern extended-key support.\n\nIf an IME candidate window does not follow the text cursor, show the hardware cursor:\n\n```bash\nexport PI_HARDWARE_CURSOR=1\npi\n```\n\n## Override detected capabilities\n\nPi automatically detects OSC 8 hyperlinks, inline image protocols, and truecolor support. A terminal proxy or multiplexer can make that detection inaccurate.\n\n| Capability | Environment variable | Setting |\n|---|---|---|\n| Hyperlinks | `PI_HYPERLINKS=1\\|0\\|auto` | `terminal.hyperlinks: true\\|false\\|\"auto\"` |\n| Inline images | `PI_IMAGE_PROTOCOL=kitty\\|iterm2\\|none\\|auto` | `terminal.images: \"kitty\"\\|\"iterm2\"\\|false\\|\"auto\"` |\n| Truecolor | `PI_TRUE_COLOR=1\\|0\\|auto` | `terminal.trueColor: true\\|false\\|\"auto\"` |\n\nSettings take precedence over environment variables. An unset value or `auto` preserves automatic detection.\n\nOnly force a capability supported by the complete terminal path. Unsupported escape sequences can corrupt rendering. See [Environment Variables](environment-variables.md#pi-process-configuration) and [Settings](settings.md) for the canonical value definitions.\n\n## Program status\n\nPi reports its state with the [Program Status Protocol (OSC 7501)](https://www.superlogical.com/rex/docs/build/program-status), so terminals and agent dashboards can show whether it is working, waiting for you, done, or failed:\n\n| State | When |\n|---|---|\n| `working` | An agent run or compaction is in progress. The message is the session name. |\n| `blocked` | An extension dialog or login waits for you. The message is the dialog title. |\n| `done` | A run finished. The message is the session name. |\n| `error` | A run ended with an error that is not retried. The message is the first line of the error. |\n| `idle` | Pi started, or you cancelled the run. |\n\nReports never contain prompts or model output. Pi sends them only after the terminal answers the protocol's support query; tmux and screen do not forward them. Set `PI_PROGRAM_STATUS=1` to send reports without asking, or `PI_PROGRAM_STATUS=0` to turn them off.\n","packages/coding-agent/docs/termux.md":"# Run Pi on Android with Termux\n\nPi runs on Android through [Termux](https://termux.dev/), a terminal emulator and Linux environment. Text input, file tools, and shell commands are supported. Pi can copy and paste text through the Android clipboard with Termux:API. Clipboard image paste is not supported.\n\n## Before you begin\n\nInstall Termux from [GitHub or F-Droid](https://github.com/termux/termux-app#installation). Do not use the deprecated Google Play build.\n\n[Termux:API](https://github.com/termux/termux-api#installation) is optional. Install it only when you want Pi to copy or paste Android clipboard text, or when shell commands need Android device APIs.\n\n## Install Pi\n\n1. Update Termux packages:\n\n   ```bash\n   pkg update && pkg upgrade\n   ```\n\n2. Install Node.js and Git:\n\n   ```bash\n   pkg install nodejs git\n   ```\n\n3. Install Pi:\n\n   ```bash\n   npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n   ```\n\n4. Verify the installation:\n\n   ```bash\n   pi --version\n   ```\n\n5. Open the folder you want to work in and start Pi:\n\n   ```bash\n   cd /path/to/working-folder\n   pi\n   ```\n\nContinue with the main [Quickstart](quickstart.md#3-choose-a-model) to connect a model and run your first task.\n\n## Access Android shared storage\n\nTermux cannot access shared Android storage until you grant permission. Run this once:\n\n```bash\ntermux-setup-storage\n```\n\nAfter approval, Android shared storage is available under `/storage/emulated/0` and through the links Termux creates under `~/storage/`.\n\nOnly grant this permission when Pi should be able to access those files. Commands and tools running in Termux use the same storage permissions as the Termux process.\n\n## Use clipboard commands\n\nPi uses `termux-clipboard-set` to copy text and `termux-clipboard-get` for its clipboard-paste shortcut. Shell commands can use both commands directly. Install the Termux:API app and its command-line package:\n\n```bash\npkg install termux-api\n```\n\nVerify the integration:\n\n```bash\nprintf 'Pi clipboard test' | termux-clipboard-set\ntermux-clipboard-get\n```\n\nThe second command should print `Pi clipboard test`.\n\nThe Termux clipboard API supports text only. Pi's clipboard-paste shortcut inserts that text into the editor but cannot attach clipboard images.\n\n## Add Termux-specific instructions\n\nPi detects that it is running in Termux, but it cannot infer how you want it to interact with Android. Add only the environment details relevant to your work to `~/.pi/agent/AGENTS.md`:\n\n````markdown\n# Termux environment\n\n- Pi runs in Termux on Android.\n- Shared Android storage is under `/storage/emulated/0`.\n- Open URLs with `termux-open-url \"https://example.com\"`.\n- Open files with `termux-open <path>`.\n- Do not access shared storage unless the task requires it.\n````\n\nRun `/reload` after changing the file during an active session.\n\n## Troubleshooting\n\n### Clipboard integration fails\n\nConfirm that you installed both components:\n\n1. The Termux:API Android app from the same source as Termux\n2. The `termux-api` command-line package\n\nThen run the clipboard verification commands above outside Pi. If they fail there, fix the Termux:API installation before retrying Pi's copy command.\n\n### Shared storage reports permission denied\n\nRun `termux-setup-storage`, approve the Android permission request, and retry the path under `~/storage/` or `/storage/emulated/0`.\n\n### Pi is not found after installation\n\nOpen a new Termux shell and run:\n\n```bash\nnpm prefix -g\ncommand -v pi\n```\n\nConfirm that the global npm binary directory is on `PATH`, then reinstall Pi if the package is missing.\n","packages/coding-agent/docs/themes.md":"# Customize Pi with themes\n\nThemes control the colors Pi uses in interactive mode and HTML exports. Pi includes the `system`, `dark`, and `light` themes. You can select one theme, follow your terminal's light or dark appearance, or create your own palette.\n\n## Use your terminal's colors\n\nThe `system` theme is the default. It builds Pi's colors from your terminal's theme, so Pi matches the terminal instead of bringing its own palette:\n\n- Pi queries the terminal's default foreground and background colors and its 16 ANSI colors.\n- Each Pi color takes its hue from one ANSI color, for example errors from red and links from blue.\n- Pi sets each color's lightness so that it stands out from the background by a minimum contrast. Body text keeps at least a 4.5:1 WCAG contrast ratio on the background and every panel.\n- When the terminal switches between light and dark, Pi queries the colors again and rebuilds the theme.\n\nThe theme adapts to what the terminal reports:\n\n| Terminal reports | Result |\n|---|---|\n| Background and ANSI colors | Colors from the terminal palette, placed for the actual background. |\n| Background only | Pi's own hues, placed for the actual background. |\n| Nothing | ANSI color indices and the terminal's default colors, which the terminal renders itself. Secondary text is faint, and panels have no background color. |\n\nPi asks the terminal for its colors when it starts. Terminals usually answer within a few milliseconds, and Pi waits at most 100 ms before showing the startup header. If the terminal does not answer in time, Pi uses the ANSI color fallback, and it still applies the colors if they arrive later, for example over a slow SSH connection. `system` is a reserved name: a custom theme with that name is ignored.\n\n<a id=\"selecting-a-theme\"></a>\n\n## Choose a theme\n\nOpen `/settings` and select **Theme**. You can use one theme for every terminal appearance or choose separate themes for light and dark terminals.\n\nThe selection is saved as the `theme` [setting](settings.md#terminal-and-display):\n\n```json\n{\n  \"theme\": \"dark\"\n}\n```\n\nWithout a `theme` setting, Pi uses `system`.\n\nAutomatic mode stores the light theme first and the dark theme second:\n\n```json\n{\n  \"theme\": \"light/dark\"\n}\n```\n\nPi decides whether the terminal is light or dark from its reported background and foreground colors. If the terminal does not report its background, Pi uses the terminal's light/dark notification, then the `COLORFGBG` environment variable, then dark. The same decision picks the theme of a light/dark pair and the appearance of `system`. When automatic mode is active, Pi changes themes when the terminal reports an appearance change. Theme names cannot contain `/` because Pi reserves it for this setting format.\n\nUse `--use-theme` to choose the initial theme for one invocation without changing the saved setting:\n\n```bash\npi --use-theme light\npi --use-theme light/dark\n```\n\nSee [CLI resources](cli.md#resources) for the command-line option.\n\n## Create a custom theme\n\nCopy one of the [built-in themes](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/src/modes/interactive/theme) or create a new JSON file conforming to the [schema](https://pi.dev/schemas/theme.schema.json). The built-in themes use OKHSL colors, with variables for colors that several roles share, so you can adjust a hue, saturation, or lightness directly.\n\n1. Save the file as `<agent-dir>/themes/my-theme.json`. The agent directory defaults to `~/.pi/agent`.\n2. Set its `name` to `my-theme`.\n3. Change values in `vars` and `colors`.\n4. Select `my-theme` through `/settings`.\n\nUse the theme name as the filename. Pi hot-reloads the active user theme only from `<agent-dir>/themes/<name>.json`. Run `/reload` after adding or changing a theme from any other source.\n\n## Understand the theme file\n\n| Property | Required | Responsibility |\n|---|---|---|\n| `$schema` | No | Enables editor validation and completion against Pi's published schema. |\n| `name` | Yes | Identifies the theme in selectors and settings. It must be unique, cannot contain `/`, and cannot be `system`. |\n| `appearance` | No | `\"dark\"` or `\"light\"`: the background the theme is designed for. Pi detects it from the theme colors when omitted. |\n| `vars` | No | Defines reusable color values. Variables can reference other variables. |\n| `colors` | Yes | Assigns colors to terminal UI roles. The schema identifies required and optional roles. |\n| `export` | No | Overrides page and panel backgrounds in HTML exports. |\n\nTheme objects are strict: only documented top-level fields and color tokens are accepted. Define reusable custom colors under `vars`; custom keys under `colors` or `export` and additional top-level metadata are rejected.\n\nA color can be written in six forms:\n\n| Form | Example | Meaning |\n|---|---|---|\n| RGB hexadecimal | `\"#0af\"` or `\"#00aaff\"` | A three- or six-digit sRGB color. |\n| OKLCH | `\"oklch(62% 0.1 200)\"` | Perceptual lightness, chroma, and hue. |\n| OKHSL | `\"okhsl(250 60% 55%)\"` | Hue, saturation, and lightness. Saturation is relative to the most the sRGB gamut allows at that hue and lightness, so every value is in gamut and equal saturation looks equally colorful. |\n| 256-color index | `39` | An ANSI palette index from `0` through `255`. |\n| Variable reference | `\"primary\"` | The value of an entry in `vars`. |\n| Terminal default | `\"\"` | The terminal's default foreground or background color. |\n\nTerminal default colors render as the terminal's own colors. Where Pi needs a concrete value, such as HTML export or extension color math, it uses the default colors the terminal reports, or a black or white guess based on the theme's appearance.\n\nPi resolves chained variable references. A missing variable or circular reference makes the theme invalid. Pi uses truecolor when available, gamut-maps OKLCH to sRGB, and approximates colors for 256-color terminals. HTML exports convert OKHSL values to hexadecimal because CSS does not support them. If colors differ from their source values, check your terminal's truecolor detection and contrast settings. See [Configure Your Terminal](terminal-setup.md#override-detected-capabilities).\n\nUse the [theme JSON schema](https://pi.dev/schemas/theme.schema.json) for the exact properties, required colors, and accepted value types.\n\nPi reports invalid theme files during startup and `/reload`.\n\n## Find the color to change\n\nTheme colors describe interface roles rather than individual components. Use these groups to find the relevant part of the schema:\n\n| Area | Color names |\n|---|---|\n| General interface | `accent`, `border*`, `text`, `muted`, `dim`, `success`, `error`, `warning` |\n| Selection and fullscreen | `selectedBg`, `searchMatch*`, `scrollbar*` |\n| Messages | `userMessage*`, `customMessage*`, `thinkingText` |\n| Tool execution | `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`, `toolTitle`, `toolOutput` |\n| Markdown | `md*` |\n| Tool diffs | `toolDiff*` |\n| Syntax highlighting | `syntax*` |\n| Editor modes | `thinking*`, `bashMode` |\n| HTML export | `export.pageBg`, `export.cardBg`, `export.infoBg` |\n\nThe schema is the format reference. The built-in themes provide complete values that you can copy and adjust.\n\nFive colors are optional and inherit another color when omitted:\n\n| Optional color | Fallback |\n|---|---|\n| `scrollbarTrack` | `muted` |\n| `scrollbarThumb` | `text` |\n| `searchMatchBg` | `selectedBg` |\n| `searchMatchText` | `text` |\n| `thinkingMax` | `thinkingXhigh` |\n\nIf `export` colors are omitted, Pi derives HTML page and panel backgrounds from `userMessageBg`.\n\n## Load a theme from a project or package\n\nPlace a project theme in `.pi/themes/`. Project themes load only after [project trust](security.md#understand-project-trust) is granted.\n\nYou can also load theme files and directories through the `themes` setting or distribute them in a Pi package. See [Configuration](configuration.md), [Settings](settings.md#resources), and [Pi Packages](packages.md).\n\nEach loaded theme must have a unique name. Pi reports duplicate names as resource collisions.\n","packages/coding-agent/docs/tmux.md":"# Run Pi in tmux\n\nPi works inside tmux, but tmux can report `Shift+Enter`, `Ctrl+Enter`, and plain `Enter` as the same key. Enable extended keys so Pi can distinguish them.\n\n## Check your tmux version\n\n```bash\ntmux -V\n```\n\nFor tmux 3.5 or newer, use the recommended CSI-u configuration below. For tmux 3.2 through 3.4, use the older-version configuration.\n\n## Enable extended keys in tmux 3.5 or newer\n\nAdd these lines to `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nPi requests extended-key reporting when the terminal does not provide the Kitty keyboard protocol directly. CSI-u is the most reliable format for forwarding modified keys through tmux.\n\n## Restart tmux\n\nThe configuration applies to the tmux server. To guarantee that it is active, close your tmux sessions and start a new server.\n\nIf you choose to stop the server from the command line, save your work first. This command terminates every session managed by that server:\n\n```bash\ntmux kill-server\ntmux\n```\n\n## Verify modified keys\n\nStart Pi inside the new tmux session and check that:\n\n1. `Shift+Enter` inserts a new line in the editor.\n2. `Enter` submits the prompt.\n3. `Alt+Enter` queues a follow-up on macOS and Linux. Windows and WSL use `Ctrl+Q` by default.\n\nIf these keys still behave like plain `Enter`, verify that the terminal outside tmux can report modified keys. See [Configure your terminal](terminal-setup.md).\n\n## Use tmux 3.2 through 3.4\n\nThese versions support extended keys but not `extended-keys-format csi-u`. Add only:\n\n```tmux\nset -g extended-keys on\n```\n\nPi supports the xterm `modifyOtherKeys` format used by these versions. Restart tmux and repeat the verification steps.\n\nFor older versions, upgrade tmux or use Pi outside tmux rather than relying on modified Enter shortcuts.\n","packages/coding-agent/docs/tui.md":"# Terminal UI\n\n`@earendil-works/pi-tui` provides the terminal component system used by Pi. Extensions use it when built-in dialogs, notifications, status text, and widgets are not enough for the interaction they need.\n\nStart with `ctx.ui` methods from an [extension](extensions.md#interact-with-the-user). Build a custom component only when the UI needs its own rendering, keyboard or mouse input, focus, layout, or lifecycle.\n\n## Choose an integration point\n\n| Need | Use |\n|---|---|\n| Select, confirm, input, or multi-line editor | `ctx.ui.select()`, `confirm()`, `input()`, or `editor()` |\n| Non-blocking feedback | `ctx.ui.notify()` or `setStatus()` |\n| Persistent content near the editor | `ctx.ui.setWidget()` |\n| Replace the header, footer, or editor | The corresponding `ctx.ui` component factory |\n| Temporary interactive screen or overlay | `ctx.ui.custom()` |\n| Custom rendering for a tool or session entry | An extension renderer |\n\nThese APIs receive Pi’s active theme and keybindings where needed. Do not create a second terminal renderer inside an extension.\n\n## Understand the component model\n\nA component renders an array of terminal lines for an available width. It can optionally handle keyboard and mouse input, and it must invalidate cached output when its state or theme-dependent content changes.\n\nEvery rendered line must fit within the supplied width. Measure visible terminal columns rather than string length because ANSI escapes, wide characters, emoji, and combining characters change display width.\n\nUse `visibleWidth()`, `truncateToWidth()`, `sliceByColumn()`, and `wrapTextWithAnsi()` instead of implementing terminal-width handling yourself. Pi resets styling and hyperlinks after every line, so reapply styles on each rendered line.\n\nAfter changing component state, invalidate the affected component and call the injected `tui.requestRender()`. The TUI coalesces render requests and updates the terminal.\n\n## Compose built-in components\n\nThe package includes components for common layouts and controls:\n\n- `Text`, `Markdown`, `Image`, and `TruncatedText` render content.\n- `Container`, `VStack`, `HStack`, `Box`, and `Spacer` compose layouts.\n- `Input` and `Editor` accept text.\n- `SelectList` and `SettingsList` implement searchable selection and settings flows.\n- `ScrollView` provides a bounded scrollable viewport.\n- `Loader` and `CancellableLoader` report ongoing work.\n- `MouseRegion` adds pointer behavior around another component.\n\nPrefer these components over rebuilding selection, scrolling, text editing, or width handling. The extension examples show how to combine them with Pi’s borders and themes.\n\n## Handle keyboard input and focus\n\nUse `matchesKey()` and `Key` for terminal keyboard input. The parser accounts for supported terminal protocols and key modifiers. Extension components should use the injected `KeybindingsManager` for configurable application actions.\n\nA component that displays a text cursor should implement `Focusable` and place `CURSOR_MARKER` immediately before its visual cursor. Wrap the cursor cell in `renderFakeCursor()` so the TUI draws it in reverse video, or hides it after `CURSOR_MARKER` when the hardware cursor is shown. The TUI uses the marker to position the hardware cursor for input method editors.\n\nContainers that wrap an `Input` or `Editor` must propagate their `focused` state to that child. Without propagation, Chinese, Japanese, Korean, and other IME candidate windows can appear at the wrong screen position.\n\nExtend Pi’s `CustomEditor` when replacing the main editor. It preserves application shortcuts and agent controls.\n\nForward keys your editor does not own to the base implementation, and restore the default by clearing the custom editor factory.\n\n## Handle mouse input\n\nFullscreen mode routes normalized mouse events to components. A handler can mark an event handled, capture a drag sequence, request focus, or request a render.\n\nUnhandled wheel events scroll the nearest `ScrollView`. Unhandled primary-button drags remain available for transcript selection. OSC 8 links take precedence over enclosing click regions.\n\nRegular mode leaves mouse input to the terminal because the terminal owns scrollback. Design every interaction with a keyboard path even when fullscreen mouse input is available.\n\n## Use custom screens and overlays\n\n`ctx.ui.custom()` temporarily gives one component control of the interactive area and resolves when that component calls the supplied completion callback.\n\n### Build a minimal custom screen\n\nThis complete extension shows the custom-component lifecycle: render within the supplied width, use injected keybindings, invalidate cached output after state changes, request a render, and finish through `done()`.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth } from \"@earendil-works/pi-tui\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"pick-target\", {\n    description: \"Pick a deployment target\",\n    handler: async (_args, ctx) => {\n      if (ctx.mode !== \"tui\") {\n        ctx.ui.notify(\"pick-target requires interactive mode\", \"error\");\n        return;\n      }\n\n      const choices = [\"development\", \"staging\", \"production\"];\n      const selected = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {\n        let selectedIndex = 0;\n        let cachedWidth: number | undefined;\n        let cachedLines: string[] | undefined;\n\n        const invalidate = () => {\n          cachedWidth = undefined;\n          cachedLines = undefined;\n        };\n\n        return {\n          render(width: number): string[] {\n            if (cachedLines && cachedWidth === width) return cachedLines;\n\n            cachedWidth = width;\n            cachedLines = [\n              theme.fg(\"accent\", \"Choose a deployment target\"),\n              ...choices.map((choice, index) => `${index === selectedIndex ? \"> \" : \"  \"}${choice}`),\n              theme.fg(\"dim\", \"up/down move, enter selects, escape cancels\"),\n            ].map((line) => truncateToWidth(line, width));\n            return cachedLines;\n          },\n          handleInput(data: string): void {\n            if (keybindings.matches(data, \"tui.select.up\")) {\n              selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;\n            } else if (keybindings.matches(data, \"tui.select.down\")) {\n              selectedIndex = (selectedIndex + 1) % choices.length;\n            } else if (keybindings.matches(data, \"tui.select.confirm\")) {\n              done(choices[selectedIndex]);\n              return;\n            } else if (keybindings.matches(data, \"tui.select.cancel\")) {\n              done(undefined);\n              return;\n            } else {\n              return;\n            }\n\n            invalidate();\n            tui.requestRender();\n          },\n          invalidate,\n        };\n      });\n\n      if (selected) ctx.ui.notify(`Selected ${selected}`, \"info\");\n    },\n  });\n}\n```\n\nThis component does not display a text cursor, so it does not need `Focusable`. A component containing `Input` or `Editor` must implement `Focusable` and propagate its `focused` value to that child.\n\nPass `overlay: true` to draw above existing content. Overlay options control size, anchors, offsets, margins, and responsive visibility. An overlay handle can change focus or temporarily hide and show the overlay with `setHidden()` while the interaction remains active.\n\nFocused overlays retain input ownership across ordinary renders. If another component should receive input while an overlay remains visible, explicitly release or redirect focus through the handle.\n\nTreat each custom component instance as belonging to one interaction. Create a new instance when starting that interaction again.\n\nFinish the interaction with the completion callback supplied to the component factory. It resolves the `ctx.ui.custom()` promise and disposes the component. Do not call `OverlayHandle.hide()` on an overlay created by `ctx.ui.custom()`.\n\nSee [`overlay-qa-tests.ts`](../examples/extensions/overlay-qa-tests.ts) for positioning, stacking, focus, responsive visibility, and animation behavior.\n\n## Apply themes correctly\n\nUse the theme passed to the extension or component callback. Theme helpers produce ANSI-styled strings for semantic colors such as accent, muted text, success, warnings, errors, tool output, and Markdown.\n\nUse `theme.style()` to combine foreground and background colors with text attributes:\n\n```typescript\nreturn new Text(\n  theme.style(\"Done!\", {\n    fg: \"success\",\n    bg: \"toolSuccessBg\",\n    bold: true,\n  }),\n  0,\n  0,\n);\n```\n\nA style color can be a semantic theme token or a concrete `Color`. Foreground tokens are accepted as `fg` and background tokens as `bg`; to use a token's color in the other position, pass its concrete color, for example `{ fg: theme.colors.userMessageBg }`. Access concrete colors through `theme.colors` and use utilities such as `mixColors()` from `@earendil-works/pi-tui` when color math is needed. Tokens that a theme sets to the terminal default render with the terminal's own color; `theme.colors` reports the color the terminal announced for them, or a guess when it did not. Use `theme.appearance` (`\"dark\"` or `\"light\"`) to decide, for example, whether to lighten or darken a color. Pi converts the result to truecolor or 256-color output based on terminal capabilities. Theme tokens are converted once per theme; compute concrete colors outside the render path when possible.\n\nThe existing `theme.fg()` and `theme.bg()` helpers remain available for applying one semantic color.\n\nDo not permanently store strings with theme colors unless `invalidate()` rebuilds them. A theme change clears render caches, but it cannot remove old ANSI colors embedded in application state.\n\nTheme callbacks evaluated during rendering do not need special rebuilding. Stateless components can also calculate themed output on every render.\n\nUse [Themes](themes.md) to create terminal palettes. Use Pi’s `getMarkdownTheme()` when rendering Markdown that should match the active application theme.\n\n## Keep rendering responsive\n\nRendering runs on the interactive path. Cache expensive layout and highlighting work by width and content, then clear that cache from `invalidate()`.\n\nKeep the default view compact and reveal detail through expansion or a dedicated screen. For custom tool rendering, handle partial results and reuse the previous component when it can be updated safely.\n\nUse `PI_TUI_WRITE_LOG` to capture the raw ANSI stream when diagnosing rendering problems. Test narrow widths, wide characters, resize events, theme changes, focus transitions, and both regular and fullscreen modes.\n\n## Examples and source\n\nThe checked extension examples cover the main patterns:\n\n- [`preset.ts`](../examples/extensions/preset.ts) and [`tools.ts`](../examples/extensions/tools.ts) use selection and settings lists.\n- [`qna.ts`](../examples/extensions/qna.ts) uses cancellable asynchronous UI.\n- [`modal-editor.ts`](../examples/extensions/modal-editor.ts) replaces the editor.\n- [`custom-footer.ts`](../examples/extensions/custom-footer.ts) replaces the footer.\n- [`widget-placement.ts`](../examples/extensions/widget-placement.ts) places persistent content around the editor.\n- [`doom-overlay/`](../examples/extensions/doom-overlay/) demonstrates a continuously rendered overlay.\n\nThe public exports are defined in [`packages/tui/src/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/tui/src/index.ts). See [Extensions](extensions.md) for extension lifecycle, state, tools, events, and mode behavior.\n","packages/coding-agent/docs/usage.md":"# Use Pi in the terminal\n\nRun `pi` from the folder you want to work in. Pi uses that folder to discover files, instructions, and configuration, and to group saved sessions. If you have not installed Pi or chosen a model yet, follow the [Quickstart](quickstart.md).\n\nPi may ask whether you trust the working folder before loading its project resources. See [Project trust](security.md#understand-project-trust).\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Pi interactive mode showing a conversation, editor, and status information\" width=\"750\"></p>\n\nThe transcript shows your prompts, Pi's responses, tool calls, results, and errors. You write prompts and commands in the editor. The footer shows the current folder, session, model, context usage, and accumulated usage and cost.\n\n## Enter a prompt\n\nType a request and press `Enter` to send it. Use `Shift+Enter` to add a line, or press `Ctrl+G` to work on a longer prompt in your configured external editor.\n\nTo include files or images:\n\n- Type `@` to search for a file and add it to your prompt.\n- Press `Tab` to complete a path.\n- Paste an image or drag it into a compatible terminal.\n\n## Follow Pi's work\n\nPi shows each tool call and result while it works. Press `Ctrl+O` to expand or collapse tool output. Press `Ctrl+T` to show or hide thinking blocks.\n\nThe startup header lists the instructions and resources Pi loaded. The editor border indicates the current thinking level. The footer updates as the model uses context and reports usage.\n\nPi does not ask before every tool call. Review commands and changed files, and use a sandbox for untrusted or unattended work. See [Security](security.md).\n\n## Change direction\n\nYou can send more input while Pi is working:\n\n| What you want | Action |\n|---|---|\n| Adjust the current task | Type a message and press `Enter` |\n| Add work after the current task | Type a message and press `Alt+Enter` |\n| Return queued messages to the editor | Press `Alt+Up` |\n| Stop the current task | Press `Escape` |\n\nA message sent with `Enter` waits until the current response and its tool calls finish, then guides the next response. A follow-up sent with `Alt+Enter` waits until Pi finishes the current task. Aborting returns queued messages to the editor.\n\nWindows Terminal reserves some Alt shortcuts. See [Terminal Setup](terminal-setup.md) for the Windows alternatives.\n\n## Change the model or settings\n\nType `/` to search the available commands. The commands you will use most often are:\n\n- `/model` selects a model. Press `Ctrl+L` to open the same selector.\n- `/thinking` selects how much reasoning the current model uses. Press `Shift+Tab` to cycle through supported levels.\n- `/login` and `/logout` manage provider access.\n- `/settings` changes common preferences.\n\nPrompt templates, skills, and extensions can add more commands to the same menu. See [Choose a Model](models.md), [Configuration](configuration.md), or the complete [Slash Commands reference](slash-commands.md).\n\n## Continue or start over\n\nPi saves sessions automatically unless session persistence is disabled.\n\n- `/new` starts a new session.\n- `/resume` opens another saved session.\n- `/name` gives the current session a recognizable name.\n- `/session` shows its file, ID, message count, token usage, and cost.\n\nUse `/tree`, `/fork`, or `/clone` when you want to explore another approach without losing existing work. Use `/compact` to reduce the conversation history sent to the model. See [Sessions and Context](sessions.md) for these workflows.\n\nAfter leaving Pi, run `pi --continue` from the same folder to resume its most recent session.\n\n## Run a terminal command\n\nPrefix a command with `!` to run it and include its output in the conversation:\n\n```text\n!git status\n```\n\nUse `!!` when you want to run a command without sending its output to the model.\n\n## Copy, export, or share results\n\nPress `Ctrl+X` or run `/copy` to copy the last assistant response. Use `/export` to save the session as HTML or JSONL.\n\nUse `/share` to upload the session and get a viewer link. By default, Radius authentication makes the artifact visible to your Radius organization; otherwise, Pi creates a private GitHub gist through the GitHub CLI. The [`sessionShareDestination`](settings.md#sessions-and-context) setting can force either destination. Review the session first because it can contain prompts, tool output, file contents, and credentials exposed during the conversation.\n\n## Adjust the terminal\n\nFullscreen mode, the default, keeps the editor and status area fixed while the transcript scrolls within the terminal window. Regular mode uses the terminal's normal scrollback. Choose a mode through `/settings` or `--tui-mode`.\n\nTerminal support for mouse input, keyboard shortcuts, and inline images varies. See [Terminal Setup](terminal-setup.md) for platform-specific configuration and [Keybindings](keybindings.md) for every configurable shortcut. Run `/hotkeys` to inspect the shortcuts active in your current session.\n\n## Collect diagnostic information\n\nWhen troubleshooting terminal rendering or conversation state, run `/debug`. Pi writes the rendered terminal lines and current session messages to `pi-debug.log` in your [agent directory](configuration.md#agent-directory).\n\nReview this file before sharing it. It can contain prompts, model responses, tool output, file contents, and terminal data.\n","packages/coding-agent/docs/virtual-models.md":"# Virtual Models\n\nA virtual model is a selectable model that picks a physical model for each request. Use one to route by task, cost, or conversation state. For example, a router can send quick questions to a small model and hard problems to a large one, while the user selects a single model.\n\nRegister virtual models from an [extension](extensions.md). They appear in `/model`, `--model`, scoped models, and settings like any other model. A virtual model can be listed under any provider, including one with physical models, such as `openai-codex/auto`.\n\n## Selection and dispatch\n\nA virtual model selects a model and a thinking level. A router maps that pair to a physical pair for each request:\n\n```\nselected (virtual model, virtual level)  ->  dispatched (physical model, physical level)\njev/auto:low                             ->  anthropic/claude-sonnet-4-5:high\n```\n\nThe virtual thinking level is an input to the router. Its meaning is up to the router; it need not correspond to a reasoning budget.\n\nPi keeps the two pairs apart:\n\n| | Selection | Dispatch |\n|---|---|---|\n| Recorded in | `model_change` and `thinking_level_change` entries | Each assistant message: `provider`, `api`, `model`, `thinkingLevel` |\n| Visible as | `ctx.model`, `ctx.thinkingLevel`, `PI_MODEL`, `PI_REASONING_LEVEL`, `/model` | The assistant message of each response |\n\nProviders only receive physical models. Assistant messages name the physical model, so replaying a conversation across different physical models works the same as after a manual model switch. Resuming a session restores the virtual selection from its latest `model_change` entry. If the virtual model is no longer registered, Pi falls back to the physical model that answered last.\n\nIn interactive mode, the footer shows the routed model next to the selection, for example `auto • high → gpt-5.6-luna • medium`. `/session` lists the cost for each physical model.\n\nContext usage uses the limits of the physical model that produced the latest response, even if that response came before switching to the virtual model. Without such a response, it uses the limits declared on the virtual model, if any. Compaction checks the same limits, and again the limits of the model each request is routed to. If that model's context window is too small for the conversation, Pi compacts before sending the request; the route stays as the router chose it.\n\n## Register a virtual model\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerVirtualModel({\n    provider: \"router\",\n    id: \"auto\",\n    name: \"Auto\",\n    thinkingLevels: [\"low\", \"high\"],\n    route(request, ctx) {\n      // Tool follow-ups and retries stay on the model that handled the turn.\n      const sticky = request.failed ?? request.previous;\n      if (request.reason !== \"user\" && sticky) {\n        return { model: sticky.model, thinkingLevel: sticky.thinkingLevel ?? \"medium\" };\n      }\n      const id = request.thinkingLevel === \"high\" ? \"claude-sonnet-4-5\" : \"claude-haiku-4-5\";\n      return { model: ctx.modelRegistry.find(\"anthropic\", id)!, thinkingLevel: \"medium\" };\n    },\n  });\n}\n```\n\n- `provider` is the provider the model is listed under. It can be any provider ID. A provider can list several virtual models next to its physical ones. On a physical provider, the virtual model is available when that provider has credentials. Under an ID that no provider uses, it is always available.\n- `id` must not be the ID of a physical model of that provider. If a catalog refresh later adds a physical model with the same ID, the virtual model hides it.\n- `thinkingLevels` lists the levels offered for selection. It defaults to `[\"off\"]`.\n- `contextWindow` and `maxTokens` are shown before the first response. Unset limits are unknown.\n- `input` lists the input types offered for selection. It defaults to text and images; physical models without image support receive placeholders.\n\nRegistration follows the same queuing and reload rules as `pi.registerProvider()`. Registering the same provider and ID again replaces the virtual model. `pi.unregisterVirtualModel(provider, id)` removes it; `pi.unregisterProvider()` does not. SDK code can register one without an extension: `modelRuntime.registerVirtualModel(definition)`.\n\n## Route requests\n\n`route(request, ctx)` runs before every request made with the virtual model and returns `{ model, thinkingLevel }`. The model can be any physical model in the catalog whose provider has credentials; look it up with `ctx.modelRegistry`. A virtual model cannot route to another virtual model. Pi clamps the thinking level to the returned model.\n\n| Field | Meaning |\n|---|---|\n| `model`, `thinkingLevel` | The selected virtual model and level |\n| `reason` | Why the request is made, see below |\n| `previous` | Physical model and thinking level of the latest successful response in `messages` |\n| `failed` | For `retry`: physical model, thinking level, and assistant `message` of the failed request, which `messages` no longer contains. The message carries `stopReason` and `errorMessage`. Absent when routing itself failed |\n| `state` | Router state last returned on this session branch, see below |\n| `messages` | The conversation for this request, including system messages |\n| `signal` | Abort signal of the request |\n\n| `reason` | Request |\n|---|---|\n| `user` | First request after a message the user wrote, including steering and follow-up messages |\n| `continuation` | Any other request in the agent loop, such as after tool results or extension messages |\n| `retry` | Automatic retry after a failed request, including after compaction for a context overflow |\n| `direct` | Request made outside the agent loop, such as a compaction summary or an extension calling `ctx.modelRegistry.streamSimple()` |\n\nReturning `previous` for `continuation` and `failed` for `retry` keeps prompt caches and thinking signatures valid. Switching models between turns is allowed but loses the prompt cache. A retry can also switch to another model, for example when `failed.message.errorMessage` reports that a provider is overloaded or the context overflowed.\n\nIf `route()` throws, or returns a virtual model or a model without credentials, the request ends with an error response.\n\n## Keep routing state\n\n`route()` can return `state` next to the model. Pi stores it on the session branch and passes it back as `request.state` on later requests. Use it for decisions the transcript does not record, such as classifier results or a routing phase:\n\n```typescript\npi.registerVirtualModel<{ phase: \"plan\" | \"build\" }>({\n  provider: \"router\",\n  id: \"phased\",\n  name: \"Phased\",\n  route(request, ctx) {\n    const state = request.state ?? { phase: \"plan\" };\n    const id = state.phase === \"plan\" ? \"claude-opus-4-5\" : \"claude-haiku-4-5\";\n    return { model: ctx.modelRegistry.find(\"anthropic\", id)!, thinkingLevel: \"medium\", state };\n  },\n});\n```\n\n- State must be JSON-serializable. Returning `undefined` or `request.state` itself keeps the current state.\n- Pi stores any other returned object as new state, before the request is sent, even when it equals the current state. Return a new object only when the state changes. The state stays stored if the request later fails.\n- State follows the session tree, so forks and `/tree` navigation see the state of their branch. It survives compaction.\n- `direct` requests have no state, and Pi ignores state they return.\n\nThe transcript already records the selection and every dispatched model, and `ctx.sessionManager.getBranch()` exposes both.\n\nRouters can call other models through `ctx.modelRegistry`, for example `ctx.modelRegistry.classify()` with a classifier model from `ctx.modelRegistry.findOfType(\"classifier\", provider, id)`. The call adds latency before the first token of the turn.\n\nSee [`jev-router.ts`](../examples/extensions/jev-router.ts) for a complete router. It plans on a strong OpenAI Codex model chosen by the Jev classifier, lets that model make the first edit, and then switches once to a cheaper model, accepting a single prompt-cache miss. It keeps the phase as router state.\n","packages/coding-agent/docs/windows.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fdocs%2Fwindows.md -->","packages/coding-agent/examples/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2FREADME.md -->","packages/coding-agent/examples/extensions/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2FREADME.md -->","packages/coding-agent/examples/extensions/doom-overlay/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fdoom-overlay%2FREADME.md -->","packages/coding-agent/examples/extensions/dynamic-resources/SKILL.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fdynamic-resources%2FSKILL.md -->","packages/coding-agent/examples/extensions/plan-mode/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fplan-mode%2FREADME.md -->","packages/coding-agent/examples/extensions/subagent/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fsubagent%2FREADME.md -->","packages/coding-agent/examples/sdk/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fsdk%2FREADME.md -->","packages/coding-agent/test/mcp-conformance/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Ftest%2Fmcp-conformance%2FREADME.md -->","packages/coding-agent/test/suite/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Ftest%2Fsuite%2FREADME.md -->","packages/durable/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fdurable%2FREADME.md -->","packages/durable/docs/spec.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fdurable%2Fdocs%2Fspec.md -->","packages/env/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2FREADME.md -->","packages/env/docs/protocol.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2Fdocs%2Fprotocol.md -->","packages/env/docs/semantics.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2Fdocs%2Fsemantics.md -->","packages/evals/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fevals%2FREADME.md -->","packages/mcp/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fmcp%2FREADME.md -->","packages/telemetry/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftelemetry%2FREADME.md -->","packages/tui/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2FREADME.md -->","packages/tui/native/darwin/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Fdarwin%2FREADME.md -->","packages/tui/native/linux/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Flinux%2FREADME.md -->","packages/tui/native/win32/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Fwin32%2FREADME.md -->"},"files":{".pi/skills/add-llm-provider.md":"---\nname: add-llm-provider\ndescription: Checklist for adding a new LLM provider to packages/ai. Covers core types, provider implementation, lazy registration, model generation, the full test matrix, coding-agent wiring, and docs.\n---\n\n# Adding a New LLM Provider (packages/ai)\n\nA new provider touches multiple files. Work through these steps in order.\n\n## 1. Core Types (`packages/ai/src/types.ts`)\n\n- Add API identifier to `Api` type union (e.g. `\"bedrock-converse-stream\"`).\n- Create options interface extending `StreamOptions`.\n- Add mapping to `ApiOptionsMap`.\n- Add provider name to `KnownProvider` type union.\n\n## 2. Provider Implementation (`packages/ai/src/providers/`)\n\nCreate a provider file exporting:\n\n- `stream<Provider>()` returning `AssistantMessageEventStream`.\n- `streamSimple<Provider>()` for `SimpleStreamOptions` mapping.\n- Provider-specific options interface.\n- Message/tool conversion functions.\n- Response parsing that emits standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`).\n\n## 3. Provider Exports and Lazy Registration\n\n- Add a package subpath export in `packages/ai/package.json` pointing at `./dist/providers/<provider>.js`.\n- Add `export type` re-exports in `packages/ai/src/index.ts` for provider option types that should remain available from the root entry.\n- Register the provider in `packages/ai/src/providers/register-builtins.ts` via lazy loader wrappers; do not statically import provider implementation modules there.\n- Add credential detection in `packages/ai/src/env-api-keys.ts`.\n\n## 4. Model Generation (`packages/ai/scripts/generate-models.ts`)\n\n- Add logic to fetch/parse models from the provider source.\n- Map to the standardized `Model` interface.\n\n## 5. Tests (`packages/ai/test/`)\n\n- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API impl such as `openai-completions`.\n- Add the provider to the broader matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.\n- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (e.g. GPT and Claude), add at least one pair per family.\n- For non-standard auth, create a utility (e.g. `bedrock-utils.ts`) with credential detection.\n\n## 6. Coding Agent (`packages/coding-agent/`)\n\n- `src/core/model-resolver.ts`: add default model ID to `defaultModelPerProvider`.\n- `src/core/provider-display-names.ts`: add API-key login display name so `/login` and related UI show the provider for built-in API-key auth.\n- `src/cli/args.ts`: add env var documentation.\n- `README.md`: add provider setup instructions.\n- `docs/providers.md`: add setup instructions, env var, and `auth.json` key.\n\n## 7. Documentation\n\n- `packages/ai/README.md`: add to providers table, document options/auth, add env vars.\n- `packages/ai/CHANGELOG.md`: add entry under `## [Unreleased]`.\n",".pi/skills/github-work.md":"---\nname: github-work\ndescription: Work on GitHub issues and pull requests of this repo. Use when asked to investigate, analyze, fix, or review an issue or PR, and when asked to wrap up such work (changelog, comment, commit, push, close).\n---\n\n# Working on GitHub issues and PRs\n\nThe repo's prompt templates in `.pi/prompts/` (two directories above this skill) define the workflow. Read the matching template in full and follow it. Where a template says `$ARGUMENTS` or `$@`, substitute the issue or PR number or URL from the request, and any extra instructions the user gave.\n\n| Request | Template |\n|---|---|\n| Investigate, analyze, or fix an issue | `.pi/prompts/is.md` |\n| Review a pull request | `.pi/prompts/pr.md` |\n| Wrap up issue or PR work: changelog, final comment, commit, push, close | `.pi/prompts/wr.md` |\n\nRules:\n\n- `is.md` analyzes and proposes only. Implement only when the user asks for it, then keep following AGENTS.md (checks, tests, changelog rules).\n- `pr.md` never checks out or switches to the PR branch.\n- Run `wr.md` only when the user asks to wrap up, commit, or push. Post at most one final GitHub comment per session, ending with the exact disclaimer line it specifies.\n- When a request spans several steps, for example \"fix issue X and wrap it up\", read every template involved before starting.\n",".pi/skills/interactive-testing.md":"---\nname: interactive-testing\ndescription: Test and debug pi's interactive mode in a controlled tmux terminal. Use for TUI behavior checks and interactive release smoke tests.\n---\n\n# Testing pi Interactive Mode with tmux\n\nRun the TUI in a controlled terminal (from the repo root, two directories above this skill):\n\n```bash\ntmux new-session -d -s pi-test -x 80 -y 24\ntmux send-keys -t pi-test \"./pi-test.sh\" Enter\nsleep 3 && tmux capture-pane -t pi-test -p     # capture after startup\ntmux send-keys -t pi-test \"your prompt here\" Enter\ntmux send-keys -t pi-test Escape               # special keys (also C-o for ctrl+o, etc.)\ntmux kill-session -t pi-test\n```\n\nFor release smoke tests, start the tmux session with `-c /tmp` and replace `./pi-test.sh` with the absolute path to the release binary. Test both Node and Bun binaries separately, submit a prompt, and wait for the model reply; startup alone is not a passing smoke test.\n",".pi/skills/release.md":"---\nname: release\ndescription: Prepare, publish, verify, and recover pi releases. Use for release preparation, local release smoke tests, publishing, and failed release CI or announcements.\n---\n\n# Releasing pi\n\nRun repository commands from the repo root (two directories above this skill), unless instructed otherwise.\n\n**Lockstep versioning**: all packages share one version; every release updates all together. `patch` = fixes + additions, `minor` = breaking changes. No major releases.\n\n1. **Update CHANGELOGs**: ask the user whether they ran the `/cl` prompt on the latest commit on `main`. If not, they must run `/cl` first to audit and update each package's `[Unreleased]` section before releasing.\n\n2. **Local smoke test**: build an unpublished release and smoke test from outside the repo (so it can't resolve workspace files):\n   ```bash\n   npm run release:local -- --out /tmp/pi-local-release --force\n   cd /tmp\n\n   # Node package install smoke tests\n   /tmp/pi-local-release/node/pi --help\n   /tmp/pi-local-release/node/pi --version\n   /tmp/pi-local-release/node/pi --list-models\n   /tmp/pi-local-release/node/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/node/pi\n\n   # Bun binary smoke tests\n   /tmp/pi-local-release/bun/pi --help\n   /tmp/pi-local-release/bun/pi --version\n   /tmp/pi-local-release/bun/pi --list-models\n   /tmp/pi-local-release/bun/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/bun/pi\n   ```\n   Verify both Node and Bun startup, model/account listing, interactive startup, and at least one real prompt with the intended default provider. The bare commands `/tmp/pi-local-release/node/pi` and `/tmp/pi-local-release/bun/pi` start interactive mode; run each in tmux, submit a prompt, and wait for the model reply before considering the interactive smoke test passed. Failures are release blockers unless the user explicitly accepts the risk.\n\n   Load and follow [interactive-testing.md](interactive-testing.md) for the tmux workflow. Start each release binary from `/tmp`, not the repo root.\n\n3. **Run the release script**:\n   ```bash\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:patch    # fixes + additions\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:minor    # breaking changes\n   ```\n   If the test suite already passed in `npm run release:local` and only fails from machine load, `PI_RELEASE_SKIP_TESTS=1` skips `./test.sh` in the release script; checks and the packed install check still run. Use it only with the user's approval.\n\n   Use `npm_config_min_release_age=0` only for the release command. The repo's normal npm age gate can otherwise block the release lockfile refresh when the current workspace package version was published recently. Review any lockfile or install lock diffs the release creates before push.\n\n   The release script pins the newest published model catalog for Nix (`nix/model-catalog.json`) and builds the checkout with it as the Nix build does (it fails if that catalog predates main's model changes; publish one first), bumps all package versions, updates changelogs, regenerates release artifacts, runs `npm run check`, commits `Release vX.Y.Z`, tags `vX.Y.Z`, adds fresh `## [Unreleased]` changelog sections, commits `Add [Unreleased] section for next cycle`, then pushes `main` and the tag. Do not rerun the release script after a tag was pushed.\n\n4. **CI verifies and announces the npm release**: pushing the `vX.Y.Z` tag triggers `.github/workflows/build-binaries.yml`. The `publish-npm` job uses npm trusted publishing through GitHub Actions OIDC with environment `npm-publish`; no local `npm publish`, `npm whoami`, OTP, or WebAuthn flow is required. After publishing, `announce-pi-dev-release` verifies every public workspace package resolves at the exact release version and that its npm tarball is available, then writes the verified release marker to R2. `pi.dev/api/latest-version` reads that marker; it must never announce a release from npm before this job succeeds.\n\n5. **CI builds the Nix release**: the tag push also triggers `.github/workflows/nix.yml`, which builds the tagged flake on Linux and macOS and then fast-forwards the `stable` branch to the release commit. `nix run github:earendil-works/pi/stable` runs the latest release. If the Nix build fails, `stable` stays on the previous release; the tag cannot be fixed, so fix `main` and ship the fix in the next release.\n\n6. **If CI publish or announcement fails**: inspect the failed job. The publish helper is idempotent and skips package versions already present on npm; the announcement job rechecks availability before updating the R2 marker. Rerun the failed job or workflow after fixing CI or transient npm issues. Do not rerun `npm run release:patch` or `npm run release:minor` for the same version.\n","AGENTS.md":"# Development Rules\n\n## Conversational Style\n\n- Keep answers short and concise\n- No emojis in commits, issues, PR comments, or code\n- No fluff or cheerful filler text (e.g., \"Thanks @user\" not \"Thanks so much @user!\")\n- Technical prose only, be direct\n- Use concise, clear, simple language. Define unavoidable jargon before using it.\n- Explain non-trivial designs and problems as: problem, concrete example or short trace, then solution. State why the solution is necessary and distinguish it from optional complexity.\n- Prefer concrete behavior and small illustrations over abstract summaries, dense terminology, or unexplained lists of changes.\n- When the user asks a question, answer it first before making edits or running implementation commands.\n- When responding to user feedback or an analysis, explicitly say whether you agree or disagree before saying what you changed.\n\n## Code Quality\n\n- Read files in full before wide-ranging changes, before editing files you have not fully inspected, and when asked to investigate or audit. Do not rely on search snippets for broad changes.\n- No `any` unless absolutely necessary.\n- Inline single-line helpers that have only one call site.\n- Check node_modules for external API types; don't guess.\n- **No inline imports** (`await import()`, `import(\"pkg\").Type`, dynamic type imports). Top-level imports only.\n- In `packages/coding-agent`, resolve package assets through helpers in `src/config.ts`. Do not use `__dirname` directly; the helpers account for source checkouts, npm installations, and standalone binaries.\n- Never remove or downgrade code to fix type errors from outdated deps; upgrade the dep instead.\n- Use only erasable TypeScript syntax (Node strip-only mode) in code checked by the root config (`packages/*/src`, `packages/*/test`, `packages/coding-agent/examples`): no parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other constructs needing JS emit. Use explicit fields with constructor assignments.\n- Always ask before removing functionality or code that appears intentional.\n- Do not preserve backward compatibility unless the user asks for it.\n- Never hardcode key checks (e.g. `matchesKey(keyData, \"ctrl+x\")`). Add defaults to `DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS` so they stay configurable.\n- Never modify `packages/ai/src/models.generated.ts` directly; update `packages/ai/scripts/generate-models.ts` instead, then regenerate. Including the resulting `models.generated.ts` diff is always OK, even if regeneration includes unrelated upstream model metadata changes. The generated model data in `packages/ai/src/providers/data/` is gitignored: never stage it, and don't look for it in git history.\n\n## Commands\n\n- After code changes (not docs): `npm run check` (full output, no tail). Fix all errors, warnings, and infos before committing. Does not run tests.\n- Never run `npm run build` or `npm test` unless requested by the user.\n- Never run the full vitest suite directly: it includes e2e tests that activate when endpoint/auth env vars are present. For all non-e2e tests, run `./test.sh` from the repo root. Otherwise run specific tests from the package root:\n  - Vitest: `node \"$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js\" --run test/specific.test.ts`\n  - `packages/tui` (`node:test`): `node --test test/specific.test.ts`\n- If you create or modify a test file, run it and iterate on test or implementation until it passes.\n- For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` + the faux provider. No real provider APIs, keys, or paid tokens.\n- When regressions tests for fixing a github issue, add a comment with the github issue number next to the test.\n- For ad-hoc scripts, `write` them to a temp file (e.g. `/tmp`), run, edit if needed, remove when done. Don't embed multi-line scripts in `bash` commands.\n- Never commit unless the user asks.\n\n## Dependency and Install Security\n\n- Treat npm dep and lockfile changes as reviewed code. Direct registry deps, including internal workspace deps, stay pinned to exact versions.\n- When updating `undici`, you MUST read its changelog/release notes for the target version and evaluate whether any changes may affect functionality before applying the update.\n- Hydrate/update locally with `npm install --ignore-scripts`; clean/CI-style with `npm ci --ignore-scripts`. Don't run lifecycle scripts unless the user asks.\n- If dep metadata changes, refresh `package-lock.json` with `npm install --package-lock-only --ignore-scripts`.\n- If `packages/coding-agent/install-lock/` needs regen, run `node scripts/generate-coding-agent-install-lock.mjs` (verify with `--check` or `npm run check`). New deps with lifecycle scripts require review and an explicit allowlist entry in that script; never add one silently.\n- Pre-commit blocks lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1`. Don't bypass unless the user wants the lockfile change committed.\n\n## Git\n\nMultiple pi sessions may be running in this cwd at the same time, each modifying different files. Git operations that touch unstaged, staged, or untracked files outside your own changes will stomp on other sessions' work. Follow these rules:\n\nCommitting:\n\n- Only commit files YOU changed in THIS session.\n- Stage explicit paths (`git add <path1> <path2>`); never `git add -A` / `git add .`.\n- Before committing, run `git status` and verify you are only staging your files.\n- `packages/ai/src/models.generated.ts` may always be included alongside your files.\n- Message format: `{feat,fix,docs}[(ai,tui,agent,coding-agent)]: <commit message> (optionally multiple lines)`. Message is informative and concise.\n\nNever run (destroys other agents' work or bypasses checks):\n\n- `git reset --hard`, `git checkout .`, `git clean -fd`, `git stash`, `git add -A`, `git add .`, `git commit --no-verify`.\n\nIf rebase conflicts occur:\n\n- Resolve conflicts only in files you modified.\n- If a conflict is in a file you did not modify, abort and ask the user.\n- Never force push.\n\n## Issues and PRs\n\nSee `CONTRIBUTING.md` for the contributor gate (auto-close workflows, `lgtm`/`lgtmi`, quality bar).\n\nWhen reviewing PRs:\n\n- Do not run `gh pr checkout`, `git switch`, or otherwise move the worktree to the PR branch unless the user explicitly asks.\n- Use `gh pr view`, `gh pr diff`, `gh api`, and local `git show`/`git diff` against fetched refs to inspect PR metadata, commits, and patches without changing branches.\n- If you need PR file contents, fetch/read them into temporary files or use `git show <ref>:<path>` without switching branches.\n\nWhen creating issues:\n\n- Add `pkg:*` labels for affected packages (`pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui`); use all that apply.\n\nWhen posting issue/PR comments:\n\n- Write the comment to a temp file and post with `gh issue/pr comment --body-file` (never multi-line markdown via `--body`).\n- Keep comments concise, technical, in the user's tone.\n- End every AI-posted comment with the AI-generated disclaimer line specified by the originating prompt (e.g. `This comment is AI-generated by `/wr``).\n\nWhen closing issues via commit:\n\n- Include `fixes #<number>` or `closes #<number>` in the message so merging auto-closes the issue. For multiple issues, repeat the keyword per issue (`closes #1, closes #2`); a shared keyword (`closes #1, #2`) only closes the first.\n\n## Testing pi Interactive Mode with tmux\n\nFor testing pi's interactive mode, load and follow [.pi/skills/interactive-testing.md](.pi/skills/interactive-testing.md).\n\n## Changelog\n\nLocation: `packages/*/CHANGELOG.md` (one per package).\n\nSections under `## [Unreleased]`: `### Breaking Changes` (API changes requiring migration), `### Added`, `### Changed`, `### Fixed`, `### Removed`.\n\nRules:\n\n- All new entries go under `## [Unreleased]`. Read the full section first and append to existing subsections; never duplicate them.\n- Released version sections (e.g. `## [0.12.2]`) are immutable; never modify them.\n- Do not create changelog entries when working on a branch other than `main` or pull request\n\nAttribution:\n\n- Internal (from issues): `Fixed foo bar ([#123](https://github.com/earendil-works/pi/issues/123))`\n- External contributions: `Added feature X ([#456](https://github.com/earendil-works/pi/pull/456) by [@username](https://github.com/username))`\n\n## Releasing\n\nFor release preparation, publishing, verification, or recovery, load and follow [.pi/skills/release.md](.pi/skills/release.md).\n\n## User Override\n\nIf the user's instructions conflict with any rule in this document, ask for explicit confirmation before overriding. Only then execute their instructions.\n","packages/agent/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fagent%2FREADME.md -->","packages/ai/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fai%2FREADME.md -->","packages/chord/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fchord%2FREADME.md -->","packages/chord/docs/guide.md":"# Chord usage guide\n\n> Describes the runtime in `src`. The [specification](spec.md) defines its semantics and lists known limitations.\n\nChord builds applications out of **objects** that live in several processes (a server, workers, browser tabs) and can be **hot reloaded** while they run.\n\n- An **object type** is declared by a contract: a name, an input (the object's address) and members. A plugin's\n  **facet** (its code for one host: server, worker, browser) serves object types.\n- An **object** is one instance of an object type: state plus code at an address. It is created on demand and lives\n  while someone references it.\n- Everyone gets a **reference**, never the object. References work the same locally and across processes, and survive reloads.\n- Every object knows its **principal**: an owner and a guest get different objects, with different methods.\n- **State** is observable JSON. Remote viewers get a snapshot followed by deltas.\n- **Hot reload** swaps code while it runs. Work in flight finishes on the code it started with, state is kept, and incompatible changes migrate at a safe point.\n\n## Contents\n\n- [Quick start](#quick-start)\n- [Contracts](#contracts)\n- [Serving objects](#serving-objects)\n- [Declarations](#declarations)\n- [Activation](#activation)\n- [Principals and variants](#principals-and-variants)\n- [References](#references)\n- [State](#state)\n- [Calls, ctx and roots](#calls-ctx-and-roots)\n- [Lifetime](#lifetime)\n- [Registries](#registries)\n- [Plugins](#plugins)\n- [Hot reload](#hot-reload)\n- [Hosts and links](#hosts-and-links)\n- [Durable](#durable)\n- [Errors](#errors)\n- [Rules of thumb](#rules-of-thumb)\n\n## Quick start\n\n```ts\nimport { Type } from \"typebox\";\nimport { createHost, method, remote } from \"@earendil-works/chord\";\n\n// contract.ts: shared by provider and consumers\nexport const Counter = remote(\"demo.counter\", {\n\tversion: 1,\n\tinput: Type.Object({ name: Type.String() }),\n\tvariants: {\n\t\tdefault: {\n\t\t\tstate: { count: Type.Number() },\n\t\t\tmethods: { increment: method(Type.Object({ by: Type.Number() })) },\n\t\t},\n\t},\n});\n\nconst host = createHost({ name: \"server\", self: { kind: \"server\" } });\n\nhost.serve(Counter, ({ name }, _principal, self) => {\n\tconst count = self.state(\"count\", Type.Number(), 0);\n\treturn {\n\t\tvariant: \"default\",\n\t\tcount,\n\t\tincrement: async ({ by }, ctx) => {\n\t\t\tcount.change(ctx, (n) => n + by);\n\t\t},\n\t};\n});\n\nawait host.root(async (ctx) => {\n\tconst clicks = await ctx.get(Counter, { name: \"clicks\" });  // ready, with its state; released when this root ends\n\tclicks.count.subscribe((n) => console.log(\"count\", n));\n\tawait clicks.increment({ by: 2 }, ctx);  // logs 0, then 2\n});\n```\n\n## Contracts\n\nA contract is a runtime value: a name, a version number, and a description of the members. Shipped bundles have no TypeScript, so everything the runtime checks must be in the contract. TypeScript types are inferred from it.\n\n### Remote types\n\nRemote types can be reached from other hosts. Everything that crosses the wire has a schema, and the provider validates it.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),  // lives on `session:S1` for `{ session: \"S1\", … }`\n\tvariants: {\n\t\tviewer: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t},\n\t\tparticipant: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t\tmethods: {\n\t\t\t\tsend: method(SendInputSchema, Type.Object({ submissionId: Type.Number() })),\n\t\t\t\tstop: method(),  // no arguments, no result: `chat.stop(ctx)`\n\t\t\t},\n\t\t},\n\t},\n});\n```\n\n**Where a remote type lives** is part of its contract, so no caller names a host:\n\n- `host: hostNamed(\"server\")`: every object lives on the host named `server`.\n- `host: hostFamily(\"session:\", \"session\")`: an object lives on `session:` + its input's `session` field, a required\n  string. TypeScript checks the field name against the input.\n\nA `get` normalizes the input (defaults count), resolves the host, and acquires the object here if that is this host,\notherwise over the link to it. `serve` refuses a type whose placement does not include the serving host, so a misplaced\ntype fails at startup or at the reload that adds it. The host that gets a hold checks that its own contract places the\ninput on itself, and a relay checks the hold's target against its contract token: a client never chooses where an\nobject lives. The placement is part of the description and the fingerprint: moving a type is a breaking change, and\nreferences to it get `upgrade_required`.\n\n### Local types\n\nLocal types are reachable only inside their host. Their members may be anything: functions, callbacks, native objects. Non-JSON members get opaque or function descriptors.\n\n```ts\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(HitSchema)),\n\t\tonChange: method(Type.Function([ChangeSchema], Type.Void()), UnsubscribeSchema),\n\t\tharness: opaque(\"pi.durable-harness@1\"),\n\t},\n});\n```\n\n### Inputs identify objects\n\n`{ session: \"S1\", conversation: 1 }` always means the same object. Inputs are normalized (defaults applied, unknown fields removed), validated, and serialized canonically. Fields equal to their default are left out, so adding a new optional field with a default doesn't change existing addresses. Changing a default, or how inputs are normalized, needs a new type name.\n\n### Versions and compatibility\n\nEvery bundle carries its own copy of each contract it was built against. When a consumer binds to a provider, the two copies are compared:\n\n> A consumer built against version M binds to a provider at version N if **N = M and the provider's description covers the consumer's**. Otherwise the provider must offer an adapter in `accepts[M]`, or binding fails with `contract_mismatch`.\n\n\"Covers\" means the provider still offers everything the consumer was built against:\n- every member exists with the same kind;\n- methods accept everything the consumer can send (new arguments must be optional) and return at least what it expects (extra result fields are fine);\n- state values fit the consumer's schema;\n- opaque members have the same name and version.\n\n| Change | Version | Result |\n| --- | --- | --- |\n| Add a method, an optional argument, a result field, a state field | keep | binds |\n| Remove or rename a member; make an argument required; change a type | **bump** | old consumers need `accepts`, or must be reloaded too |\n| Same shape, different meaning (`query` is now a regular expression) | **bump** | as above; undetectable, so it's on you |\n| Forgot to bump on a breaking shape change | keep | refused: \"consumer needs member `search`\" |\n\n```ts\n// version 2 changed what `search` means; old consumers keep working through an adapter\nhost.serve(Index, indexFactory, {\n\taccepts: {\n\t\t1: (current) => ({ search: ({ query }, ctx) => current.search({ query: escapeRegExp(query) }, ctx) }),\n\t},\n});\n```\n\n## Serving objects\n\n```ts\nhost.serve(Type, (input, principal, self) => implementation, options);\n```\n\n- The **factory** runs synchronously. It declares what the object needs and returns its implementation: methods and state members, plus `variant` for remote types. Returning a promise throws.\n- There is one **object** per `(type, input, principal)`. It holds the object's state, resources and references, and survives reloads. Only the implementation is replaced.\n- **Options:**\n  - `accepts`: adapters for consumers built against older contract versions;\n  - `lifetime`: `\"referenced\"` (default) creates the object at its first `get` and disposes it after the last reference\n    is released; `\"host\"` creates it when the plugin loads and keeps it until the host closes (see [Lifetime](#lifetime));\n  - `linger`: how long to keep a referenced object after the last reference is released.\n\n## Declarations\n\nA reload runs the factory again over the same object. Declarations that keep something across a reload have a **name**: the new code's `status` cell is the old code's `status` cell, with its value. Derivations and tasks hold nothing across a reload and have no name.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 });\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async ({ signal }) => openSqlite(statusPath(session), { signal }),\n\t\tclose: async (db) => db.close(),\n\t});\n\tself.on(\"catalogChanges\", catalog.rows, (rows, ctx) => errors.replace(ctx, checkRows(rows)));\n\tself.task(async (ctx) => {\n\t\twhile (!ctx.signal.aborted) {\n\t\t\tawait sleep(10_000, ctx.signal);\n\t\t\tawait db.value.run(\"INSERT INTO history VALUES (?)\", [JSON.stringify(status.value)]);\n\t\t}\n\t});\n\tconst summary = self.derive([status, errors], (s, e) => ({ ...s, failing: e.length > 0 }));\n\treturn { summary, report: reportMethod(status) };  // exposed state is what the implementation returns\n});\n```\n\n| Declaration | What it is | On reload |\n| --- | --- | --- |\n| `self.state(name, schema, initial \\| { init }, { version?, migrate? })` | an observable JSON cell; the schema is required | same cell and value (`init` does not run); migrated when its version rises |\n| `self.get(Type, input, { at? })` | a reference to another object; its identity is the address | kept if declared again, otherwise released |\n| `self.resource(name, { version, deps?, exclusive?, open, close? })` | a connection, watch, socket, port | kept; reopened only if `version`, `deps` or `exclusive` change |\n| `self.on(name, source, handler)` or `self.on(name, source, select, handler)` | reacts to state or to an event source | the same subscription, with the new handler, as long as the source and `select` are unchanged |\n| `self.task(async (ctx) => ...)` | long-running background work | the old run is aborted through `ctx.signal`; the new code starts once it finished (at most `taskStopDeadline`, default 5 s) |\n| `self.derive(sources, fn)` | live projection of state; `fn` is synchronous | a new derivation, computed when the reload commits |\n| `self.register(Registry, { key: value })` | an entry in a registry | replaced at commit |\n| `self.expose(resource)` | a member backed by a resource (local types) | follows the resource |\n\nNames are strings: declaring one twice in a run fails (`invalid/duplicate`). Each kind has its own names, so a cell and a resource may share one.\n\nA listener keeps its subscription across reloads when it is declared on the same resource (or source) with the same `select`: the same function, or one with the same source text. `select` is not called again, so a source that accepts a single subscriber (a Durable watch) survives reloads, and `select` may wrap the value in a new adapter object. A `select` whose result depends on captured values that change between reloads is not noticed; give such a listener a new name.\n\n**A listener on state reconciles.** A listener on state (a cell, a derivation, a reference's state member) gets the current value when it starts, then every new value. When a reload replaces its source (a derivation is new at every reload), it subscribes again and gets the current value again. Write its handler to bring things in line with the value it gets (set, compare, start or stop what the value says), not to count or append per call. An event source (a resource's events) delivers no current value.\n\nA name you no longer declare is released, closed or stopped, after the old code has finished what it was doing.\n\n**State that must survive a reload lives in cells,** never in closure variables. A closure variable lasts as long as one version of the code.\n\n```ts\n// wrong: reset on every reload\nhost.serve(Visits, (_, _p, self) => {\n\tlet visits = 0;\n\treturn { hit: async (ctx) => ++visits };\n});\n\n// right\nhost.serve(Visits, (_, _p, self) => {\n\tconst visits = self.state(\"visits\", Type.Number(), 0);\n\treturn {\n\t\thit: async (ctx) => {\n\t\t\tvisits.change(ctx, (n) => n + 1);\n\t\t\treturn visits.value;\n\t\t},\n\t};\n});\n```\n\n**Methods** are declared with `method(args?, result?)` in the contract. Without `args` a method takes none: callers write `ref.hit(ctx)` (or `ref.hit()`), and its implementation is `(ctx) => …`. Without `result` it returns nothing: the implementation returns nothing, and callers get `undefined`. `method(undefined, Type.Number())` takes no arguments and returns a number.\n\n## Activation\n\nThe factory is synchronous, but activation isn't. `self.get` and `self.resources` only *declare*; the slow work happens afterwards:\n\n```text\nget(Index, { session: \"S1\" })\n 1. run the factory               → declarations\n 2. open declared resources        → e.g. connect to SQLite\n 3. wait for declared refs        → dependencies activate first\n 4. run the cells' init           → e.g. read the rows into a cell\n 5. live                           → refs become \"live\"; listeners and tasks start\n```\n\n```ts\nhost.serve(Index, ({ session }, _host, self) => {\n\tconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(indexPath(session)), close: (db) => db.close() });\n\treturn {\n\t\tsearch: async ({ query }, ctx) => db.value.all(\"SELECT entryId, text FROM fts WHERE fts MATCH ?\", [query]),\n\t};\n});\n\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tconst index = self.get(Index, { session });  // a placeholder until Index is live\n\treturn {\n\t\tvariant: \"participant\",\n\t\tfind: async ({ query }, ctx) => index.search({ query }, ctx),  // only ever runs once Chat (and Index) are live\n\t\t// ...\n\t};\n});\n```\n\n**Rules:**\n- Inside the factory, never use what you declared: `db.value` and `index.someState.value` throw `not_ready`, and calling a reference throws. Use them in methods, listeners, tasks and `init`.\n- **The object goes live only with its references ready**: live, and (for a remote reference) every state member of its variant hydrated. The same holds for state it forwards or derives from a remote reference it holds some other way: it goes live once that state has its value. A reference that is not ready after the host's `readyTimeout` (default 60 s) fails the activation with `unavailable/ready_timeout`; a reference cycle across hosts ends that way instead of hanging.\n- **Concurrent `get`s** of one address share one activation, so the database opens once.\n- **Failure propagates:** if `openSqlite` throws, `Index` fails, `Chat` fails with that cause, and every waiting `get` rejects. The next `get` tries again.\n- **Abandoned activations:** if everyone releases before activation finishes, the result is disposed.\n\n### Startup readiness and parent admission\n\n`await host.load(facets)` and `await host.install(facet)` wait for the `lifetime: \"host\"` objects they install to\nbecome live: resources are open, declared references are live, and registrations are published. All facets in a load\nare installed before activation is awaited, so a host object can use a provider in a later facet. Referenced objects\nare created only when something references them. Factory, dependency and resource errors reject startup with the\nreference's activation error; host object failures also reach `onError`. Installation is not rolled back on failure: close the host\nwhen abandoning startup. `install` registers synchronously, so setup/registration errors still throw synchronously.\n\n`await host.ready()` waits for the host objects present when called, including direct\n`host.serve(..., { lifetime: \"host\" })` and previously deferred loads. It is an activation barrier, not a continuous health check, a wait for tasks to\nfinish. Later installations require another readiness wait.\n\nA worker may need to open its local Harness and announce control readiness **before** the parent accepts its link.\nA host object reporting to the server's SessionStatus cannot become live before that acceptance. Awaiting the\nwhole load before announcing control readiness would deadlock. Defer the whole barrier explicitly:\n\n```ts\n// Connect the upstream link first; its outgoing references can queue until the parent accepts it.\nawait worker.load(workerFacets, { awaitHostObjects: false });  // also supported by install\nconst harness = await worker.get(Harness, {});        // local bootstrap only; no upstream dependency\nannounceControlReady();                               // parent can now accept the worker link\nawait worker.ready();                                 // every host object live, including upstream references\nharness.release();\n```\n\n`awaitHostObjects: false` still starts every host object; it skips waiting, not activation. There is no\nautomatic classification of upstream dependencies. Choose a local bootstrap reference with no upstream dependency,\nconnect before loading upstream consumers, and let the parent admit the link before the full barrier. Control\nreadiness means admission is possible, not that every host object is live. Keep worker bootstrap counted as busy\nuntil the full barrier settles, and report a later failure through the worker's fatal/startup-error path. Never await a\nreference whose object needs the parent (a remote one, or a local one that holds one) before the parent admits the link.\n\nTo hand out a native object a resource opens (a local type's `opaque` member), expose the resource. References read its value once the object is live, and `undefined` before:\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", { version: 1, open: () => DurableHarness.open(storage), close: (h) => h.close() });\n\treturn { harness: self.expose(durable) };  // contract member: harness: opaque<DurableHarness>(\"pi.durable-harness@1\")\n});\n\nconst harness = await ctx.get(Harness, {});\nharness.harness.resume();  // the DurableHarness itself\n```\n\n## Principals and variants\n\nThe application defines its principals and how they're keyed. Chord invents none: the app gives every principal a\nrequest can act as.\n\n```ts\ntype Principal =\n\t| { kind: \"owner\"; access: \"local\" | \"relay\" }\n\t| { kind: \"guest\"; id: string; name: string; session: string; mode: \"view\" | \"full\"; revision: number }\n\t| { kind: \"worker\"; session: string; generation: number }\n\t| { kind: \"server\" }   // the parent's own requests, as a worker sees them\n\t| { kind: \"host\" };    // a host's own code\n\nconst host = createHost<Principal>({\n\tname: \"server\",\n\tself: { kind: \"host\" },\n\tprincipalKey: (p) =>\n\t\tp.kind === \"owner\" ? `owner:${p.access}` :\n\t\tp.kind === \"guest\" ? `guest:${p.id}:r${p.revision}` :\n\t\tp.kind === \"worker\" ? `worker:${p.session}:${p.generation}` :\n\t\tp.kind,\n});\n```\n\n- `createHost<Principal>` types every factory's `principal`, `ctx.principal`, routes, `accept` and\n  `invalidatePrincipal` patterns, so code narrows on `kind` without casts. Facet modules say it too:\n  `defineFacet<Principal>((api) => …)` (an app usually exports its own `defineFacet` and `Ctx` aliases). Without a type\n  argument, principals are `unknown`.\n- There is one object per principal key: two tabs of the owner share an object, and a guest gets its own.\n- Code inside a host acts as `self`, so internal objects are shared. A principal with the same key as `self` is the\n  host's own, whichever object carries it.\n- A link stamps the principal of whatever connects to it. Callers can never supply their own.\n- A child host names what its parent's own requests act as: `connect(link, { principal })` (or\n  `upstream: { link, principal }`). Requests the parent relays act as the caller it forwards.\n- A route names what the hosts it resolves act as when they call back: `route(prefix, { …, principal: (name) => … })`.\n\n**A remote factory picks a variant:**\n\n```ts\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tif (!canView(principal, session)) throw forbidden();\n\tconst c = self.get(Conversation, { session, conversation });  // as host: one shared instance\n\tif (!canSend(principal)) return { variant: \"viewer\", view: c.view };\n\tconst author = authorOf(principal);\n\treturn {\n\t\tvariant: \"participant\",\n\t\tview: c.view,\n\t\tsend: async (input, ctx) => c.submit(input, author, ctx),\n\t\tstop: async (ctx) => {\n\t\t\tawait c.stop(author, ctx);\n\t\t},\n\t};\n});\n```\n\n```ts\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // on session:S1\nif (chat.variant === \"participant\") showComposer(chat.send);  // narrows: `send` exists only there\nelse showReadOnly();\n```\n\n**What a variant doesn't do:**\n- **It isn't complete authorization.** Methods still check their arguments: that a submission belongs to this conversation, or a session ID to this principal.\n- **It doesn't cover flags that change while you hold the object.** Those are state, for example `you: { canDraw: boolean }`.\n- **Revocation** is `host.invalidatePrincipal(match)`. It force-disposes matching objects, here and over links, and their references become `gone`. A new `get` runs the factory with the new grants.\n\nLocal types have no variants. Their only callers are inside the host, acting as its `self`. If a local object needs to know who's acting, pass that explicitly, like `author` above.\n\n## References\n\n| Where | API | Released |\n| --- | --- | --- |\n| a factory | `self.get(Type, input)`: synchronous; the object goes live after it is ready | with the object (or its replaced declaration) |\n| a call | `await ctx.get(Type, input, { state? })` | when the call settles |\n| outside Chord | `await host.get(Type, input, { signal?, state? })` | `ref.release()` |\n| a UI runtime | `host.hold(Type, input)`: synchronous, possibly `loading` | `ref.release()` |\n\n```ts\nconst chat = await ctx.get(Chat, input);  // ready: live, every state member of its variant hydrated\nchat.status;  // \"loading\" | \"live\" | \"released\" | \"gone\"\nchat.variant;  // the current variant; a multi-variant reference narrows on it\nchat.shape;  // { variant, members, contract }\nconst send = chat.send;  // a stable function: always dispatches to the current code\nchat.view.value;  // the current value, typed T\nchat.view.subscribe((v) => render(v));\nchat.onStatus((status, error) => { if (status === \"gone\") reconnect(error); });\nchat.release();\n```\n\n- **A reference you hold is usable:** an awaited `get` resolves once the object is live and every state member of its variant has its first snapshot, local or remote. It rejects with the activation error (`forbidden`, unreachable, …), with `aborted` (a `ctx.get` whose ctx is aborted or whose call settles first; `host.get`'s `signal`), or with `unavailable/ready_timeout` after the host's `readyTimeout`. Only that reference is released then.\n- **A held reference stays subscribed** to the state of its variant, with or without listeners, until it is released. After a link loss the last values stay readable while it re-holds (status `loading`).\n- **Reading state before the first hydration throws `not_ready`** (a `hold` that is still loading, a member read inside a factory). A member outside the current variant throws `not_in_variant`; an ended reference throws its end (`released`, or the `gone` error). A listener added before the hydration gets its snapshot then.\n- **`state: false`** gives a method-only reference: it receives no state (unless another reference to the same object wants it) and is ready once its object is live.\n- **Calls on a `loading` reference wait** until it's live, respecting `ctx` cancellation and a deadline.\n- A member missing from the current variant rejects with `not_in_variant`.\n- After `release()`, every member rejects with `released`.\n- **`gone`** means the object was forced away while referenced: revocation, archive, its host dying. Call `get` again, or show \"unavailable\". `onStatus` reports every status change (`live`, `loading` while a link reconnects, `released`, `gone` with the error), so a UI needn't poll.\n- **References to one remote object share one binding** per host and key (type, normalized input, principal key, contract fingerprint): one hold over the link, one stream per state member, calls over the same hold. Each reference keeps its own status, observers and `release`. With `createHost({ linger: { types: { [typeName]: ms }, cap } })`, a binding whose last reference is released stays held (and hydrated) for `ms`, at most `cap` (default 4) at a time, so taking the reference again costs nothing on the link.\n- **Return values are values.** Anything long-lived you want to share is an object you reach with `get`.\n\n## State\n\n```ts\nconst todos = self.state(\"todos\", Type.Array(TodoSchema), []);\n\ntodos.change(ctx, (draft) => {\n\tdraft.push({ id: \"t1\", text: \"Write docs\", done: false });\n});\ntodos.change(ctx, (draft) => {\n\tdraft[0].done = true;\n});\ntodos.replace(ctx, []);\n\ntodos.value;  // immutable\ntodos.subscribe((value) => render(value));\n\n// mirroring an external immutable store: subscribers get a diff, not the whole value\nview.replace(ctx, conversation.view);                    // diffs when the new value reuses old containers\nview.replace(ctx, rebuilt, { diff: true });              // always diff\n```\n\n- A `change` is atomic: one revision, or nothing if the callback throws.\n- `change` only touches the paths you mutate, so fields your code doesn't know about survive. Use `replace` only where your code owns the whole value.\n- Values are immutable. Never mutate what you read or receive.\n- Remote references get a snapshot of each state member of their variant, then deltas. After a reconnect or a reload they get a `reset` snapshot, and nothing is replayed.\n- `replace` sends a structural diff when the new value shares containers with the old one (a new revision of the same state), and a single replacement otherwise. `{ diff: true | false }` forces either. The value is the same either way.\n- An implementation may expose another object's state member as its own: `return { variant: \"viewer\", view: conversation.view }`. The object goes live only once that member has its value, local or remote.\n\n**A cell whose first value comes from a source** declares `{ init }` instead of an initial value:\n\n```ts\nconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(path), close: (db) => db.close() });\nconst notes = self.state(\"notes\", Type.Array(NoteSchema), { init: () => db.value.all(\"SELECT * FROM notes\") });\n```\n\n`init(ctx)` runs once the resources are open and the references live, before the object goes live, so no method,\nlistener, task or subscriber sees a value that did not come from the source. A task that loads the value instead\nstarts only after the object is live: a method called in between answers from the placeholder (a note that exists\nreads as unknown), and an async load can overwrite a write made during its `await`. `init` may be async and gets a\n`ctx` to call other objects. The `init`s of one object run in parallel, so one must not read another's cell. A throw or\nan invalid value fails the activation. A reload keeps the cell and its value and does not run `init` again; a cell the\nreload adds runs its `init` while the reload prepares, and a failure refuses the reload.\n\n`init` reads a source the object has anyway. It saves nothing: what must outlive the process is written to that source\n(a database, a file, a Durable document) by the code that changes it.\n\n```ts\nconst visible = self.derive([catalog.rows], (rows) => rows.filter((r) => visibleTo(principal, r)));\nconst openCount = self.derive([todos], (t) => t.filter((x) => !x.done).length);\n```\n\nDerivation functions are synchronous. A derivation has no name: every reload makes a new one, computed when the\nreload commits.\n\n## Calls, ctx and roots\n\nEvery method gets `(args, ctx)`, or `(ctx)` when it takes no arguments. Pass `ctx` on to everything you call:\n\n```ts\nsend: async (input, ctx) => {\n\tctx.signal.throwIfAborted();\n\tconst uploads = await ctx.get(Uploads, {});  // a reference owned by this call; released when it settles\n\tconst files = await uploads.resolve({ ids: input.uploads }, ctx);\n\treturn c.submit({ ...input, files }, author, ctx);\n},\n```\n\nA **root** is a short tree of calls, started at ingress:\n- by a link, for a request that arrives without one;\n- by `host.root(fn)`, for timers and scripts;\n- by each event delivered to a listener (`self.on`);\n- by each Chord call made from a `task` or a Durable phase.\n\nA root runs on the code that was current when it started (its **code version**), across hosts, until it ends. Keep roots short. Long-running work is a `task`, and long waits are state.\n\n```ts\n// wrong: holds a root (and its version of the code) open for minutes\napprove: async (ctx) => waitForHuman(ctx),\n\n// right: record the request as state; the answer is a separate call\nrequest: async (req, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\td.push(req);\n\t});\n},\nanswer: async ({ id, value }, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\tsettle(d, id, value);\n\t});\n},\n```\n\n## Lifetime\n\nAn object lives while it's **referenced**, and every reference has an owner:\n\n| Owner | Acquired with | Released when |\n| --- | --- | --- |\n| another object | `self.get` | that object is disposed, or no longer declares it |\n| a call | `ctx.get` | the call settles |\n| a connection | a remote host's `get` | release, disconnect, revocation |\n| the host | `lifetime: \"host\"` | host shutdown (or a reload removing its facet) |\n| UI | `useObject` | unmount |\n\n- **Disposal** runs after the last reference is released, plus `linger`. Tasks are aborted, listeners removed, resources closed in reverse order (every close runs, and errors are collected), then the object's own references are released, which can dispose further objects.\n- **A resource's `close` may still use the objects its object references.** That holds for host shutdown and revocation too: `host.close()` and `invalidatePrincipal` dispose referrers before what they reference, and close links last. Bound remote calls in `close` with a signal: a call over a link that is already down waits for a reconnect.\n- **Forced disposal** (revocation, archive) makes references `gone` at once and aborts in-flight calls.\n- **References must not form cycles.** Inside one host this is checked at `get`.\n\n```ts\nhost.serve(Conversation, conversationFactory, { linger: 30_000 });  // switching back quickly doesn't reopen its watch\nhost.serve(Index, indexFactory, { lifetime: \"host\" });              // one object from load to host shutdown\n```\n\n**`lifetime: \"host\"`** is for local types with an empty input (or only defaults): the type has exactly one object,\ncreated for the host principal when its facet loads. Every `get` reaches that object, whoever asks; a `get` with\nanother input is invalid. References others hold do not change its lifetime, and it cannot `linger`. Only such\nobjects may register into registries. TypeScript refuses `lifetime: \"host\"` for a remote type, for an input with a\nrequired field and together with `linger`; `serve` checks the same at runtime, for callers without types.\n\n## Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\n\n// contribute (only from `lifetime: \"host\"` objects)\nhost.serve(Downloads, (_, _host, self) => {\n\tconst blobs = self.get(Blobs, {});\n\tself.register(Routes, {\n\t\t\"GET /download\": async (request, who, ctx) => {\n\t\t\tconst file = await blobs.find({ id: request.query.id }, ctx);\n\t\t\tif (!canView(who, file.session)) return forbidden();\n\t\t\treturn stream(file, ctx);\n\t\t},\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n\n// consume: a request sees the table of its own code version\nserver.on(\"request\", (req, res) =>\n\thost.root(async (ctx) => {\n\t\tconst route = host.registry(Routes).get(`${req.method} ${pathOf(req)}`, ctx);\n\t\treturn route ? route(req, principalOf(req), ctx) : notFound(res);\n\t}),\n);\n```\n\n- Each code version has one complete table. A reload never leaves a moment where an entry is missing.\n- A request that started before a reload keeps using the old table.\n- Order is first-registration order and survives replacement.\n- `conflict: \"error\"` makes a duplicate key fail the registration (or the reload).\n\n## Plugins\n\nA plugin is a feature with one **facet** per host kind:\n\n```text\nplugins/chat/\n  contract.ts    Chat, Conversation, Drafts\n  server.ts      facet for the server\n  worker.ts      facet for session workers\n  web.tsx        facet for browsers\n```\n\n```ts\n// plugins/chat/worker.ts\nimport { defineFacet } from \"@earendil-works/chord\";\nimport { Chat, Conversation } from \"./contract.ts\";\n\nexport default defineFacet((host) => {\n\thost.serve(Conversation, conversationFactory, { linger: 30_000 });\n\thost.serve(Chat, chatFactory);\n});\n```\n\n```ts\n// a host loads the plugins' facets for its kind\nawait host.load([\n\tnew URL(\"../plugins/chat/worker.ts\", import.meta.url),\n\tnew URL(\"../plugins/status/worker.ts\", import.meta.url),\n]);\n```\n\nBuilt-in plugins are trusted code from this repository and load directly from source.\n\nThe facet's `setup` argument is its host, scoped to what a plugin needs: `serve`, `name`, `root(fn)`, `registry(R)` and `signal`, which aborts once this run of the facet is no longer installed (a reload replaced or removed it, or the reload that loaded it aborted). Code that is not a Chord method, listener or task (Durable tools and hooks, callbacks of native libraries) keeps it to start roots and read registries, instead of reaching for a process-global host:\n\n```ts\nexport default defineFacet((host) => {\n\thost.serve(Sessions, sessionsFactory);\n\ttools.register(\"rename_session\", (args) =>\n\t\thost.root(async (ctx) => (await ctx.get(Sessions, {})).rename(args, ctx)),\n\t);\n});\n```\n\n## Hot reload\n\n```ts\nawait host.reload([new URL(\"../plugins/chat/worker.ts\", import.meta.url)]);  // or reload every changed plugin\n```\n\n### What happens\n\n1. **Load** the new modules.\n2. **Prepare:** run the new factories over the existing objects, without side effects. Cell writes throw, listeners and tasks don't start, registrations are collected, and changed resources are opened next to the old ones. Contracts are checked against every live consumer. Any failure leaves everything as it was.\n3. **Gate:** only if a cell's version was raised or an exclusive resource changes.\n4. **Commit:** in one synchronous step, install the new code, swap registry tables, restart tasks, rebind listeners, and send `reset` and shape updates to subscribers.\n5. **Retire:** old code keeps running the roots it started. Once they end, it's dropped, along with whatever it alone declared. If they are still running when the next reload starts, that reload waits up to `retireDeadline`, then aborts them (their `ctx.signal`) before it closes what only the old code used.\n\n### Compatible changes: no waiting\n\nBug fixes, new methods, new cells, new resources and registration changes all switch immediately. A root that started on the old code finishes on it, and new roots get the new code.\n\n### Changing what state looks like or means: bump the cell's version\n\n```ts\n// v1: { running, tasks } where tasks counted phases\n// v2: tasks counts tasks, and usage is new\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusV2, { running: false, tasks: 0, usage: emptyUsage() }, {\n\t\tversion: 2,\n\t\tmigrate: (old, from) => (from < 2 ? { running: old.running, tasks: 0, usage: emptyUsage() } : old),\n\t});\n\treturn {\n\t\tstatus,\n\t\treport: async (s, ctx) => {\n\t\t\tstatus.replace(ctx, s);\n\t\t},\n\t};\n});\n```\n\nAt the gate, for each live `StatusStore`:\n1. **Roots already inside finish.** Their re-entrant calls are let in.\n2. **Old roots that haven't touched it yet get `upgrade_conflict`** instead of old code over migrated state. They retry as a new root.\n3. **`migrate` runs** on a private copy, and the result is validated against the new schema.\n4. **Commit.** New roots use the new code and the migrated state.\n\nIf the roots inside don't finish within the deadline (5 s), the reload is **aborted** and nothing changes. The error names the blocking roots.\n\nThe version belongs to the cell (default 1); other cells of the object are not migrated. The runtime compares each\ncell's schema and version between old and new code:\n- unchanged: compatible;\n- version raised: migrate (without `migrate`, the value is kept and must fit the new schema);\n- schema changed **without** a raised version, or a lowered version: the reload is refused.\n\nA change of meaning with an identical schema needs a bump too. Nothing can detect that one for you.\n\n### Resources\n\n```ts\nconst db = self.resource(\"db\", { version: 2, open: () => openSqlite(path, { wal: true }), close: (db) => db.close() });  // 1 → 2: reopen\nconst port = self.resource(\"port\", { version: 1, exclusive: true, open: () => listen(8787), close: (s) => s.close() });\n```\n\n- A reload keeps a resource's open value unless its `version`, `deps` or `exclusive` change: `open` doesn't run\n  again. The version is required: a native value has no schema to detect a change. The kept resource still takes the\n  new code's declaration at the commit, so a later `close` is the new one.\n- A changed resource opens next to the old one during prepare. The old one is closed with its own `close` once the\n  old code retires.\n- An `exclusive` resource can't exist twice, so it's swapped at the gate: close the old one, open the new one, and reopen the old one if that fails.\n\n### Contract changes\n\n| Change | What happens |\n| --- | --- |\n| Additive, same version | reloads; consumers keep working |\n| Breaking, version bumped, with `accepts[old]` | reloads; old consumers go through the adapter |\n| Breaking, without an adapter | refused, unless the consumers reload in the same batch |\n| Breaking, version not bumped | refused: \"consumer needs member …\" |\n\nThe host's own **reference to a `lifetime: \"host\"` object** is a lifecycle pin, not an old-version consumer. When a reload replaces\nits provider, that reference adopts the new contract at commit; it does not require an `accepts` adapter for itself.\nThe object, cells and unchanged resources stay alive without reactivation. A refused reload keeps the original\nreference and contract. Other references (`self.get`, `ctx.get`, `host.get`, peers) still obey the compatibility rules above;\nstate-version bumps and migrations are still required when stored state changes.\n\n### Several plugins at once\n\nReload them together: `host.reload([a, b])`. Prepare sees all new code at once, so a changed contract and its updated consumer check against each other.\n\nOn the server, `host.reload(serverFacets, { remote: { \"session:\": workerFacets } })` reloads every connected worker too. Its report covers the server; `report.children[\"session:S1\"]` says what each worker changed, prepared and gated.\n\n## Hosts and links\n\n```ts\n// server\nconst server = createHost<Principal>({ name: \"server\", self: { kind: \"host\" }, principalKey });\nserver.accept(webSocketLink(wss, { principal: (req) => authenticate(req) }));  // stamps owner or guest from cookies\nserver.route(\"session:\", {\n\tauthorize: (principal, host) => canReach(principal, host),  // before resolving or spawning\n\tresolve: (host) => workers.ensure(host),  // a link to the worker, spawned if needed\n\tprincipal: (host) => workers.principal(host),  // what the worker acts as when it calls the server\n});\n\n// worker\nconst worker = createHost<Principal>({\n\tname: `session:${id}`,\n\tself: { kind: \"host\" },\n\tprincipalKey,\n\tupstream: { link: ipcLink(process), principal: { kind: \"server\" } },  // the server's own requests act as this\n});\n\n// anywhere\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // browser → server → worker session:S1\nconst sessions = self.get(Sessions, {});  // `hostNamed(\"server\")`: worker → server, acting as worker{S1}\n```\n\n- **The server relays** between its children: worker to worker, and browser to worker.\n- **Disconnects:** when a browser disconnects, exactly the references it had in workers are released.\n- **Principals across links:**\n  - a forwarded principal is accepted only from the parent link;\n  - calls an object makes itself act as its host;\n  - links ping, and a dead link releases everything held through it.\n- **Client origins:** a hold that comes from a browser carries an opaque id of its connection (`origin`), forwarded by the server when it relays the hold, so a worker can tell its clients apart. A client cannot set it. A disconnect releases that client's holds at once.\n- **Slow peers:** a peer acknowledges what it processed. While a window (`window`, default 256 KiB) of frames is unacknowledged, state waits and is coalesced per member's stream; calls, results and the first snapshots of a new reference are never held back, so a `get` does not wait behind other references' state. Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams are replaced by their current value. So under load a result can arrive before the state change it caused: wait for the state you need instead of assuming it is there when the call returns.\n- **Reloads across hosts:** the server coordinates prepare and commit for every host.\n- **Workers that start behind** (spawned after a reload) are caught up to the server's code version before anything is routed to them. Calls their roots make to the server meanwhile run in the server's current code version instead of failing `superseded`.\n- **Protocol version:** both ends announce the link protocol version in their hello. A peer of another version is not\n  talked to: the link closes, and `onProtocolMismatch` (a `LinkOptions` field of `connect` and `accept`) tells the\n  side that has to load new code. A browser reloads its page instead of reconnecting:\n\n  ```ts\n  host.connect(webSocketLink(socket), { onProtocolMismatch: () => location.reload() });\n  ```\n- **Browsers** don't take part in commits. They learn about new code through shape and `reset` messages, and an incompatible contract makes their calls fail with `upgrade_required`.\n\n## Durable\n\nA worker's host provides the Durable Harness as a local object of lifetime `\"host\"`. A Durable phase isn't a root: it's pinned to its registry snapshot by Durable itself. Each Chord call a hook or tool makes starts a fresh short root.\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", {\n\t\tversion: 1,\n\t\topen: () => DurableHarness.open(storage, { models, registry: durableRegistry(host, Extensions) }),  // a reader over the code version tables\n\t\tclose: (h) => h.close(),\n\t});\n\treturn { conversation: (id: number) => durable.value.conversation(id) };\n}, { lifetime: \"host\" });\n\n// a plugin contributes a Durable extension like any registration\nhost.serve(Guard, (_, _host, self) => {\n\tself.register(Extensions, {\n\t\tguard: defineExtension({\n\t\t\tname: \"guard\",\n\t\t\thooks: [hook(ToolTask, { beforeTool: (call) => (isDangerous(call) ? { block: \"Needs approval\" } : undefined) })],\n\t\t}),\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n```\n\nA reload changing the guard: Durable phases already running keep the old snapshot, the next phase uses the new one, and there's never a phase without a guard.\n\n## Errors\n\n| Code | Meaning |\n| --- | --- |\n| `forbidden` | The factory or a method refused this principal |\n| `invalid` | Input, arguments or a result failed validation |\n| `not_ready` | A declared resource or reference was used inside a factory, or state read before its reference's first hydration |\n| `not_in_variant` | The member doesn't exist in the variant you have |\n| `released` | The reference was released |\n| `gone` | The object was forced away while referenced. Call `get` again. |\n| `contract_mismatch` | The provider's contract doesn't cover the consumer's, and no adapter exists |\n| `upgrade_conflict` | An old root reached an object migrated meanwhile. Retry as a new root. |\n| `superseded` | The root's code version is no longer available on this host. Retry. |\n| `upgrade_required` | A browser's contract is outdated. Reload its web facets. |\n| `unavailable` | The target host can't be reached or started; reason `ready_timeout`: a reference was not ready within `readyTimeout` |\n| `aborted` | The call's `ctx` was cancelled |\n\n## Rules of thumb\n\n| Do | Not |\n| --- | --- |\n| Keep reloadable state in cells | Closure variables (lost on reload) |\n| Bump the cell's version, with `migrate`, when a cell's shape or meaning changes | Changing a schema without a bump (refused) or a meaning without a bump (silent) |\n| Bump the contract version on breaking changes; add `accepts` for old consumers | Renaming or removing members at the same version (refused) |\n| Do slow setup in a resource's `open` | Awaiting inside the factory (it's synchronous) |\n| Do side effects in resources, listeners, tasks and registrations | Effects in the factory body (they run during prepare) |\n| Use `ctx.get` inside methods | `self.get` inside methods (a new reference per call) |\n| Keep roots short; long waits are state | Awaiting a human inside a method |\n| Check arguments in methods | Treating the variant as all of authorization |\n| Pass `ctx` everywhere | Starting background work with the caller's `ctx` (use a task) |\n| Treat state values as immutable | Mutating what you read |\n","packages/chord/docs/spec.md":"# Chord specification\n\nThis document specifies the runtime in `src`. Where it and the code disagree, the disagreement is a bug. [Known limitations and decisions](#12-known-limitations-and-decisions) lists what the runtime does not do yet and the choices the sections below leave open.\n\nHard constraints:\n- Chord's runtime depends only on its own `delta` module (tracker, applyImmutable, encoder/decoder, diff) and Node/browser built-ins plus `typebox` for schemas. No other Pi packages.\n- Built-in plugins are trusted, loaded from source in-process. The plugin loader uses dynamic `import()` with a cache-busting query (owner-approved exception, `TODO(loader)`: replace with an isolating, evicting loader; old module copies leak; TUI plugins need isolation).\n- Hot reload is required. Host restart only for Chord/runtime code changes or host-level crash recovery.\n\n## 1. Concepts\n\n| Term | Meaning |\n| --- | --- |\n| facet | A plugin's code for one host (surface): it serves object types (`defineFacet`). |\n| object type | Name + contract. `remote` (addressable from other hosts; schemas for input, variant state members, method args/results) or `local` (host-only; members may be anything, including functions and native objects, but must be listed in the contract: `method`, `opaque` or a state schema). |\n| factory | `(input, principal, self) => implementation`. Synchronous. Registered with `host.serve(type, factory, options)` from a plugin facet. |\n| object | One instance of an object type. Stable identity per `(type, canonicalInput, principalKey)`. Owns cells, resources, references, listeners, tasks, registrations. Survives reloads. |\n| implementation | The object's code: what a factory run returns (methods + state members; `variant` for remote types). Immutable, per code version. |\n| reference | What `get` returns: a late-bound claim on an object, never the object or its implementation. An object lives while referenced. |\n| principal | App-defined. Stamped by links. In-host code acts as the `host` principal. |\n| root | One short Chord call tree, admitted at trusted ingress (link request without root, UI event, timer, `host.root`). Carries `{ rootId, codeVersion }`. |\n| code version | One committed set of implementations/registrations per host (server-issued id). |\n| code holder | Long-lived execution that is NOT a root: Durable phases/tool runs, browsers, `self.task` loops. Every Chord call they make starts a fresh short root. |\n\n## 2. Contracts\n\nContracts are runtime values for every type (shipped bundles have no TypeScript; third-party plugins are built separately). TS types are inferred from them.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,                                   // contract version: a plain number, bumped only on breaking changes\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),       // where its objects live: `session:` + input.session\n\tvariants: {\n\t\tviewer: { state: { view: ViewSchema } },\n\t\tparticipant: {\n\t\t\tstate: { view: ViewSchema },\n\t\t\tmethods: { send: method(SendSchema, Type.Object({ submissionId: Type.Number() })) },\n\t\t},\n\t},\n});\n\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(Hit)),\n\t\tonChange: method(Type.Function([Change], Type.Void()), Unsubscribe),   // non-JSON allowed for locals\n\t\tharness: opaque(\"pi.durable-harness@1\"),                               // native object, by name+version\n\t},\n});\n```\n\n- Input identity: normalize (apply defaults, strip unknown), validate, then canonical JSON with fields equal to their schema default omitted. Changing defaults or normalization requires a new type name.\n- Remote calls validate args/results and state values at the provider boundary. Local calls are not validated per call.\n- Local contracts list every member (`members: { search: method(...), harness: opaque(...), status: Schema }`). A reference that is still `loading` must know whether `h.x` is a method (a caller that waits until live), a state member (a readable) or an opaque value, before any implementation exists. Members missing from the contract are found only once the reference is live.\n- Every bundle embeds the contract tokens it was built against (its own copy). Fingerprint = hash of the canonical description.\n- Placement (remote types, required): `host: hostNamed(name)` (one host) or `hostFamily(prefix, key)` (the host `prefix` + the input's required string field `key`). Data only, in the description (and so the fingerprint and the `hold` token); there is no `at` option. A `get` normalizes the input with the caller's contract, resolves the host and acquires locally if that is this host, else remotely. `serve` (and a reload that loads a facet) refuses a type whose placement excludes the host's name. A host refuses a hold whose input its own contract places elsewhere (`invalid/placement`), and a relay refuses a hold whose target disagrees with its token's placement and input. Coverage requires equal placements, and an `accepts` adapter never bridges two placements: a moved type is a breaking change. A remote reference's address is the type, the normalized input and the principal: no host name.\n\n### Compatibility rule\n\nA consumer built against version M binds to a provider at version N iff N = M and the provider's description covers the consumer's; otherwise only through `accepts[M]` adapters declared by the provider (`serve(type, factory, { accepts: { 1: (current) => v1View } })`); else `contract_mismatch`.\n\nCovers: every consumer member exists with the same kind; methods: both take arguments or neither does, the provider accepts every argument the consumer can send (new fields optional, nothing newly required or retyped), and the provider's result fits the consumer's expected result (extra fields fine; a void consumer accepts any); state: provider value fits consumer schema (extra fields fine); opaque: same name@version.\n\nAdditive changes need no bump. Breaking changes (remove/rename/retype, newly required, or a change of meaning with the same shape) bump the number. Same version + non-covering description = forgotten bump, refused. Meaning changes without a bump are undetectable (author's responsibility, as with Durable document versions).\n\nChecked at: `get` binding, reload prepare (new provider token vs every surviving consumer holding the type and every loaded plugin importing it), remote references (consumer token in the `hold` frame).\n\nThe exact host-owned reference to a `lifetime: \"host\"` object is a lifecycle pin, not a surviving old-contract consumer. When its\nprovider is replaced, commit replaces that reference with one bound to the new provider contract, acquiring it before\nreleasing the old one so the same object, state and unchanged resources survive. It needs no `accepts` adapter\nfor its own previous contract. Prepare/abort do not change that reference. All other consumers retain their own tokens\nand the normal coverage/adapter checks; state-version and migration checks are unchanged.\n\n## 3. Factory and declarations\n\nFactories are synchronous; returning a promise throws. A declaration has a name only if something of it survives a reload and must be found again: state (its value) and resources (the open value), plus listeners (a kept subscription). Names are strings, unique per object per declaration kind (a repeated name is `invalid/duplicate`); kinds have separate names. Derivations and tasks have no names: a reload makes new ones. References are identified by their address.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 },  // schema required, initial value\n\t\t{ version: 2, migrate: (old, from) => upgrade(old, from) });                     // per cell; default version 1\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});                         // reference; identity = canonical address (+ at)\n\tconst watch = self.resource(\"watch\", {\n\t\tversion: 1, deps: [catalog], exclusive: false,\n\t\topen: async ({ signal }) => openWatch(session, signal),\n\t\tclose: async (w) => w.close(),\n\t});\n\tself.on(\"changes\", watch, (w) => w.events, (event, ctx) => status.change(ctx, (d) => { d.tasks = event.tasks; }));\n\tself.task(async (ctx) => { /* loop until ctx.signal aborts */ });\n\tconst visible = self.derive([status], (s) => project(s));      // synchronous\n\tself.register(Routes, { \"GET /status\": handler });              // registry keys are the registry's own keys; `lifetime: \"host\"` objects only\n\treturn { status: visible, errors, report: (args, ctx) => { status.replace(ctx, args); } };  // exposed state = what is returned\n}, {\n\tlinger: 30_000,                                                  // with lifetime \"referenced\" (the default) only\n});\n```\n\n| Declaration | Survives reload | Notes |\n| --- | --- | --- |\n| `state(name, schema, initial \\| { init }, { version?, migrate? })` | same cell and value (`initial` ignored, `init` not run); migrated when its version rises; a cell the reload adds runs `init` at prepare (gated objects: at the gate) | Schema required (no TypeScript at runtime). `change(ctx, draft => void)`, `replace(ctx, value)`, `value`, `subscribe`. Writes through `change` touch only mutated paths. Private state is a cell the implementation does not return. `init(ctx)` (may be async) gives the first value, see activation |\n| `get(type, input, { at? })` | kept if declared again (same address) | returns a reference |\n| `resource(name, { version, deps?, exclusive?, open, close? })` | kept unless `version`, `deps` or `exclusive` change; a kept resource adopts the new declaration (its `close`) at commit | `open` runs at prepare (non-exclusive) or at the gate (exclusive); returns a resource whose `.value` is the open value; a replaced resource closes with its own declaration's `close` |\n| `on(name, source, [select,] handler)` | same subscription while source and `select` (identity or source text) are unchanged; `handler` swapped | each event starts a fresh root in the current code version; `select` is not re-run for a kept subscription. State sources deliver their current value at start and after every reload that replaced them |\n| `task(fn)` | restarted with new code at commit: the old run is aborted via ctx.signal, and the new one starts once it finished (at most `taskStopDeadline`, default 5 s, then anyway, reported) | a code holder; calls inside start fresh roots |\n| `derive(sources, fn)` | a new derivation, computed at commit | synchronous fn |\n| `register(registry, { key: value })` | value swapped at commit | `lifetime: \"host\"` objects only |\n| `expose(resource)` | follows the resource | a local member (usually `opaque`) whose reference value is the resource's value once live |\n\nState members of an implementation may be cells, derivations, or state members of references the object holds (they are defined once the object is live). `cell.replace(ctx, value, { diff? })` publishes a structural diff when the new value shares containers with the old one (or `diff: true`), else `r`. Facet `setup` receives `{ serve, name, root, registry, signal }` of its host; `signal` aborts once this run of the facet is no longer installed (a committed reload replaced or removed it, the reload that loaded it aborted, or the host closed).\n\n`migrate` per cell is `(old, fromVersion) => new`. A cell may be several versions behind (author bumped more than once between reloads). Without `migrate`, a raised version keeps the value, which must fit the new schema.\n\n**Listeners on state reconcile.** A listener whose source is state (a cell, a derivation, a reference's state member) is subscribed when it starts and gets the current value, then every new value. A reload that replaces its source (every derivation is new; a reference to another address is another source) subscribes it again, so it gets the current value again: its handler must reconcile (be idempotent), not count events. An event source (a resource's events, any other `subscribe`) delivers no current value; a kept subscription to it is not touched by a reload.\n\n**Methods.** `method(args?, result?)`: without `args` the method takes none (callers `ref.name(ctx?)`, implementation `(ctx) => …`; `method(undefined, result)` for one that returns something); without `result` it is void (callers get `undefined`, whatever the implementation returned). The description records both (absent fields), and the `call` frame carries no `args` and the `result` frame no `value` for them. A consumer and provider must agree on whether a method takes arguments; a consumer without a result accepts a provider's.\n\nRemoved vs guide: `self.own`, `holdWhile`, async factories, inferred dependencies.\n\n### Synchronous factory, asynchronous activation\n\n`self.get` and `self.resource` only declare and return placeholders. Activation of an object:\n\n1. run the factory synchronously (declarations);\n2. await declared resources' `open()` (e.g. connect a DB);\n3. await declared references becoming live (dependencies activate first, coalesced);\n4. run the `init` of every cell declared with one, in parallel, each in a root of its own acting as the host principal; validate and install the values;\n5. attach derivations, start listeners and tasks;\n6. object is live: references `live`, `ready` resolves.\n\nInside the factory, placeholders must not be used (`resource.value` and an `init` cell's `value` throw `not_ready`; reference calls throw). An `init` that throws, or returns a value its schema refuses, fails the activation like a failed `open`. An `init` must not wait for its own object or one that holds it (it is not live yet), and must not read another `init` cell of its object (they run in parallel). `init` gives a first value from a source the object reads anyway; it does not persist anything, and a cell is not saved anywhere by having one. Methods/listeners/tasks run only on live objects, so their dependencies are live. An object goes live only once its references are ready (live and, for remote ones, hydrated) and the state sources it exposes or derives from have their values, also where they forward a remote reference held some other way; waiting for references is bounded by the host's `readyTimeout` (default 60 s, `unavailable/ready_timeout`, also for a reference cycle across hosts). External consumers holding a `loading` reference (`host.hold`): calls wait until live (ctx cancellation, deadline); state members throw `not_ready` until the first hydration. Activation failures propagate to dependents and all waiting gets; the next get retries.\n\n### Lifetime\n\n`serve(type, factory, { lifetime })`:\n\n| `lifetime` | Created | Ends | `linger` |\n| --- | --- | --- | --- |\n| `\"referenced\"` (default) | at the first `get` of its address | after the last reference is released, plus `linger` | allowed (default 0) |\n| `\"host\"` | when its facet loads (or at `serve`); loading waits until it is live | when the host closes, or a reload removes its facet | refused |\n\n- `\"host\"` is refused at `serve` for remote types, for an input that does not accept `{}`, and together with `linger`;\n  the types (`ServeOptionsFor`) refuse the same at compile time, where such an input is one with a required field.\n  The type has exactly one object: the empty input (defaults applied), created for the host principal. Every `get`\n  reaches it, whatever its principal (the factory sees the host principal); a `get` with another input is\n  `invalid/input`. Other references do not change its lifetime: the host keeps its own until it closes.\n- Only `\"host\"` objects may register (`invalid/not_host_lifetime` otherwise).\n- A `\"host\"` object whose activation fails is started again by the next reload that changes its type. A reload that\n  makes a type `\"host\"` creates its object; one that makes it `\"referenced\"` releases the host's reference, so the\n  object lives on only while others reference it.\n\n### Host objects readiness\n\n- `host.install(facet, { awaitHostObjects? })` installs declarations synchronously and returns a promise for this\n  facet's host object activations. `host.load(facets, { awaitHostObjects? })` installs the entire batch before awaiting\n  its host object activations. `awaitHostObjects` defaults to true. Unreferenced objects are not activated.\n- Factory, resource-open, dependency and activation-finalization failures reject the returned promise with the\n  activation error (ChordError codes preserved; ordinary errors wrapped as `unavailable`). Host object failures also\n  reach `onError`. Installation is not a rollback transaction; setup/registration errors from `install` throw\n  synchronously. Abandoned startup should close the host.\n- `{ awaitHostObjects: false }` starts the same host objects but resolves after installation, without waiting for\n  their activation. `host.ready()` awaits a snapshot of the host's references to its host objects at call time\n  (including direct `serve` and deferred loads), rejects their activation failures, and does not acquire additional\n  references. It is not an ongoing health check and does not await task completion or remote state hydration.\n- Local bootstrap readiness and upstream admission are separate. A worker connects its upstream link, loads with\n  `{ awaitHostObjects: false }`, awaits its local bootstrap reference (e.g. Harness, with no upstream dependency), then\n  announces control readiness so its parent can accept the link. Only after admission does it await `host.ready()`.\n  Otherwise a host object reporting to the server's SessionStatus deadlocks with the parent waiting for control\n  readiness. Deferred loads skip the whole barrier, not selected dependencies; callers explicitly choose local\n  bootstrap references, keep bootstrap busy until full readiness, and propagate later startup errors.\n\n## 4. Principals, variants, references\n\n- Principals are the app's type `P` (`createHost<P>`, `Host<P>`, `defineFacet<P>`, `Factory<C, P>`, `Ctx<P>`; default `unknown`). Chord invents none: `createHost({ self })` is the principal of the host's own code (roots, code inside objects, `lifetime: \"host\"` objects), `connect(link, { principal })` what the parent's own requests act as, `accept(link, { principal })` what a link acts as, `Route.principal(hostName)` what a routed host acts as when it calls back. All are required.\n- `createHost({ principalKey })` maps principal to a key that includes connection provenance and grant revision. Objects are per principal key. The host's own principal is recognized by key (`principalKey(p) === principalKey(self)`), not by object identity, so an equal principal never creates a second object.\n- Remote factories return `{ variant, ...members }`; `variant` must be a declared variant; members must match it. Local factories return any object. `variant` is reserved as a member name.\n- Reference: `status` (`loading | live | released | gone`), `ready` promise, `variant`, `shape` (`{ variant, members, contract }`), `onStatus(listener)` (every status change, with the error for `gone`), late-bound members, `release()`. `self.get` is synchronous (the object goes live after its references are ready); `ctx.get` and `host.get` resolve once the reference is ready: live, and every state member of its variant hydrated (`state: false`: live only). They reject with the activation error, `aborted` (ctx aborted or settled, `signal`), or `unavailable/ready_timeout`, releasing only that reference. `host.hold` is the synchronous form for UI runtimes. A held remote reference stays subscribed to its variant's state until released; after a link loss its last values stay readable while it re-holds. State members are typed `T`; reading before the first hydration throws `not_ready`, outside the variant `not_in_variant`, after the end the end's error. A remote reference type is a union discriminated by `variant`. Shape changes (reload, variant) are pushed; members missing from the current variant reject `not_in_variant`; released references reject `released`; forced disposal makes references `gone`.\n- Shared bindings: remote references on a host with the same key (type, normalized input identity, principal key, consumer contract fingerprint) are views onto one binding: one hold, one stream per state member, calls over that hold. Views have their own owner (object, call, external), status, observers and `release`; the binding ends (gone) or re-holds for all of them. Without views a binding closes, or lingers for `linger.types[typeName]` ms (`createHost({ linger: { types, cap } })`, at most `cap`, default 4, least recently released first) while staying held and hydrated.\n- Methods are `(args, ctx) => Promise<result>`. Factories still check arguments; variants are not complete authorization.\n- Revocation: `host.invalidatePrincipal(match)` force-disposes matching objects on this host and propagates over links; links re-stamp or close.\n\n## 5. Lifetime\n\n- Reference owners: object (declared `get`), call (`ctx.get`, released at settle), connection (remote references; relay maps per origin connection), host (`lifetime: \"host\"`), UI.\n- Activation coalesced per address; if all requesters leave before it completes, the result is disposed. Factory throw: unwind declarations, reject gets, next get retries.\n- Disposal after the last reference is released + `linger`: tasks aborted, listeners removed, resources closed (reverse order; all run; errors aggregated), the object's references released (cascade).\n- Bulk forced disposal (`host.close`, `invalidatePrincipal`) goes in waves, referrers before what they reference, so a resource's `close` can still use its object's references; `host.close` closes links after the objects.\n- Forced disposal: references `gone` immediately, new calls rejected, in-flight call contexts aborted, then cooperative cleanup.\n- Reference graph acyclic in-host (checked at get); across hosts by convention.\n- Links have ping/timeout; a dead link releases everything held through it.\n\n## 6. Roots and code versions\n\n- `Ctx = { signal, rootId, codeVersion, get(...): Promise<Ref>, principal? }`. Explicit; no AsyncLocalStorage dependency (may be used in dev to warn).\n- A link creates a root for a request that carries none; nested remote calls carry `{rootId, codeVersion}`.\n- Dispatch: a call with code version E runs the object's implementation for E. A host retains at most `current` and `previous` code versions (two-version rule). A call for a code version the host doesn't have (older than previous, or host started later) is rejected `superseded` (retryable by starting a new root). Never substitute another code version.\n- Browsers never carry code versions into the server: browser calls start roots at the server. Browsers learn of changes via shape/reset pushes and a server `Manifest` object; incompatible contract fingerprint -> `upgrade_required`.\n\n## 7. Reload\n\nExplicit trigger (`reload_plugins` tool, button, API). Serialized; no nesting.\n\n1. Build/type-check gate (application tooling, outside Chord): dependency-closed set; refuse breaking remote contract changes without a contract version bump.\n2. Load: on every affected host, import new facet modules (cache-busted).\n3. Prepare: for each live object of each affected type, run the new factory in recording mode: existing cells readable, writes throw; listeners/tasks/registrations recorded, not started; references recorded; non-exclusive resources whose name is new or whose `version`/`deps` changed are opened as candidate-owned (closed on abort). Validate keys, variants, registry collisions. New objects activated during prepare use the current code version (old code). Any failure: abort, discard candidates. Vote.\n4. Gate (only objects with a cell whose version was raised or which replace an exclusive resource, plus objects depending on those resources):\n   - Admission cut: from now, an old-code-version root that has not touched the object gets `upgrade_conflict` (retryable), not queued. Roots that already hold an activation lease on it are grandfathered; their re-entry is admitted.\n   - Wait for grandfathered activations to finish, deadline (default 5 s). Missed deadline: abort the reload, reopen, nothing changed (report blocking roots).\n   - Exclusive resources: close old, open new; on failure reopen old and abort.\n   - Run `migrate` into private copies; validate.\n5. Commit (server commits itself, then sends commit to children; the decision is not persisted, see section 12; FIFO links guarantee no host receives a new-code-version root before committing): each host in one synchronous step installs migrated cell values and cell versions, installs implementations as code version E', swaps registry tables, restarts tasks with new code, rebinds listeners, pushes shape/reset to subscribers, opens gates. E' current, E previous.\n   The coordinator's result carries `children`: each participating child's prepared report (its vote).\n6. Retire: when no roots of `previous` remain (and, on a child, the parent retired it), drop its implementations and release declarations only it declared. The next reload first waits for that, at most `retireDeadline`; then it aborts the roots still running on `previous` (their ctx signals), gives them up to 1 s to stop, and retires anyway. A coordinator retires before it sends `prepare`, so its `retire` frames reach the children first.\n\nCompatible changes (no state bump, no exclusive resource change) skip the gate: no waiting.\n\nState compatibility rule: reload prepare compares each cell's old and new schema and version. Unchanged: compatible (no gate). Version raised: gate + `migrate` (or the kept value), validated against the new schema. Schema changed without a raised version: reload refused. A lowered version: refused. A change of meaning with an identical schema needs a bump too (undetectable; author's responsibility). Initial values and migrate outputs are validated; per-write validation in dev mode.\n\n## 8. Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\nself.register(Routes, { \"GET /download\": handler });       // in a `lifetime: \"host\"` object; keys are the registry's keys\nhost.registry(Routes).get(\"GET /download\", ctx);            // table of ctx.codeVersion (or current without ctx)\nhost.registry(Routes).subscribe((table, codeVersion) => ...);     // bridges for imperative consumers (Durable)\n```\n\nPer-code-version immutable tables; order = first registration order, kept on replace. An application can implement Durable's `RegistryReader` from the table (snapshot per code version) so phases pin a complete snapshot; Chord does not ship that bridge.\n\n## 9. Hosts and links\n\n- `createHost({ name, self, principalKey, upstream?: { link, principal } })`; `host.accept(link, { principal })` for children; `host.connect(link, { principal })` for the parent; `host.route(prefix, { authorize(principal, hostName), resolve(hostName) => Promise<Link>, principal(hostName) })`.\n- Links: `memoryLinkPair()` (tests), Node IPC (child_process), WebSocket (server `ws`, browser `WebSocket`).\n- Principal stamping: the accepting side stamps the principal of what connects (from its authenticator); forwarded principals accepted only from the parent link; the parent's own requests (no forwarded principal) act as the principal given to `connect`; payload principal fields ignored.\n- A child that connects behind its parent's code version is fenced (nothing routed to it) and reconciled by a reload of its modules to the parent's code version. Calls its roots of the old code version make upstream are admitted in the parent's current code version (the parent never ran that code version), not rejected `superseded`.\n- Relay: server forwards a hold to the child link of its target host (`at`, resolved by the consumer from the contract's placement and the normalized input, and checked against both), mapping references per origin connection; authorizes before resolving/spawning and re-checks after.\n- Wire frames (JSON): `hello`/`bye`, `hold`/`release` (a reference over a link: `hold` returns `shape` or `gone`; there is no separate `get`), `shape`, `call`/`result`/`cancel`, `sub`/`unsub`/`resync` and `state`, `gone`, `root_end`, `retire`, `invalidate`, `ping`/`pong`, `ack`, reload control (`prepare`/`vote`/`commit`/`abort`).\n- State: a `hold` implies the state of its variant (`state: false`: none until `sub`; `unsub` stops it). The provider answers with units: a `shape` whose `state` lists the members the consumer lacks, followed by one `state` frame `as: \"snap\"` per listed member; the consumer applies the shape once those snapshots are here. Later `state` frames of a member (addressed by hold id and member `m`) are `ops` (per-stream delta encoder), `reset`, or `error`; a `snap`/`reset` without `value` carries `undefined`. A failed snapshot before the first hydration ends the reference with its error. A reload or variant change sends a unit with the members that are new; members that left stop. `again` marks a re-hold after a link loss (the consumer has values). `origin` names the client connection a hold comes from (stamped by the host that accepted the client, forwarded by relays, honored only from the parent link).\n- Protocol version: `hello` carries `protocol` (`PROTOCOL_VERSION`, now 4; absent means 1). A side that sees another version processes nothing more, calls `LinkOptions.onProtocolMismatch({ local, peer })` and closes the link with reason `protocol-version <its version>`; the other side's connection reads that reason and reports the mismatch too. Peers do not negotiate: the outdated side must load the current code (a browser reloads its page). Frame fields follow the version: the `shape` frame names the variant `variant`, code versions travel as `codeVersion` (2); remote contract tokens carry their placement (`host`), and a `hold` carries the input normalized by the consumer (3); a hold implies its state, units, `origin`, and `shape` without a code version (4).\n- Flow control: both ends announce `acks` in their hello and acknowledge what they processed (`ack`, a cumulative count of every frame but `ack`). While the frames not yet acknowledged exceed the window (`window`, default 256 KiB of JSON text), state waits at the sender in slots flushed in creation order: one per member stream (ops are appended, a snapshot or reset replaces it, a release or `unsub` drops it) and one per hold for a unit (lazy: built from the current values when sent; a newer unit merges into it). Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams become a reset of their current value built at flush (a relay sends `resync` downstream and drops that member's ops until the reset arrives). The first unit of a hold that is not `again` and every frame but state are sent at once. Links that report no frame sizes (IPC) are never windowed; a peer without `acks` is neither windowed nor acknowledged. An acceptor can limit the calls a peer has in progress (`maxCalls`); calls beyond it are answered at once with `unavailable/too_many_calls`. Nothing outside the connection may add frames to a link: acknowledgements count them.\n\n## 10. Host manager (library)\n\n`createHostManager({ driver, keepAlive })`: fork driver (entry, init, lock, env), serialized start/stop/replace, exit-before-replace, process-tree kill after grace, control channel (ready/fatal/shutdown), liveness callback (booting, activeCalls, viewers with heartbeats, durable tasks, idleFor).\n\n## 11. Test coverage\n\nThe tests cover at least: lifetime/references/linger/forced disposal/gone; activation coalescing and abandonment; principals and variants; remote calls over memory links incl. relay and principal forgery attempts; state snap/ops/reset over a link with reconnect; registries per code version; reload: compatible (no wait, pinned roots), prepare isolation (writes throw, failing factory leaves everything unchanged), state bump with migrate and gate, `upgrade_conflict` for late old roots, deadline abort, exclusive resource replacement, cross-host commit ordering, two-version retirement and `superseded`.\n\n## 12. Known limitations and decisions\n\n### Still open\n\n- **Retirement across hosts.** A child retires a code version only after its parent retired it (`retire` frame) and its own roots of that code version ended; the server decides from its own roots. A child that comes in step (hello or reconciliation) is told which code versions the parent retired before. A root started on a worker that calls up to a server which has already retired the code version both had still gets `superseded` (retryable).\n- **The commit decision is not persisted** (section 7, step 5). A server crash between its own commit and a child's commit leaves that child on the old code version until it reconnects and is reconciled.\n- **One level of reload participants.** Only the coordinator's direct child hosts take part; a child does not forward `prepare` to its own children.\n- **Reconciliation needs the child's module list.** It is remembered from cluster reloads (`host.remoteModules`), so a server restart forgets it, and a child behind a restarted server then reloads no modules: it gets the code version id with its own code. The reconciliation vote deadline is fixed at 30 s; a failed reconciliation closes the link, so the host manager restarts the worker.\n- **Links do not resume.** The provider releases everything held through a closed link at once, so re-held objects are new ones. Keeping references for a reconnect window would need session ids on links. Calls in flight when a link drops reject `unavailable`; whether they ran is unknown.\n- **Refused clients cannot tell.** The WebSocket adapter closes unauthenticated sockets; the client sees a dropped upstream and its calls wait for a reconnect until their ctx is aborted.\n- **The publication queue is process-wide and synchronous.** A slow observer delays every later notification in the process, including those of other hosts in the same process.\n- **No automatic restart.** A terminal host failure is picked up by the host manager's `keepAlive` or `fatal` path, not inside Chord.\n- **The Node loader leaks.** Every reload imports fresh module copies that are never evicted, and it does not isolate plugins (`TODO(loader)`).\n- **Stale socket locks.** On Unix systems other than Linux, two processes that probe the same stale socket file at the same moment could both take the lock. Managers serialize starts per lock name.\n- **Listener `select` identity.** A listener's subscription is kept across factory runs when its origin and `select` are unchanged; `select` counts as unchanged when identical or with the same source text, so a `select` whose implementation depends on captured values that change between runs is not detected.\n- **Shutdown and dead links.** `host.close()` disposes objects in waves (referrers before what they reference) and closes links last. A `close` that awaits a remote call over a link that is already down waits for a reconnect; bound such calls.\n\n### Decisions\n\n- **Errors.** The guide's codes; a `reason` field carries the detail, for example `invalid/args`, `invalid/cycle`, `invalid/duplicate`, `invalid/read_only`, `invalid/not_host_lifetime`, `invalid/registry_conflict`, or `unavailable/unknown_type`. A `ChordError` thrown by a factory or a dependency keeps its code through every dependent; other errors become `unavailable`, with the original as `cause`.\n- **Reload errors.** There is no `reload_aborted` code. A failed reload throws the cause's code and reason with the message `reload aborted: …` (the cause is attached). Reasons of its own are `aborted/gate_deadline` (with the blocking roots in `details`), `aborted/deadline`, `aborted/vote_deadline` and `unavailable/reload_running`. Across hosts, the message names the host that failed. A child cannot reload on its own (`unavailable/not_coordinator`).\n- **New objects during prepare.** A reload pre-allocates the next code version (\"pending\"). Types the same reload adds are served from it; their objects install implementation only for the pending code version and are force-disposed if the reload aborts.\n- **Gate admission.** From the gate until commit or abort, creating a new object of a changed type fails with `upgrade_conflict` (retryable, reason `gate`); nothing is paused, so nothing can deadlock. Gated objects get `minCodeVersion = E'`: calls and writes carrying an older code version are rejected `upgrade_conflict` permanently, and a gated object drops its previous-code-version implementation at commit. Only objects whose cell versions or exclusive resources change are gated.\n- **Tasks and listeners during the gate.** Tasks of gated objects are aborted at the admission cut and restart with new code at commit, or with the old code on abort. Listener events for gated objects are queued and replayed through the new listener at commit, or the old one at abort.\n- **Grandfathered roots** are the roots that called into the object and are still running at the cut. Every host remembers on which links a root's calls went out and sends `root_end` on them when the root ends, so leases outlive calls across hosts.\n- **Removed types and missed retirement.** Objects of removed types are force-disposed at commit. When a retirement deadline is missed, retirement is forced: those roots are aborted first (their ctx signal, reason `superseded`; a relayed call's root cancels its downstream call), then what only that code version declared is closed, and their next call is `superseded`.\n- **Joining during a reload.** Participants are the child hosts in step when a reload starts. A child whose hello arrives while it runs is decided when it ends: in step, or reconciled to the committed code version; a child already behind is fenced at once, so the calls its roots make meanwhile are admitted in the parent's code version. A reconciliation that is running when a reload starts syncs its child only after that reload ended, against the code version it committed. A reconciliation announces the parent's retired code versions before its `prepare`.\n- **Two-version rule.** An object of a changed type created after a commit has only new code; a previous-code-version root calling it gets `superseded`.\n- **Validation.** Remote calls are validated at reference dispatch, in-host too: a direct binding against the provider's schema, a binding through an adapter against the consumer's. A provider validates each snapshot and reset of a remote state member once per (re)subscription, against the consumer's contract through an `accepts` adapter and the provider's otherwise; ops are validated only in dev mode. At reload, the new provider's description must cover the old provider's when the version is unchanged, in addition to the check against live consumers and facets' `uses`.\n- **Contracts.** Local contracts list their members, so a loading reference knows whether a member is a method, state or opaque. Canonical identity omits input fields equal to their default; with TypeBox, use `Type.Optional(Type.X({ default }))` to let callers omit them. Provider variants must cover consumer variants; members the consumer's contract does not list are hidden. Schema change detection compares canonical JSON, so reordering `anyOf` branches counts as a change. Fingerprints use a non-cryptographic 64-bit hash: synchronous and browser-safe; contracts are trusted.\n- **Principals.** A root ctx at ingress acts as its principal (`host.root(fn, { principal })`); contexts inside objects act as `host`. The acceptor decides a link's kind and name (`accept(link, { principal, kind, name })`); only configured host links are addressable, carry roots and may vote, and control frames are accepted only from the parent. Clients never carry roots or code versions. Relaying needs a matching `route`; `authorize(principal, hostName)` runs before `resolve` and again after, and revoking or re-stamping a principal closes its relays at once. `invalidatePrincipal(match)` re-stamps or closes matching links and propagates pattern matches to child hosts.\n- **Cells and readables.** A value returned from `change` replaces the value and is always validated; `replace` transfers ownership without copying; by default (`{ diff }` overrides) it publishes a diff when the new value reuses a container of the old one at the same key, else `r`. `subscribe(fn)` delivers values, starting with the current one; `events(fn)` delivers `snap`, then `ops`, or `reset`. Reference state members are stable readables that throw `not_ready` while loading and `not_in_variant` outside the variant, and reset when their source moves; a listener added before they are readable gets its snapshot then. Remote state members stay hydrated while the reference is held.\n- **References and calls.** A reference's identity is its address; a second `self.get` of the same address in one run returns the same reference. A reference call without ctx, or with a task ctx, starts a fresh root in the current code version; a ctx whose call or root already settled is refused (`aborted/expired`), one from another host `invalid/foreign_ctx`. Calling a reference inside a factory throws `not_ready`.\n- **Resources and disposal.** New and replaced exclusive resources open at the gate, and so do non-exclusive resources of the same object that depend on them. A new object for an address that is still being disposed waits for that disposal. A resource open that completes after its signal aborted closes its value at once.\n- **Links and the host manager.** Every received frame is validated against its schema; malformed frames, duplicate ids and control frames from the wrong side close the link. Each connection pings every `pingInterval` and closes after `pingTimeout` without a frame. The child holds a kernel lock (an abstract socket on Linux and Android, a named pipe on Windows, a probed socket file elsewhere), acquired by `childControl().lock()` before storage opens. The manager kills the child's process group on every exit path and derives `booting` and `idleFor` from the child's liveness reports. WebSocket support is structural, so Chord does not depend on `ws`.\n- **Flow control holds back state only.** A relay (Cloudflare to a browser) can hide a slow reader, so transport backpressure cannot be relied on; acknowledgements are end to end. Results must not wait behind state, so links are FIFO except that waiting state frames can be overtaken: a result may arrive before state changes made before it. Frames of one subscription stay in order. Waiting state is coalesced rather than queued and capped per link, so a stalled peer costs bounded memory, and the next frame it gets is current. The first unit of a new reference is not held back, so a `get` never waits behind other streams.\n- **The state API stays.** Objects keep named cells and unnamed derivations; giving each object exactly one state value was rejected. Do not reopen this without a new, concrete problem.\n","packages/chord/docs/tutorial.md":"# Build a shared todo list with Chord\n\n## 1. What problem does Chord solve?\n\nA todo list has data, operations on that data, and several people looking at it.\nYou could hand-write object creation, subscription cleanup, messages between\nprocesses, and code replacement. Chord connects these pieces for you.\n\nA **host** is the runtime that owns objects. An **object** combines state with\nmethods at an address, such as “the family todo list.” A **reference** is a caller's\nreference to that object, not the object itself. Several references can reach the\nsame object. References can also reach another host over a connection.\n\n```text\nbrowser host                         server host\nref + subscribed rows -- link --> Todos view (owner or guest)\n                                           |\n                                        ref\n                                           v\n                                   TodoStore: rows + methods\n```\n\nWe will start inside one host, add observable state, then share it with a\nbrowser. Finally we will add background work, plugins, and hot reload.\n**Live state** means subscribers receive new values when data changes, without\nasking for it again.\n\n<!-- example:01 -->\n```ts\nconst host = createHost({ self: \"server\", name: \"todo-server\" });\nassert.equal(host.name, \"todo-server\");\nawait host.close();\n```\n\nThe assertion passes: the host's name is `todo-server`. Creating a host does\nnot create a todo object, start a server socket, or persist data. `close()`\nshuts down its objects and connections.\n\n> **Tip:** These are executable TypeScript examples, using assertions to show\n> results. Shared imports are at the end. Later sections reuse earlier contracts\n> and functions, but each section's test starts with fresh hosts. Test-only\n> cleanup closes hosts even when an assertion fails; snippets omit that cleanup.\n\n## 2. Your first object\n\nBefore callers can use an object, they need to agree on its name and members.\nThat agreement is a **contract**: a runtime value that also gives TypeScript its\ntypes. A **schema** describes a value's shape. We use TypeBox's `Type` to write\nschemas. `method(args, result)` declares a method by its argument and result\nschemas: `method()` takes no arguments and returns nothing, and\n`method(undefined, Type.String())`, used below, takes no arguments and returns a\nstring.\n\nStart with a label for a named list. `local` means only code inside this host\ncan reach it. The input `{ list: \"family\" }` identifies which list we want;\nit is not an argument to `label`.\n\n<!-- example:02-contract -->\n```ts\nconst TodoLabel = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { label: method(undefined, Type.String()) },\n});\n```\n\nA **factory** is a synchronous function that creates the object's implementation:\nthe methods and state callers will see. `serve` registers a contract and its\nfactory. Registration alone does not activate this lazy object; the first\n`get` does.\n\nA **root** is one short tree of calls, such as the work for a button click.\n`host.root` starts it. Its **context**, called `ctx`, carries cancellation,\ncaller identity, and the code version for nested calls. Pass it to methods you\ncall from that root.\n\n<!-- example:02-call -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoLabel, ({ list }) => ({ label: () => `Todos for ${list}` }));\nconst result = await host.root(async (ctx) => {\n\tconst todos = await ctx.get(TodoLabel, { list: \"family\" });\n\treturn todos.label(ctx);\n});\nassert.equal(result, \"Todos for family\");\n```\n\nThe result is `\"Todos for family\"`. `await ctx.get` resolves once the object is\nlive (and, as we will see, has its state). The reference acquired through\n`ctx.get` is released when this root ends. We never receive the factory's\nreturned object directly. A standalone call without `ctx`, such as\n`todos.label()`, starts its own root. A method without arguments takes only\nthe context: `todos.label(ctx)`; its implementation is `(ctx) => …`. Inside a root or method, pass the\nexisting `ctx` so nested calls stay in the same call tree.\n\nThe factory may also receive `principal` (who is asking) and `self` (the\nobject's declaration API). We did not need them yet.\n\n> **Why:** The reference separates callers from implementations. Later, replacing\n> code will not require replacing every caller's reference.\n\n## 3. Give the list live state\n\nA label is not a todo list. We need rows that can change, and a count that always\nagrees with them. A **cell** owns an observable JSON value. A **derivation**\ncomputes another observable value from cells or other readable sources.\n\nExtend the contract with `rows`, `openCount`, and editing methods. State\nmembers describe values, not methods. This adds members at contract version 1;\nolder callers that only need `label` still have everything they need.\n\n`self.state(name, schema, initial)` declares a cell and returns it. The name\n`rows` is important: on reload, Chord matches that name to the existing cell and\nkeeps its value. `self.derive(sources, fn)` declares a derivation; it has no\nname, because it holds nothing a reload must keep.\n\n<!-- example:03-contract -->\n```ts\nconst Todo = Type.Object({ text: Type.String(), done: Type.Boolean() });\nconst Rows = Type.Array(Todo);\nconst TodoStore = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: {\n\t\tlabel: method(undefined, Type.String()),\n\t\trows: Rows,\n\t\topenCount: Type.Number(),\n\t\tadd: method(Type.Object({ text: Type.String() })),\n\t\tfinish: method(Type.Object({ index: Type.Integer() })),\n\t\treset: method(Rows),\n\t},\n});\n```\n\nThe factory now declares a cell and a derivation. It is wrapped in `todoStore`,\nwhich takes the cell's options (section 9 raises its version); for now\n`todoFactory` uses the defaults. Each method receives its arguments and a\ncontext; changing the cell updates its readers and its derived count together.\n`add`, `finish` and `reset` declare no result, so they return nothing.\n\n<!-- example:03-factory -->\n```ts\n/** The store's factory; `options` versions its rows (section 9). */\nconst todoStore =\n\t(options?: StateOptions<Static<typeof Rows>>): Factory<typeof TodoStore> =>\n\t({ list }, _principal, self) => {\n\t\tconst rows = self.state(\"rows\", Rows, [], options);\n\t\tconst openCount = self.derive([rows], (todos) => todos.filter((todo) => !todo.done).length);\n\t\treturn {\n\t\t\tlabel: () => `Todos for ${list}`,\n\t\t\trows,\n\t\t\topenCount,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tdraft.push({ text, done: false });\n\t\t\t\t});\n\t\t\t},\n\t\t\tfinish: ({ index }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tconst todo = draft[index];\n\t\t\t\t\tif (!todo) throw invalid(\"args\", \"No such todo\");\n\t\t\t\t\ttodo.done = true;\n\t\t\t\t});\n\t\t\t},\n\t\t\treset: (next, ctx) => {\n\t\t\t\trows.replace(ctx, next, { diff: true });\n\t\t\t},\n\t\t};\n\t};\nconst todoFactory = todoStore();\n```\n\n`change(ctx, fn)` gives `fn` a temporary mutable **draft**. Here `add`\npushes a row and `finish` changes one field. The draft is discarded if the\ncallback throws. The callback must be synchronous.\n\n`replace(ctx, value)` replaces the whole value. We use it for resetting a\nlist we own completely. `{ diff: true }` asks Chord to publish just the\nstructural changes. Without that option, replacement diffs automatically when\nthe new value shares containers at the same keys with the old one; unrelated\nvalues normally publish a whole-value replacement. Either way, subscribers\nreceive the same resulting value.\n\n<!-- example:03-call -->\n```ts\nconst host = createHost({ self: \"server\", dev: true });\nhost.serve(TodoStore, todoFactory);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nconst counts: (number | undefined)[] = [];\nconst stop = todos.openCount.subscribe((value) => counts.push(value));\nawait host.root(async (ctx) => {\n\tawait todos.add({ text: \"Buy milk\" }, ctx);\n\tawait todos.finish({ index: 0 }, ctx);\n\tawait todos.reset([], ctx);\n});\nassert.deepEqual(counts, [0, 1, 0]);\nassert.deepEqual(todos.rows.value, []);\nstop();\ntodos.release();\n```\n\nThe count subscriber sees `0, 1, 0`: initially empty, one unfinished todo,\nthen no unfinished todos. Resetting to empty does not add another count\nnotification because the derived count is still zero. The final rows are `[]`.\n`subscribe` calls back immediately with the current value and returns an\nunsubscribe function. The awaited `get` makes sure there is a value: a reference\nyou hold is live, with its state.\n\n> **Why:** Store reloadable state in cells, not closure variables. Treat\n> `cell.value`, `ref.rows.value`, and subscriber values as immutable.\n> Only mutate drafts inside `change`; do not keep a draft for later use.\n> A derivation runs synchronously, so use it for projections, not database work.\n\n## 4. Let objects use objects and resources\n\nSaving a todo may require both the list and a database connection. A\n**reference** keeps the object it refers to alive. `self.get` declares a\nreference owned by this object; it lasts longer than a single method call.\n\nA **resource** is something the runtime opens and later closes, such as a\ndatabase connection. Its `open` may be asynchronous even though the factory\nmust be synchronous. The example database is deliberately fake: it just saves\nstrings in memory.\n\n`opaque<T>(\"name@version\")` describes a native, host-only member. It is not\nJSON state and cannot be sent over a link. `self.expose` connects that member\nto a declared resource, without reading the resource during the factory.\n\n<!-- example:04-database -->\n```ts\nconst Database = local(\"tutorial.database\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { db: opaque<{ saved: string[] }>(\"tutorial.database@1\") },\n});\nconst SavedTodos = local(\"tutorial.saved-todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { add: method(Type.Object({ text: Type.String() })) },\n});\nconst host = createHost({ self: \"server\" });\nlet opens = 0;\nconst closed = Promise.withResolvers<void>();\nhost.serve(Database, (_input, _principal, self) => {\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async () => {\n\t\t\topens++;\n\t\t\treturn { saved: [] as string[] };\n\t\t},\n\t\tclose: () => closed.resolve(),\n\t});\n\treturn { db: self.expose(db) };\n});\n```\n\nThe factory returns an exposed marker, not an opened connection. Chord runs\n`open` after declarations finish. The promise named `closed` lets the test\nobserve eventual cleanup.\n\nNow declare `SavedTodos` as an object that holds both the store and the database.\nIts editing method uses these dependencies only after activation.\n\n<!-- example:04-saving -->\n```ts\nhost.serve(TodoStore, todoFactory);\nhost.serve(\n\tSavedTodos,\n\t({ list }, _principal, self) => {\n\t\tconst database = self.get(Database, {});\n\t\tconst todos = self.get(TodoStore, { list });\n\t\treturn {\n\t\t\tadd: async ({ text }, ctx) => {\n\t\t\t\tdatabase.db!.saved.push(text);\n\t\t\t\tawait todos.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t},\n\t{ linger: 0 },\n);\n```\n\nAn outside consumer holds a `SavedTodos` reference. We also hold `Database`\ndirectly so we can inspect what was saved.\n\n<!-- example:04-call -->\n```ts\nconst saved = await host.get(SavedTodos, { list: \"family\" });\nconst database = await host.get(Database, {});\nawait host.root((ctx) => saved.add({ text: \"Buy milk\" }, ctx));\nassert.deepEqual(database.db!.saved, [\"Buy milk\"]);\nassert.equal(opens, 1);\nconst statuses: string[] = [];\nsaved.onStatus((status) => statuses.push(status));\ndatabase.release();\nsaved.release();\nawait closed.promise;\nassert.equal(saved.status, \"released\");\nassert.ok(statuses.includes(\"released\"));\n```\n\nAfter the method, the fake database contains `[\"Buy milk\"]`. It opened once,\neven though two objects referenced it. `SavedTodos` and `Database` become live\nonly after their resources and declared dependencies are ready. That is why\n`database.db!` is safe inside this live object's method: activation has\nalready established that dependency. An outside caller awaits its `get`.\n\nWhen both external references are released, `SavedTodos` is disposed and\nreleases its own database reference. With no references left, the database closes; we\nwait for `closed.promise` to prove it. The released reference stays\n`released`, and `onStatus` reports that transition. `onStatus` watches\nchanges; unlike state subscription, it does not deliver an initial status.\n\n`linger` is a delay in milliseconds after the last reference is released. Zero disposes\nwithout a grace period; a positive value can avoid reopening an expensive\nresource when a screen is briefly hidden. Linger is not persistence.\n\n> **Tip:** A factory only declares dependencies. Never call a referenced object's method or\n> read `resource.value` there, including during reload. Activation happens\n> afterward. Bump a resource's `version` when its configuration should be reopened.\n> Disposal stops tasks and listeners, closes resources, then releases references.\n\n## 5. Who is asking? One store, two views\n\nOwners may add todos; guests may only read them. A **principal** is the\napplication-defined identity of a caller. A **variant** is a named set of\nmembers a remote object offers to that principal. It lets the UI discover\nwhether an editing method is available.\n\nDo not create a separate data cell for each viewer: their lists would diverge.\nInstead, each view holds the same host-local `TodoStore` and exposes its rows.\n`self.get` acts as the host's own principal, so owner and guest views share\nthat store. Remote view objects themselves are separate per principal key.\nA remote contract also says where its objects live: `host: hostNamed(\"server\")`\nputs every `Todos` object on the host named `server`. Section 6 uses that.\n\n<!-- example:05-views -->\n```ts\nconst Todos = remote(\"tutorial.todo-view\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\thost: hostNamed(\"server\"),\n\tvariants: {\n\t\tviewer: { state: { rows: Rows } },\n\t\teditor: {\n\t\t\tstate: { rows: Rows },\n\t\t\tmethods: { add: method(Type.Object({ text: Type.String() })) },\n\t\t},\n\t},\n});\n\n/** Who may ask: the owner, a guest, or the server's own code. */\ntype Principal = \"owner\" | \"guest\" | \"server\";\n\nfunction serveViews(api: Pick<Host<Principal>, \"serve\">): void {\n\tapi.serve(Todos, ({ list }, principal, self) => {\n\t\tif (list !== \"family\") throw forbidden(\"This list is not shared\");\n\t\tconst store = self.get(TodoStore, { list });\n\t\tif (principal === \"guest\") {\n\t\t\treturn { variant: \"viewer\", rows: store.rows };\n\t\t}\n\t\treturn {\n\t\t\tvariant: \"editor\",\n\t\t\trows: store.rows,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\tif (text.trim().length === 0) throw invalid(\"args\", \"A todo needs text\");\n\t\t\t\treturn store.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t});\n}\n```\n\nHere principals are the strings `\"owner\"` and `\"guest\"`, assigned by\ntrusted application code, and `\"server\"`, the host's own code: every host is\ncreated with `self`, the principal its own roots and objects act as. Chord\ninvents no principals. The `Principal` type parameter of `createHost` and\n`Host` makes `principal` in the factory that type, so TypeScript checks the\ncomparisons. `principalKey` maps identity to object identity. Real\napplications should use stable identity, connection provenance, and grant\nrevision, not display names.\n\n<!-- example:05-call -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", principalKey: (principal) => principal });\nhost.serve(TodoStore, todoFactory);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nconst guest = await host.get(Todos, { list: \"family\" }, { principal: \"guest\" });\nassert.ok(owner.variant === \"editor\"); // narrows the reference to the editor variant\nassert.equal(guest.variant, \"viewer\");\nawait host.root((ctx) => owner.add({ text: \"Buy milk\" }, ctx), { principal: \"owner\" });\nassert.deepEqual(guest.rows.value, [{ text: \"Buy milk\", done: false }]);\n// @ts-expect-error A viewer has no add: TypeScript refuses it, and so does the provider.\nawait assert.rejects(guest.add({ text: \"Not allowed\" }), (error) => isChordError(error, \"not_in_variant\"));\nawait assert.rejects(owner.add({ text: \" \" }), (error) => isChordError(error, \"invalid\"));\n```\n\nThe owner gets `editor`; the guest gets `viewer`. Both see the same milk\ntodo. A reference is typed as the union of its variants: `add` is reachable only\nafter narrowing on `variant` (`assert.ok(owner.variant === \"editor\")` does that).\nThe guest's `add` is refused by TypeScript, and, called anyway, rejects with\n`not_in_variant`. Whitespace-only text\nrejects with `invalid`: it is a string, as the schema demands, but it breaks a\nrule only the method knows. Use `invalid` for bad input and `forbidden` only\nfor callers who may not do something.\n\n> **Why:** A variant is not all of authorization. The factory checks which\n> list may be opened; the method checks the operation's arguments. Production\n> code must also enforce grants that can change. Use\n> `host.invalidatePrincipal(match)` to revoke matching objects; held references\n> become `gone`. A UI watches `onStatus` and gets a new reference when appropriate.\n> Guest-provided names or message text must never establish identity.\n\n## 6. Reach the list from another process\n\nA browser cannot call a host-only contract. `remote` contracts describe only\nschema-checked state and methods that can travel as JSON. Our `Todos` view is\nremote; the underlying `TodoStore` and database stay local. A caller never\nnames a host: `browser.get(Todos, …)` goes to `server` because the contract\nsays so, and the server refuses to serve, or to be asked for, a type it is not\nthe place of. A type that lives on one host per session says\n`host: hostFamily(\"session:\", \"session\")`: its input's `session` field picks\nthe host (`{ session: \"S1\" }` lives on `session:S1`).\n\nA **link** carries ordered messages between hosts. A memory link runs both ends\nin one test process, but uses the remote protocol. The accepting host stamps\nthe caller's principal from its authenticator, not from method arguments or a\nclient's claimed identity. The connecting side names the principal its parent's\nown requests act as (`upstream: { link, principal }`, or `connect(link, {\nprincipal })`).\n\n<!-- example:06 -->\n```ts\nconst server = createHost<Principal>({ self: \"server\", name: \"server\" });\nserver.serve(TodoStore, todoFactory);\nserveViews(server);\nconst [down, up] = memoryLinkPair();\nserver.accept(down, { principal: \"owner\", kind: \"client\" });\nconst browser = createHost({\n\tself: \"browser\",\n\tname: \"browser\",\n\tkind: \"client\",\n\tupstream: { link: up, principal: \"server\" },\n});\nconst todos = await browser.get(Todos, { list: \"family\" });\nassert.ok(todos.variant === \"editor\");\nassert.deepEqual(todos.rows.value, []);\nconst updated = Promise.withResolvers<void>();\nconst stop = todos.rows.subscribe((rows) => {\n\tif (rows.length === 1) updated.resolve();\n});\nawait browser.root((ctx) => todos.add({ text: \"Buy milk\" }, ctx));\nawait updated.promise;\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\n// Deliberately bypass TypeScript to simulate a malformed caller.\nawait assert.rejects(todos.add({ text: 123 } as never), (error) => isChordError(error, \"invalid\"));\nstop();\n// A held reference keeps its state without listeners.\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\ntodos.release();\n```\n\nThe browser sees an empty snapshot, then the new milk todo. `ready` means\nthe remote reference has bound to a live object **and** has its state: every\nstate member of its variant has a snapshot. A held reference stays subscribed,\nwith or without listeners, until it is released.\n\nWhat crosses the wire: the input and consumer's contract description, the\nprovider's shape (variant and members), method arguments and results, and the\nstate of the reference's variant. State starts with a **snapshot**, the whole current value;\nsubsequent **deltas** describe changes. Rebinds and reloads can send a\n`reset`, a fresh whole value. Functions, resource connections, and cell\ninstances do not cross the wire.\n\nThe deliberate bad argument rejects with `invalid`. Remote arguments,\nresults, and outgoing state are validated at the provider boundary. Local\nmethods do not get per-call schema validation; they still need app-level checks.\n`dev: true` additionally checks cell writes while developing.\n\n> **Tip:** Replace `memoryLinkPair` with `webSocketLink` for a WebSocket or\n> `ipcLink` for a child-process IPC channel; object contracts stay the same.\n> Those adapters wrap an existing connection, not an authenticator. Configure\n> `accept` with an authenticated principal. A browser is `kind: \"client\"`;\n> a trusted child host is accepted with `kind: \"host\"` and its configured name.\n\n## 7. React to changes and run background work\n\nNot all work comes from method calls. The app may update an activity indicator\nwhen rows change, or run a background reminder. A **listener** reacts to an\nobservable source. A **task** is background execution owned by an object,\nwhich is aborted when the object is disposed or its code is reloaded.\n\nOur activity object mirrors the row count. Its task inserts a sample todo,\nthen waits for shutdown. The promises make those events deterministic in a\ntest; they are not stored application state.\n\n<!-- example:07 -->\n```ts\nconst Activity = local(\"tutorial.activity\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { seen: Type.Number() },\n});\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst observed = Promise.withResolvers<void>();\nconst seeded = Promise.withResolvers<void>();\nconst stopped = Promise.withResolvers<void>();\nhost.serve(Activity, (_input, _principal, self) => {\n\tconst todos = self.get(TodoStore, { list: \"family\" });\n\tconst seen = self.state(\"seen\", Type.Number(), 0);\n\tself.on(\"rows\", todos.rows, (rows, ctx) => {\n\t\tseen.replace(ctx, rows?.length ?? 0);\n\t\tif (rows?.length === 1) observed.resolve();\n\t});\n\tself.task(async (ctx) => {\n\t\tawait todos.add({ text: \"Review the list\" }, ctx);\n\t\tseeded.resolve();\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tif (ctx.signal.aborted) resolve();\n\t\t\telse ctx.signal.addEventListener(\"abort\", () => resolve(), { once: true });\n\t\t});\n\t\tstopped.resolve();\n\t});\n\treturn { seen };\n});\nconst activity = await host.get(Activity, {});\nawait Promise.all([seeded.promise, observed.promise]);\nassert.equal(activity.seen.value, 1);\nactivity.release();\nawait stopped.promise;\n```\n\nThe final `seen` is `1`. Each listener event receives a fresh root's\n`ctx`; its write belongs to that short call tree. A task is not one enormous\nroot: Chord method calls made with its task context start fresh short roots.\nWe pass that context into `todos.add` so cancellation follows the task.\nReleasing the activity reference aborts its task and resolves `stopped`.\n\n`self.on(name, source, handler)` declares a listener; `self.task(fn)` a task.\nA listener on state, like `rows` here, gets the current value when it starts,\nthen every change. It gets the current value again whenever a reload replaced\nits source (a derivation is new at every reload), so its handler must\nreconcile (`seen` is set from the rows, not counted up). Listeners can also\nsubscribe to a resource's event stream with `self.on(name, resource, select,\nhandler)`: `select` gets the opened resource and returns something with\n`subscribe`; an event stream delivers no current value. Kept subscriptions\nsurvive reload while the resource and selector remain unchanged; the handler\nswitches to new code. A selector that captures changing configuration should\nget a new listener name. Tasks have no names: a reload restarts them all.\n\n> **Why:** Do not wait for a person, sleep for minutes, or run an endless loop\n> inside a method. That pins a root and can block state migration. Store a\n> pending request in a cell and answer in another call, or use a task that obeys\n> `ctx.signal`. Never retain a completed method's `ctx` for background work.\n\n## 8. Package features as plugins\n\nNow a plugin can contribute a menu action without editing a central switch.\nA **registry** is a named table of contributions. A **facet** is a plugin's code\nfor one kind of host: it serves object types. A plugin can have server, worker, and browser\nfacets, each installing that part of the feature.\n\nRegistrations belong to objects of **lifetime `\"host\"`**: the host creates such\nan object when its facet loads and keeps it until the host closes, even if no\nscreen asks for it. That keeps our menu contribution present. Objects of the\ndefault lifetime, `\"referenced\"`, are still created only when referenced.\n\n<!-- example:08 -->\n```ts\nconst Actions = defineRegistry<(ctx: Ctx) => Promise<number>>(\"tutorial.actions\");\nconst Menu = local(\"tutorial.menu\", { version: 1, input: Type.Object({}) });\nlet click: (() => Promise<number>) | undefined;\nconst storeFacet: Facet = {\n\tname: \"todo-store\",\n\t...defineFacet((api) => api.serve(TodoStore, todoFactory)),\n};\nconst menuFacet: Facet = {\n\tname: \"todo-menu\",\n\t...defineFacet(\n\t\t(api) => {\n\t\t\tapi.serve(\n\t\t\t\tMenu,\n\t\t\t\t(_input, _principal, self) => {\n\t\t\t\t\tconst todos = self.get(TodoStore, { list: \"family\" });\n\t\t\t\t\tself.register(Actions, {\n\t\t\t\t\t\taddSample: async (ctx) => {\n\t\t\t\t\t\t\tawait todos.add({ text: \"Try plugins\" }, ctx);\n\t\t\t\t\t\t\treturn todos.openCount.value;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\treturn {};\n\t\t\t\t},\n\t\t\t\t{ lifetime: \"host\" },\n\t\t\t);\n\t\t\tclick = () => api.root((ctx) => api.registry(Actions).get(\"addSample\", ctx)!(ctx));\n\t\t},\n\t\t{ uses: [TodoStore] },\n\t),\n};\nconst host = createHost({ self: \"server\" });\nawait host.load([menuFacet, storeFacet], { awaitHostObjects: false });\nawait host.ready();\nassert.equal(await click!(), 1);\nassert.equal(host.registry(Actions).table().size, 1);\n```\n\nClicking returns `1`, and the registry has one entry. The menu's dependency\ncan come from a later facet in the load batch: `load` installs the whole\nbatch before waiting for host objects to activate. The facet declares `uses` so\nreload can also check the contract it consumes.\n\nThe facet API supplies `serve`, `name`, `root`, and `registry`.\nOur ordinary UI callback uses its saved `api.root`, not a process-global\nhost. Looking up a registry entry with `ctx` uses the table belonging to that\nroot's code version. Without `ctx`, lookup uses the current table. Duplicate\nkeys fail by default; `subscribe` can bridge tables to an imperative consumer.\n\nNormally use `await host.load(facets)` (or `await host.install(facet)`),\nwhich waits for those host objects. This example explicitly defers that barrier\nwith `awaitHostObjects: false`, then awaits `host.ready()`. That waits for a\nsnapshot of the host's objects, not for task completion or later loads.\nFailures reject the readiness promise; abandoning startup requires closing the\nhost, because installation is not a rollback transaction.\n\n> **Tip:** Use deferred readiness only when needed. A worker may have to await\n> an independent local bootstrap reference, announce control readiness so its\n> parent admits its link, and only then await `host.ready()`. Waiting for\n> upstream-dependent host objects before admission would deadlock.\n\nFor real plugin files, default-export `defineFacet(...)`. Node hosts configure\n`createHost({ loader: nodeLoader })`, with `nodeLoader` imported from\n`@earendil-works/chord/node`, then pass module URLs to `load` and `reload`.\nThe in-memory facet objects here avoid filesystem setup. Built-in facets are\ntrusted in-process code, not a sandbox; the current Node loader keeps old module\ncopies in memory, so repeated loading has that limitation.\n\n## 9. Change running code without losing the list\n\nA reference is useful precisely because callers keep it while implementations\nchange. An **code version** identifies one committed set of code and registry tables.\nA root keeps the code version it started with. New roots use the latest one.\n\n### Compatible code change: no waiting\n\nGive the same named facet a new factory. It changes the label but keeps the\ncell declaration. This test deliberately pauses one caller to show what an\nold root sees; application methods should not pause like this.\n\n<!-- example:09-compatible -->\n```ts\nconst host = createHost({ self: \"server\" });\nconst facet = (label: string): Facet => ({\n\tname: \"todos\",\n\tsetup: (api) =>\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tlabel: () => label,\n\t\t})),\n});\nawait host.load([facet(\"Original\")]);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nawait todos.add({ text: \"Buy milk\" });\nconst entered = Promise.withResolvers<void>();\nconst resume = Promise.withResolvers<void>();\nconst oldRoot = host.root(async (ctx) => {\n\tconst first = await todos.label(ctx);\n\tentered.resolve();\n\tawait resume.promise; // Test-only pause: pin an old caller during reload.\n\treturn [first, await todos.label(ctx)];\n});\nawait entered.promise;\nconst compatible = await host.reload([facet(\"Updated\")]);\nassert.deepEqual(compatible.gated, []);\nassert.equal(await todos.label(), \"Updated\");\nresume.resolve();\nassert.deepEqual(await oldRoot, [\"Original\", \"Original\"]);\nassert.equal(todos.rows.value?.[0]?.text, \"Buy milk\");\n```\n\nReload commits while the old root is paused: `gated` is empty. New calls see\n`\"Updated\"`; the resumed old root sees `\"Original\"` twice. The milk row\nsurvives. Compatible code changes do not wait for that old root to finish.\n\n### State change: bump its version and migrate\n\nSuppose a new policy converts existing todo text to uppercase. The schema is\nunchanged, but the data's meaning changes. We still bump the cell's **state\nversion** (`self.state`'s `version` option, default 1). It belongs to one cell,\nindependently of the contract version. A **migration** (`migrate`) converts an\nold value of that cell to the new representation.\n\n<!-- example:09-migrate -->\n```ts\nconst events: string[] = [];\nconst stopEvents = todos.rows.events((event) => events.push(event.type));\nconst migrated = await host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tTodoStore,\n\t\t\t\ttodoStore({\n\t\t\t\t\tversion: 2,\n\t\t\t\t\tmigrate: (old, from) => {\n\t\t\t\t\t\tassert.equal(from, 1);\n\t\t\t\t\t\treturn (old as Static<typeof Rows>).map((todo) => ({ ...todo, text: todo.text.toUpperCase() }));\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t),\n\t},\n]);\nassert.equal(migrated.gated.length, 1);\nassert.deepEqual(todos.rows.value, [{ text: \"BUY MILK\", done: false }]);\nassert.deepEqual(events, [\"snap\", \"reset\"]);\nstopEvents();\n```\n\nThe migration receives the old rows and their state version, `1`. It returns\nnew rows; the reference now reads `[{ text: \"BUY MILK\", done: false }]`.\nSubscribers get a `reset` event containing the migrated value. The event\nassertion sees an initial `snap`, then that `reset`.\nOne object is gated. A **gate** waits for calls already inside that object to\nfinish before migrating a private copy, validating it, and committing. Old\nroots that had not entered it cannot later use old code over migrated data:\nthey get `upgrade_conflict` and should retry in a new root.\n\nChanging a cell's schema also requires a version bump, and a migration unless\nthe old value already fits the new schema. A change of meaning with the same schema,\nlike this example, cannot be detected automatically. Real migrations must\nreference each stored version they support, not assume every installation is\nexactly one version behind.\n\n### Contract change: keep an adapter for old callers\n\nRename `label` to `heading`. That is a breaking contract change, so use\ncontract version 2. The provider's `accepts[1]` adapter builds the implementation\nthat version-1 consumers still expect.\n\n<!-- example:09-contract -->\n```ts\n// Remove label from the new provider's contract: it is now called heading.\nconst { label: _label, ...members } = TodoStore.def.members;\nconst RenamedStore = local(\"tutorial.todos\", {\n\tversion: 2,\n\tinput: TodoStore.def.input,\n\tmembers: { ...members, heading: method(undefined, Type.String()) },\n});\nawait host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tRenamedStore,\n\t\t\t\t(input, principal, self) => {\n\t\t\t\t\tconst { label, ...implementation } = todoStore({ version: 2 })(input, principal, self);\n\t\t\t\t\treturn { ...implementation, heading: label };\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\taccepts: {\n\t\t\t\t\t\t1: (current) => ({ ...current, label: current.heading }),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t},\n]);\nassert.equal(await todos.label(), \"Todos for family\");\nconst newer = await host.get(RenamedStore, { list: \"family\" });\nassert.equal(await newer.heading(), \"Todos for family\");\nassert.equal(todos.rows.value?.[0]?.text, \"BUY MILK\");\n```\n\nThe old reference's `label` and the new reference's `heading` both return\n`\"Todos for family\"`. Both still reach the migrated rows. The adapter maps\nimplementations, not references; forwarding the existing method preserves its context.\nWe keep the cell at version 2 here so this code-only change does not repeat\nmigration.\n\n> **Why:** Contract versions protect callers; state versions protect stored\n> data; resource versions control reopening. Do not use one number for all\n> three. Additive compatible contracts can keep their version; breaking shape\n> or meaning changes need a contract bump and adapters for old consumers.\n> Reload prepares candidates first; preparation failure leaves the old code\n> running. A migration gate that misses its deadline aborts rather than\n> switching data underneath active old code.\n\n## 10. Test the objects, not a whole deployment\n\nMost implementation needs only a bare host. Use real factories for state tests, and a\nfake facet when you want to test delegation without a real database or service.\nHere the fake records what the editing view sends to the store.\n\n<!-- example:10 -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", dev: true });\nconst received: string[] = [];\nconst fake: Facet = {\n\tname: \"fake-store\",\n\t...defineFacet((api: FacetApi) => {\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tadd: ({ text }) => {\n\t\t\t\treceived.push(text);\n\t\t\t},\n\t\t}));\n\t}),\n};\nawait host.load([fake]);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nassert.ok(owner.variant === \"editor\");\nawait owner.add({ text: \"Test delegation\" });\nassert.deepEqual(received, [\"Test delegation\"]);\nawait assert.rejects(owner.add({ text: \"\" }), (error) => isChordError(error, \"invalid\"));\nowner.release();\nawait assert.rejects(owner.add({ text: \"Too late\" }), (error) => isChordError(error, \"released\"));\n```\n\nThe fake received `[\"Test delegation\"]`. Empty text failed before reaching\nthe fake. The released reference rejects further calls with `released`.\n`isChordError(error, code)` checks a stable error code; avoid testing only a\nmessage's wording.\n\nOur other tests exercise the real cells (section 3), resource cleanup (4),\nprincipal variants (5), a client over a memory link (6), and callers kept through\nreload (9). After an awaited `get` the state is there; for later changes, await\nthe value you need. Always unsubscribe and close hosts; leaked references can hide\nlifetime bugs or leave background tasks running.\n\n> **Tip:** A fake must honor the contract it serves. This fake delegates all\n> ordinary members to `todoFactory` and replaces only `add`; it deliberately\n> objects calls rather than updating rows. Do not confuse that with persistence.\n\n## 11. Keep these habits\n\nA temporary call reference should not accidentally become a permanent one. A\nscreen should not lose its data merely because a button click finishes. Use\n`ctx.get` for the first lifetime and `host.get` for the second.\n\n<!-- example:11 -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst screen = await host.get(TodoStore, { list: \"family\" });\nawait host.root(async (ctx) => {\n\tconst temporary = await ctx.get(TodoStore, { list: \"family\" });\n\tawait temporary.add({ text: \"Keep roots short\" }, ctx);\n}); // The temporary reference is released here; screen still holds the store.\nassert.equal(screen.openCount.value, 1);\nscreen.release(); // No reference remains; without persistence, disposal loses these rows.\n```\n\nThe screen still sees count `1` after the root finishes, because it owns a\nseparate reference. When the screen releases it, no reference remains. This tutorial's cells\nare in memory: reload preserves a live object, but disposal or process restart\nloses its rows. Real persistence belongs in a database resource, with an\nexplicit load/save policy.\n\n### Checklist and common mistakes\n\n- Keep reloadable state in cells, not closure variables: factories rerun.\n- Change drafts, not published values: mutation outside cells bypasses updates.\n- Use `change` for field edits and `replace` for owned snapshots: replacing\n  can erase fields your code does not know about.\n- Declare dependencies in a synchronous factory, not during methods: activation\n  and reload need a complete declaration list.\n- Use `ctx.get` for temporary method dependencies: their references end with the call.\n- Pass `ctx` into nested calls: cancellation and code versions must stay together.\n- Keep roots short and tasks cancellable: migration and shutdown need work to settle.\n- Check grants and arguments, not just variants: available methods are not a full policy.\n- Stamp principals at trusted ingress, not from client payloads: names are not identities.\n- Await `get` before reading state: a reference you hold is live and hydrated, a\n  loading one (`host.hold`) throws `not_ready`.\n- Release references and unsubscribe: otherwise unused objects remain referenced or observed.\n- Bump state versions, contract versions, and resource versions for their own changes:\n  each protects a different boundary.\n- Await host object readiness before reporting startup success: declared is not yet live.\n- Keep reference dependencies acyclic: an object cannot wait for itself through another object.\n\n### Imports and running the examples\n\nThese are the shared imports from `test/tutorial.test.ts`. The relative\npath is correct from that test directory; applications import the same names\nfrom `@earendil-works/chord`. `assert` expresses expected values; `it` and\n`onTestFinished` come from the test runner; `readFileSync` is used by the\ntutorial's source-matching check, not by the application.\n\n<!-- example:imports -->\n```ts\nimport assert from \"node:assert/strict\";\nimport { readFileSync } from \"node:fs\";\nimport { type Static, Type } from \"typebox\";\nimport { it, onTestFinished } from \"vitest\";\nimport {\n\ttype Ctx,\n\tcreateHost,\n\tdefineFacet,\n\tdefineRegistry,\n\ttype Facet,\n\ttype FacetApi,\n\ttype Factory,\n\tforbidden,\n\ttype Host,\n\thostNamed,\n\tinvalid,\n\tisChordError,\n\tlocal,\n\tmemoryLinkPair,\n\tmethod,\n\topaque,\n\tremote,\n\ttype StateOptions,\n} from \"../src/index.ts\";\n```\n\nFrom `packages/chord`, run:\n\n```sh\nnode ../../node_modules/vitest/dist/cli.js --run test/tutorial.test.ts\n```\n\nThere is one test per numbered section. Every TypeScript block is matched\nagainst its runnable source, so changing either copy without the other fails a\ntest. The test harness adds teardown and drift checks; it never uses a real\ndatabase, network service, or provider API.\n\nNext read [the usage guide](guide.md) for the API reference and\n[the specification](spec.md) for compatibility, lifetime, and reload\nrules, including its known limitations. If a reference disagrees with\n`src`, the current implementation wins.\n","packages/chord/src/delta/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fchord%2Fsrc%2Fdelta%2FREADME.md -->","packages/codemode/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcodemode%2FREADME.md -->","packages/codemode/examples/cloudflare/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcodemode%2Fexamples%2Fcloudflare%2FREADME.md -->","packages/coding-agent/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2FREADME.md -->","packages/coding-agent/docs/cli-integration.md":"# CLI Integration\n\nBy default, running `pi` opens the interactive terminal interface. When input or output is piped or redirected, Pi uses print mode instead. You can also select print, JSON, or RPC mode explicitly for scripts and applications.\n\nAll four modes use the same agent, sessions, resources, and tools. The mode determines how input enters Pi, how output is exposed, and whether the process remains available for more commands.\n\nThe SDK is not a CLI mode. It embeds the agent directly in a Node.js or Bun process. See the [SDK](sdk.md) when direct TypeScript access is preferable to a process boundary.\n\n## Choose a mode\n\n| Mode | Interface | Lifetime | Use it when |\n|---|---|---|---|\n| Interactive | Terminal UI | Until the user exits | A person is working with Pi directly |\n| Print | Final text on stdout | One invocation | A script needs the final assistant response |\n| JSON | JSONL events on stdout | One invocation | A process needs structured progress from a run |\n| RPC | JSONL commands, responses, and events | Long-lived | A process needs bidirectional control |\n\nCLI options still select the working directory, model, tools, resources, and session persistence independently of the mode. See [Command Line](cli.md) for the complete startup options.\n\n## Print to stdout\n\nPrint mode runs the supplied prompts, writes the final assistant text to stdout, and exits:\n\n```bash\npi --print \"Summarize the changes in this repository\"\n```\n\nUse print mode when only the final text is needed, including command substitution, pipelines, and one-shot jobs. Intermediate events are not exposed.\n\nPrint mode writes errors to stderr. A final assistant response with an `error` or `aborted` stop reason produces a nonzero exit status. A response cut off at the model's output token limit (`length` stop reason) still prints its partial text to stdout, writes a warning to stderr, and produces a nonzero exit status.\n\nWhen no mode is selected explicitly, non-TTY stdin or stdout also selects print mode. This allows piped input and output without adding `--print`.\n\n## Stream JSON events\n\nJSON mode writes a session header followed by agent and session events as newline-delimited JSON:\n\n```bash\npi --mode json \"Review this repository\" > events.jsonl\n```\n\nThis is structured event output, not a single JSON result or a constraint on the format of the model’s response.\n\nAll prompts are supplied when the process starts. The process streams events for that run and then exits; it does not accept later commands.\n\nA failed or aborted assistant response appears in the event stream but does not by itself produce a nonzero exit status. Inspect the events when success or failure matters. Pi still exits nonzero if the invocation throws an error.\n\nStreaming `message_update` records contain deltas rather than a growing message snapshot. Assemble live output from the delta events, then replace it with the authoritative message from `message_end`.\n\n`agent_end` can be followed by automatic recovery or queued work. `agent_settled` marks the end of automatic work for the current run.\n\nStdout is reserved for JSONL. Diagnostics and application logging are written to stderr. See [JSON Event Stream](json.md) for framing, event shapes, and reconstruction rules.\n\n## Control Pi with RPC\n\nRPC mode keeps Pi running while another process sends commands and receives responses and events:\n\n```bash\npi --mode rpc --no-session\n```\n\nCommands are JSON objects written to stdin. Responses and events are JSON objects written to stdout. Every record occupies one line.\n\nAdd an `id` to commands that need correlation. The matching response repeats that ID. Events generally have no command ID because they describe session activity rather than one request.\n\nA successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean the run completed. Continue consuming events through `agent_settled` when completion matters.\n\nRPC commands can change models, inspect state, manage sessions, run shell commands, and answer extension UI requests.\n\nExtension dialogs form a request-response subprotocol. Other extension UI updates are notifications that a client may display or ignore. TUI-only extension capabilities are unavailable or degraded outside interactive mode.\n\nFor Node.js or TypeScript integrations, prefer `RpcClient` from `@earendil-works/pi-coding-agent`. It starts a Pi RPC child process, correlates requests, exposes typed command methods, and delivers session events to listeners.\n\nThe [RPC client example](../examples/rpc-client.ts) sends one prompt, streams text and tool activity, waits for `agent_settled`, and shuts down the child process. It is included in the repository’s TypeScript checks.\n\n`RpcClient.promptAndWait()` installs its event listener before sending the prompt, avoiding a race with fast completions. For separate operations, subscribe before calling `prompt()` and call `waitForIdle()` only while a run is active.\n\nThe client requires a path to a runnable Pi CLI. The repository example points at `dist/cli.js`, so the package must be built before that example runs from a checkout.\n\nIf you are building a client without `RpcClient`, start with [RPC Protocol](rpc.md), then use [RPC Commands](rpc-commands.md) and [JSON Event Stream](json.md) as the wire references.\n\n## Fork and rebrand Pi\n\nA source fork can change the CLI name and configuration directory through `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"my-agent\",\n    \"configDir\": \".my-agent\"\n  }\n}\n```\n\nChange the top-level `bin` field to set the executable name. These settings affect the CLI banner, configuration paths, and derived environment variable names.\n\n## Examples and references\n\n- [RPC client](../examples/rpc-client.ts): typed Node.js integration\n- [RPC extension UI](../examples/rpc-extension-ui.ts): custom terminal client with extension dialogs\n- [Command Line](cli.md): startup options and mode selection\n- [JSON Event Stream](json.md): JSON event reference\n- [RPC Protocol](rpc.md): RPC lifecycle, framing, errors, and shutdown\n- [RPC Commands](rpc-commands.md): command and response reference\n- [RPC Extension UI](rpc-extension-ui.md): extension interaction subprotocol\n- [SDK examples](../examples/sdk/): in-process TypeScript integrations\n","packages/coding-agent/docs/cli.md":"<a id=\"cli-and-modes-reference\"></a>\n\n# Command Line\n\nThis page documents Pi's built-in command-line commands and options. Run `pi --help` or append `--help` to a command for the exact interface in your installed version. The top-level help also includes options registered by loaded extensions.\n\n```sh\npi [options] [--] [@files...] [messages...]\npi install <source> [options]\npi remove <source> [options]\npi uninstall <source> [options]\npi update [target] [options]\npi list\npi config [options]\npi auth <check|print-api-key|print-bearer-token> [options]\npi mcp <list|login|logout> [options]\n```\n\n<a id=\"modes\"></a>\n\n## Invocation and output\n\n```sh\npi\npi --print \"Summarize this repository\"\ngit diff | pi --print \"Review this change\"\npi --mode json \"Inspect this repository\" > events.jsonl\n```\n\nWith terminal stdin and stdout, Pi opens the terminal UI unless `--print`, `--mode json`, or `--mode rpc` selects another interface. When either stream is redirected and neither JSON nor RPC mode is selected, Pi uses print mode. See [CLI Integration](cli-integration.md) for choosing between interactive, print, JSON, RPC, and SDK integration.\n\n| Input | Behavior |\n|---|---|\n| `message` | Provide an initial prompt |\n| `@path` | Include a text file or image in the first prompt |\n| Piped stdin | Prepend its contents to the first prompt |\n| `--` | Stop option parsing so a prompt can begin with `-` |\n\nPi resolves `@path` from the current working directory. The working directory also controls project configuration, resource discovery, and session grouping.\n\n`--print` controls whether Pi runs once and exits. `--mode` selects the output interface. `--mode text` does not force one-shot execution when stdin and stdout are terminals; use `--print` for that behavior.\n\n| Option | Behavior |\n|---|---|\n| `-p`, `--print` | Run the supplied prompts, write the final assistant text to stdout, then exit |\n| `--mode text` | Select text output; still open the terminal UI when stdin and stdout are terminals |\n| `--mode json` | Run the supplied prompts, write JSONL events to stdout, then exit |\n| `--mode rpc` | Read JSONL commands from stdin and write responses and events to stdout until shutdown |\n| `--export <input> [output]` | Export a session file to HTML and exit; derive the destination when `output` is omitted |\n\nRPC mode rejects `@file` arguments. JSON and RPC modes reserve stdout for protocol records. See [JSON Event Stream](json.md) and [RPC Protocol](rpc.md).\n\n<a id=\"model-options\"></a>\n\n## Models\n\n```sh\npi --model sonnet:high\n```\n\nSee [Choose a Model](models.md) for model selection and [Providers](providers.md) for credentials.\n\n- `--provider <name>`<br>\n  Restricts `--model` lookup to one provider. It requires `--model`.\n- `--model <pattern>`<br>\n  Selects by exact ID or fuzzy ID/name match. It accepts `provider/id` and an optional `:<thinking>` suffix. If several providers match, Pi prefers the one with credentials, otherwise it asks for `--provider`.\n- `--api-key <key>`<br>\n  Uses a non-persistent API-key override. It requires a model selected through `--model` or `--models`. It does not choose between providers that offer the same model.\n- `--thinking <level>`<br>\n  Sets `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. It overrides a `--model` suffix and is clamped to the model's capabilities.\n- `--models <patterns>`<br>\n  Sets a comma-separated scope for startup and cycling. It accepts exact IDs, fuzzy matches, case-insensitive globs, and optional `:<thinking>` suffixes. A model offered by several providers is added for each, with a warning.\n- `--list-models [search]`<br>\n  Lists available models, optionally filtered by a fuzzy search, then exits.\n\n<a id=\"session-options\"></a>\n\n## Sessions\n\n```sh\npi --continue\n```\n\nSee [Sessions and Context](sessions.md) for resuming, forking, naming, and storing sessions.\n\n- `-c`, `--continue`<br>\n  Continues the most recent session for the current project.\n- `-r`, `--resume`<br>\n  Opens the session selector.\n- `--session <path|id>`<br>\n  Opens by file path, exact ID, or partial ID. Pi searches the current project first and offers to fork a cross-project match.\n- `--session-id <id>`<br>\n  Opens the exact project session ID or creates it if absent. IDs accept letters, numbers, `.`, `_`, and `-`.\n- `--fork <path|id>`<br>\n  Forks an existing session into a new session for the current project.\n- `--session-dir <dir>`<br>\n  Overrides storage and lookup. It takes precedence over `PI_CODING_AGENT_SESSION_DIR` and the `sessionDir` setting.\n- `--no-session`<br>\n  Uses an in-memory session that is not persisted.\n- `-n`, `--name <name>`<br>\n  Sets the session display name.\n\nConstraints:\n\n- Session IDs must start and end with a letter or number.\n- `--fork` cannot be combined with `--session`, `--continue`, `--resume`, or `--no-session`.\n- `--session-id` cannot be combined with `--session`, `--continue`, or `--resume`. Combine it with `--fork` to choose the new ID.\n\n<a id=\"tool-options\"></a>\n\n## Tools\n\n```sh\npi --tools read,grep,find,ls --print \"Review this project\"\n```\n\nSee [Settings](settings.md#tools) for configuring the default tool selection.\n\n- `-t`, `--tools <list>`<br>\n  Replaces the default selection with a comma-separated allowlist of built-in, extension, or custom tools. Entries are tool names or patterns where `*` matches any characters. MCP tools are kept unless an entry starts with `mcp__` (see [MCP tools](#mcp-tools)). A list of only `+name` and `-name` entries is not an allowlist; it changes the default selection instead.\n- `-xt`, `--exclude-tools <list>`<br>\n  Disables comma-separated tool names or patterns after all other selection options, MCP tools included.\n- `-nbt`, `--no-builtin-tools`<br>\n  Disables default built-in tools while retaining extension and custom tools.\n- `-nt`, `--no-tools`<br>\n  Starts with all built-in, extension, custom, and MCP tools disabled.\n\nDefault enabled tools are `read`, `bash`, `edit`, and `write`, unless `defaultTools` changes them. `--tools` with plain names replaces the whole selection, so name every tool you want. Like `defaultTools`, it also accepts a list of only `+name` and `-name` entries, which adds tools to or removes them from the default selection: `pi --tools +codemode,-write` keeps the other default tools, enables `codemode`, and disables `write`. These entries take exact tool names, not `*` patterns; use `--exclude-tools` to disable tools by pattern. Plain names and `+name`/`-name` entries cannot be mixed. `/reload` enables tools newly added to `defaultTools`, but a tool removed with `-name` stays removed.\n\n<a id=\"mcp-tools\"></a>\n\n`--tools` selects the tools declared to the model. It does not remove MCP tools, whose reach is set by their [exposure](mcp.md#control-tool-exposure): `pi --tools read,codemode` keeps every MCP tool callable from codemode scripts. An MCP tool that no entry names or matches is never declared directly, whatever its exposure; only `tool_search`, if listed, can load it. Once an entry starts with `mcp__`, `--tools` filters MCP tools too, so this keeps only the tools of the `radius` server:\n\n```sh\npi --tools read,bash,codemode,'mcp__radius__*'\n```\n\nThe MCP resource tools (`list_mcp_resources`, `list_mcp_resource_templates`, `read_mcp_resource`) count as MCP tools. To remove MCP tools, use `--exclude-tools 'mcp__*'` or [`--no-mcp`](#resource-options).\n\n| Built-in | Purpose |\n|---|---|\n| `read` | Read text files and supported images |\n| `bash` | Run shell commands |\n| `powershell` | Run PowerShell commands on Windows |\n| `edit` | Apply exact text replacements to an existing file |\n| `write` | Create or overwrite a file |\n| `grep` | Search file contents |\n| `find` | Find paths using glob patterns |\n| `ls` | List directory contents |\n\nBuilt-in extensions add two more tools. They are off by default; the MCP extension turns them on when an MCP server needs them (see [MCP](mcp.md#exposure)). To enable them yourself, name them in `--tools` or `defaultTools`.\n\n| Built-in extension | Purpose |\n|---|---|\n| `codemode` | Run JavaScript that calls the other tools, for example in parallel with `Promise.allSettled`; only the script's output reaches the model |\n| `tool_search` | Search tools that are not declared to the model (`codemode` and `deferred` exposure, such as MCP tools) and declare the matches for the next call |\n\n### Enable codemode\n\nTo turn on `codemode` for every session, add it to the default tools in `~/.pi/agent/settings.json` or a project's `.pi/settings.json`:\n\n```json\n{\n  \"defaultTools\": [\"+codemode\"]\n}\n```\n\nThis keeps `read`, `bash`, `edit`, and `write` and adds `codemode`. For one invocation, add it with `--tools`:\n\n```sh\npi --tools +codemode\n```\n\nCodemode is useful without MCP: scripts can run several tool calls in parallel, filter large output before it reaches the model, call classifier models such as TypeSafe's Jev through `models.classify()` (see [Classifier models](models.md#use-classifier-models)), and generate images through `models.generateImages()` (see [Image models](models.md#use-image-models)).\n\n### How codemode works\n\nScripts run in a QuickJS sandbox and reach the other tools through `tools.<name>(args)`. [Codemode](codemode.md) describes the script API, how tools are listed and found, the `store()` and `models` globals, and the limits.\n\n### Tool search\n\n`tool_search` is off by default; enable it with `\"defaultTools\": [\"+tool_search\"]` or `--tools`. It uses the same ranking as `searchTools()` over tools that are not declared yet and declares the matches for the next model call. Loaded tools are recorded in the session like other tool changes, so they stay declared on that branch.\n\n<a id=\"resource-options\"></a>\n\n## Resources\n\n```sh\npi --extension ./review.ts\n```\n\nSee [Configuration](configuration.md) for conventional directories and project trust, [Settings](settings.md#resources) for configured paths, and [Pi Packages](packages.md) for package sources.\n\n- `-e`, `--extension <path>`<br>\n  Loads an extension file or directory, or a built-in extension such as `builtin:mcp`, and is repeatable.\n- `-ne`, `--no-extensions`<br>\n  Disables discovered, configured, and built-in extensions. Explicit `-e` paths still load, so `pi -ne -e builtin:mcp` keeps only the built-in MCP support.\n- `--no-mcp`<br>\n  Disables the built-in MCP support for this run: no servers connect, and there are no MCP tools or `/mcp`. It does not affect an extension that replaces the built-in MCP support.\n- `--skill <path>`<br>\n  Loads a skill file or directory and is repeatable.\n- `-ns`, `--no-skills`<br>\n  Disables discovered and configured skills. Explicit `--skill` paths still load.\n- `--prompt-template <path>`<br>\n  Loads a prompt-template file or directory and is repeatable.\n- `-np`, `--no-prompt-templates`<br>\n  Disables discovered and configured templates. Explicit `--prompt-template` paths still load.\n- `--theme <path>`<br>\n  Loads a theme file or directory and is repeatable.\n- `--use-theme <name[/name]>`<br>\n  Selects the initial interactive theme for this run.\n- `--no-themes`<br>\n  Disables discovered and configured themes. Explicit `--theme` paths still load.\n- `-nc`, `--no-context-files`<br>\n  Disables `AGENTS.md` and `CLAUDE.md` discovery.\n\nResource paths apply only to the current process. Relative paths resolve from the current working directory.\n\n<a id=\"prompt-and-display-options\"></a>\n\n## Prompts and process\n\n```sh\npi --append-system-prompt ./instructions.md\n```\n\nSee [Configuration](configuration.md) for saved configuration, [Security](security.md#understand-project-trust) for project trust, and [Environment Variables](environment-variables.md) for process controls.\n\n- `--system-prompt <text|path>`<br>\n  Replaces the default system prompt with text or the contents of an existing file.\n- `--append-system-prompt <text|path>`<br>\n  Appends text or an existing file to the system prompt and is repeatable.\n- `--tui-mode <mode>`<br>\n  Uses `fullscreen` (default) or `regular` terminal mode.\n- `--verbose`<br>\n  Shows verbose interactive startup information, overriding `quietStartup`.\n- `-a`, `--approve`<br>\n  Trusts project-local configuration and resources for this process.\n- `-na`, `--no-approve`<br>\n  Ignores trust-gated project-local configuration and resources for this process.\n- `--offline`<br>\n  Disables automatic network activity, including model catalog refreshes. Equivalent to `PI_OFFLINE=1`.\n- `-h`, `--help`<br>\n  Shows help, including flags registered by loaded extensions, then exits.\n- `-v`, `--version`<br>\n  Shows the Pi version, then exits.\n\nExtensions may register additional long-form options. Unknown short options are rejected.\n\n## Package commands\n\n```sh\npi install npm:@scope/package\n```\n\nSee [Pi Packages](packages.md) for source formats, filtering, installation, and project scope.\n\n### Common tasks\n\n| Task | Command |\n|---|---|\n| Install a package | `pi install <source>` |\n| List configured packages | `pi list` |\n| Remove a package and its settings entry | `pi remove <source>` |\n| Configure which package resources load | `pi config` |\n\nAdd `--local` or `-l` to `install`, `remove`, `uninstall`, or `config` to use project settings instead of global settings.\n\n### Update Pi or packages\n\nRunning `pi update` without a target updates Pi itself.\n\n| Task | Command |\n|---|---|\n| Update Pi | `pi update` |\n| Update all installed packages | `pi update --extensions` |\n| Update one installed package | `pi update <source>` |\n| Refresh model catalogs | `pi update --models` |\n| Update Pi and all installed packages | `pi update --all` |\n\nAdd `--force` to reinstall Pi when the selected update includes Pi.\n\n`pi update` cannot update Pi when another package manager provides it, such as Nix. Update Pi with that package manager, for example `nix profile upgrade pi`. Package and model catalog updates still work.\n\n### Aliases and command options\n\n- `pi uninstall <source>` is an alias for `pi remove <source>`.\n- `pi update --self`, `pi update self`, and `pi update pi` are aliases for `pi update`.\n- `pi update --extension <source>` is an alias for `pi update <source>`.\n- `-a`, `--approve` trusts project-local files for one command. `-na`, `--no-approve` ignores trust-gated project-local files.\n- Append `-h` or `--help` to a command for its exact usage and option constraints.\n\n## Credential commands\n\n```sh\npi auth check --provider openai --json\n```\n\nAuthentication commands require `--provider <provider>` or `--model <model>`. See [Providers](providers.md) for supported methods.\n\n| Command | Description |\n|---|---|\n| `pi auth check` | Print `ready`, `not_ready`, or `invalid`; exit with status `0`, `1`, or `2`, respectively |\n| `pi auth print-api-key` | Print the resolved API key |\n| `pi auth print-bearer-token` | Print a resolved OAuth bearer token |\n\n| Option | Applies to | Description |\n|---|---|---|\n| `--provider <provider>` | All | Resolve credentials for a provider |\n| `--model <model>` | All | Resolve credentials from a model; may be combined with `--provider` |\n| `--json` | `auth check` | Write the structured result as JSON |\n| `--credentials` | `auth check` | Emit the resolved credential when ready |\n| `--no-refresh` | `auth check` | Do not refresh expired OAuth credentials; refresh is the default |\n| `--min-expiry <duration>` | `print-bearer-token` | Require remaining token lifetime using `ms`, `s`, `m`, or `h`, such as `30m` |\n\nCredential-printing commands write secrets to stdout.\n\n## MCP commands\n\nThese commands work outside a session, so agents can run them through `bash`. See [MCP Servers](mcp.md).\n\n| Command | Description |\n|---|---|\n| `pi mcp add <server> [options] -- <command> [args...]` | Add or replace a stdio server in `mcp.json`; `--env KEY=VALUE` (repeatable) and `--cwd <dir>` set its environment and working directory. Arguments after the command are passed to it |\n| `pi mcp add <server> [options] --url <url>` | Add or replace a streamable HTTP server; `--header KEY=VALUE` (repeatable), `--bearer-token-env-var <NAME>` (sends `Authorization: Bearer ${NAME}`), `--oauth-client-id`, `--oauth-client-secret`, `--oauth-callback-port`, and `--oauth-client-name` configure authentication |\n| `pi mcp remove <server>` | Remove a server from `mcp.json`; stored OAuth credentials are kept |\n| `pi mcp list [--json]` | Connect to every enabled server and print its state, tools, and errors; exit with `1` when a config entry is invalid or an enabled server is not connected |\n| `pi mcp login <server> [--timeout <seconds>]` | Sign in to an OAuth server: open the authorization page and wait for the browser; a terminal also accepts the pasted redirect URL. `--timeout` limits the whole sign-in, including requests to the authorization server (default 300 seconds) |\n| `pi mcp logout <server>` | Delete the stored OAuth credentials of a server |\n\n`add` and `remove` change `~/.pi/agent/mcp.json`, or `.pi/mcp.json` in the current directory with `--local` (`-l`). `add` also takes `--exposure <mode>` (see [Exposure](mcp.md#exposure)) and `--description <text>` and does not connect; run `pi mcp list` to check the server.\n\nProject `.pi/mcp.json` files are only read for projects that are already trusted.\n","packages/coding-agent/docs/codemode.md":"# Codemode\n\nThe `codemode` tool lets the model write a JavaScript script that calls pi's other tools and runs non-LLM models, such as classifiers and image models. Only the script's output reaches the model, so a script can run calls in parallel and filter large results before the model sees them. To turn it on, see [Enable codemode](cli.md#enable-codemode).\n\n## Scripts\n\nThe tool input is raw JavaScript source, not JSON and not a markdown code fence. It runs as the body of an async function in a QuickJS sandbox, so top-level `await` and `return` work. The sandbox has no Node APIs, file system, network, or timers; scripts reach the outside world only through tools and `models`.\n\nA script may start with an options line. It must be the first line; an options line anywhere else fails the call before the script runs:\n\n```js\n// @options: {\"max_output_tokens\": 2000, \"timeout_ms\": 60000}\n```\n\n- `max_output_tokens` (default 10000) limits the output. Longer output keeps its start and end, and the full text is written to a temp file whose path is included in the result. A script fails when its output passes 16777216 characters of text and base64 image data or 100000 `text()`, `image()`, and `console` calls; write large data to a file with a tool instead.\n- `timeout_ms` is a hard deadline for the whole script, including any wait for a free worker. It is unset by default. Image generation can take minutes, so do not set a short deadline for scripts that generate images.\n\nThe result starts with `Script completed` or `Script failed`, the wall time, and the output. Text and image items appear in order, each on its own line. When the output has more than one text item (from `text()` or `return`), each starts with a `==> text N/M <==` line. `console` calls follow in one `<console_output>` block with one line per call. A failed script keeps its partial output, followed by `Script error:` and the error. Tool calls are real: calls made before a failure are not undone. Calls still running when the script ends are cancelled, and unawaited promises are discarded.\n\n## Globals\n\n| Global | Purpose |\n|---|---|\n| `tools.<name>(args)` | Call a tool. See [Call tools](#call-tools). |\n| `text(value)` | Add a text item to the output. Strings are added as is, other values as JSON. |\n| `image(value)` | Add an image to the output: a base64 `data:` URL, an `{ image_url }` object, or an image block `{ type: \"image\", data, mimeType }` such as those returned by MCP tools and `models.generateImages()`. Remote URLs are not supported. PNG, JPEG, GIF, and WebP are accepted. Each image is also saved to a temp file, and the result names the path before the image. |\n| `console.log(...)` | Add a line to the `<console_output>` block after the other output. Arguments are joined with spaces; `info`, `warn`, `error`, and `debug` do the same. |\n| `return value` | A top-level `return` adds the value like `text()`. |\n| `exit()` | End the script successfully. |\n| `store(key, value)` / `load(key)` | Keep small JSON values across `codemode` calls. See [Store values](#store-values). |\n| `ALL_TOOLS` | Every callable tool as `{ name, description }`, including tools the description does not list. |\n| `searchTools(query, { limit?, namespace? })` | Rank callable tools by relevance (BM25, default limit 8). Resolves to `{ name, description }[]`. |\n| `describeTool(name)` | Resolves to a tool's description and TypeScript declaration, or `undefined`. |\n| `describeNamespace(name)` | Resolves to `{ name, description?, instructions?, tools }` for a namespace such as an MCP server, or `undefined`. |\n| `models` | List and run non-LLM models. See [Models](#models). |\n\n## Call tools\n\nEvery tool the session can call is a method of `tools`, named by its identifier: characters that are not valid in a JavaScript identifier become `_`, so the MCP tool `mcp__dev-radius__search` is `tools.mcp__dev_radius__search`. Each method takes one object with the tool's arguments.\n\nWhat a call resolves to depends on the tool:\n\n- Tools with an output schema resolve to a structured value. `bash` resolves to `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`, also for non-zero exit codes. Its `output` is not limited to the 2000 lines or 50KB the model sees: it holds up to 1 MiB, and longer output keeps its first and last 512 KiB around an omission marker, with `truncated` set and the full output in `full_output_path`.\n- MCP tools resolve to their `CallToolResult`, including `isError` and `structuredContent`.\n- `read` resolves to the file's text, or for an image to an image block `{ type: \"image\", data, mimeType, note }` that `image()` shows. `data` is the base64 image the model would see and `note` the text that goes with it, such as resize hints.\n- Other tools, such as `edit` and `write`, resolve to their text output.\n\nA call that fails, is blocked, or gets invalid arguments rejects with an `Error` that carries the tool's error text. Use `Promise.allSettled()` to keep the results of the calls that succeed.\n\nThe `codemode` description lists tools with their TypeScript declarations, grouped by namespace (for example one MCP server). Tools with `deferred` exposure, which includes MCP tools with the default `codemode` exposure, are not listed, so the description stays the same while MCP servers connect. Listed declarations share a budget of 3000 estimated tokens (`codemode.inlineBudget` in [settings](settings.md#tools)). Scripts find the other tools with `searchTools()`, `describeTool()`, `describeNamespace()`, or by filtering `ALL_TOOLS`.\n\nWhile `codemode` is active, `codemode.mode` in [settings](settings.md#tools) decides how the other tools are presented. With `on` (default) declared tools stay declared, and their descriptions say how to call them from scripts. With `only` they are hidden from the model and listed in the `codemode` description instead, so the model calls them through scripts. Tool declarations in the `codemode` description, `describeTool()`, and `ALL_TOOLS` carry the tools' prompt guidelines, since the system prompt rules only cover declared tools.\n\n## Store values\n\n`store(key, value)` keeps a JSON value under a string key for later `codemode` calls; storing `undefined` deletes the key. `load(key)` returns the value, or `undefined`. Writes are kept only when the script succeeds: each successful script that stores values appends a `codemode-store` custom entry to the session, so resumed sessions keep the values and each branch sees only the values written on its path.\n\nThe store is for small state such as IDs, cursors, or summaries. One value may have at most 262144 characters of JSON and all values together at most 1048576. Do not store image data; show images with `image()`, which also saves them to a temp file.\n\n## Models\n\n`models` reaches the model catalog and runs non-LLM models with the session's credentials: classifiers, which answer typed questions about JSON state and, for some models, images, and image models, which generate images. Chat models are listed but cannot be run from scripts. Which classifier and image models exist is described in [Use classifier models](models.md#use-classifier-models) and [Use image models](models.md#use-image-models).\n\n```ts\ntype ModelType = \"chat\" | \"image\" | \"classifier\";\n\n/** A catalog entry. `provider` and `id` identify it; other fields depend on the type. */\ninterface ModelInfo {\n  type?: ModelType;\n  provider: string;\n  id: string;\n  name: string;\n  api: string;\n  input: (\"text\" | \"image\")[];\n  contextWindow?: number;\n  [key: string]: unknown;\n}\n\ndeclare const models: {\n  /** Every known model of a type, optionally for one provider. */\n  getModelsOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** Models of a type whose provider has working credentials. */\n  getAvailableOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** One catalog entry, or undefined. */\n  getModelOfType(type: ModelType, provider: string, id: string): Promise<ModelInfo | undefined>;\n  /** Answer `context.questions` about `context.state`; answers are in `result.answers` by question ID. */\n  classify(model: ModelInfo, context: ClassifierContext): Promise<ClassifierResult>;\n  /** Generate images from `context.input` text and image blocks; show `result.output` blocks with image(). Can take minutes. */\n  generateImages(model: ModelInfo, context: ImagesContext): Promise<ImagesResult>;\n};\n```\n\n`classify()` and `generateImages()` use only the `provider` and `id` of `model`, so `{ provider, id }` works as well. They do not throw on provider errors: check `stopReason` and `errorMessage`. At most four such calls run at once per script; more calls wait for a free slot, so `Promise.all()` over many items is fine. Their usage is added to the `codemode` tool result and counts toward the session cost.\n\nModel IDs differ between providers, for example `typesafe/jev-latest` and `openrouter/typesafe/jev-1.13`. Use `models.getAvailableOfType(type)` to find the IDs that work with the current credentials.\n\n### Classify\n\n```ts\ninterface ClassifierContext {\n  /** The data to classify. */\n  state: Record<string, unknown>;\n  /** Images judged together with `state`. Only models whose `input` includes \"image\" accept them. */\n  images?: { type: \"image\"; data: string; mimeType: string }[];\n  /** Questions by ID. One call answers all of them. */\n  questions: Record<string, ClassifierQuestion>;\n}\n\ntype ClassifierQuestion =\n  /** Pick one label. `criteria` maps each label to what it means. */\n  | { type: \"choice\"; instructions: string; criteria: Record<string, string> }\n  /** Score on an ordered scale. `criteria` describes each level, lowest first. */\n  | { type: \"score\"; instructions: string; criteria: string[] }\n  /** Yes or no. */\n  | { type: \"bool\"; instructions: string; criteria: { true: string; false: string } };\n\ninterface ClassifierResult {\n  provider: string;\n  model: string;\n  /** Answers by question ID. */\n  answers: Record<string, ClassifierAnswer>;\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype ClassifierAnswer =\n  | { type: \"choice\"; choice: string; probabilities: Record<string, number>; confidence: number }\n  /** `score` is the expected level index, from 0 to `criteria.length - 1`. */\n  | { type: \"score\"; score: number; confidence: number }\n  /** Probability of `true`. */\n  | { type: \"bool\"; probability: number };\n\n/** Token counts and cost in USD, when the service reports them. */\ntype ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };\n```\n\nClassify several items by calling `classify()` once per item. This script sorts feedback messages, for example ones a tool returned earlier in the script:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst results = await Promise.all(\n  messages.map((message) =>\n    models.classify(jev, {\n      state: { message },\n      questions: {\n        sentiment: {\n          type: \"choice\",\n          instructions: \"How does the user feel about the product?\",\n          criteria: { positive: \"Satisfied or happy\", negative: \"Unhappy or frustrated\", neutral: \"Neither\" },\n        },\n        urgency: {\n          type: \"score\",\n          instructions: \"How urgently does this need a reply?\",\n          criteria: [\"no reply needed\", \"reply this week\", \"reply today\"],\n        },\n      },\n    }),\n  ),\n);\nreturn results.map((result, i) =>\n  result.stopReason === \"stop\"\n    ? { message: messages[i], sentiment: result.answers.sentiment.choice, urgency: result.answers.urgency.score }\n    : { message: messages[i], error: result.errorMessage },\n);\n```\n\nClassifiers whose `input` includes `\"image\"` also judge images. `tools.read()` returns an image file as an image block that `images` accepts. Other classifiers return an error result when `images` is not empty.\n\n```js\nconst luna = await models.getModelOfType(\"classifier\", \"openai\", \"gpt-6-luna\");\nconst photo = await tools.read({ path: \"screenshot.png\" });\nconst result = await models.classify(luna, {\n  state: { task: \"Settings page redesign\" },\n  images: [photo],\n  questions: {\n    broken: {\n      type: \"bool\",\n      instructions: \"Does the screenshot show a broken layout?\",\n      criteria: { true: \"Overlapping, cut-off, or misaligned elements\", false: \"Clean layout\" },\n    },\n  },\n});\n```\n\n### Generate images\n\n```ts\ninterface ImagesContext {\n  /** The prompt as text blocks, plus image blocks to edit or use as references. */\n  input: (TextBlock | ImageBlock)[];\n}\n\ninterface ImagesResult {\n  provider: string;\n  model: string;\n  /** Generated images, and text blocks for models that also return text. */\n  output: (TextBlock | ImageBlock)[];\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype TextBlock = { type: \"text\"; text: string };\n/** `data` is base64. */\ntype ImageBlock = { type: \"image\"; data: string; mimeType: string };\n```\n\nShow generated images with `image(block)`. Do not print `data` with `text()`, `console`, or `return`: it is large and the model cannot read it as text. `image()` also saves each image to a temp file and puts its path in the result, so a later turn can copy or move the file.\n\n```js\n// @options: {\"timeout_ms\": 300000}\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) {\n  if (block.type === \"image\") image(block);\n  else text(block.text);\n}\n```\n\n## Limits\n\n- A script's VM has 256 MB of memory. Running out throws `InternalError: out of memory`; filter or aggregate large data instead of accumulating it.\n- On Bun, stopping a script or reaching its deadline ends the call promptly, but a built-in that does not poll QuickJS's interrupt handler can keep computing in its worker until it returns.\n- At most two codemode workers may be alive across all sessions in one pi process; `PI_CODEMODE_MAX_WORKERS` changes the limit. A cancelled worker occupies its slot until it exits. At capacity, new scripts wait for a free worker in arrival order. A script that gets no worker within 60 seconds, or before its `timeout_ms`, fails without running.\n- A script that waits on a promise that can never settle (no tool call pending) fails immediately, since there are no timers.\n- Scripts cannot start other `codemode` scripts.\n","packages/coding-agent/docs/compaction.md":"# Compaction Reference\n\nThis reference describes automatic compaction, branch summarization, persisted entries, and extension hooks. For the user workflow, see [Sessions and Context](sessions.md#manage-conversation-context).\n\n**Source files** ([pi](https://github.com/earendil-works/pi)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - Auto-compaction logic\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - Branch summarization\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - Shared utilities (file tracking, serialization)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts) - Entry types (`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) - Extension event types\n\nFor TypeScript definitions in your project, inspect `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Overview\n\nPi has two summarization mechanisms:\n\n| Mechanism | Trigger | Purpose |\n|-----------|---------|---------|\n| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |\n| Branch summarization | `/tree` navigation | Preserve context when switching branches |\n\nBoth use closely related structured formats and track file operations cumulatively. Summarization requests disable prompt-cache writes because these one-off prompts are unlikely to be reused.\n\n## Compaction\n\n### When It Triggers\n\nAuto-compaction triggers when:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nBy default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`). It only decides when compaction starts. Summary requests use the model's output limit, reduced when needed so input and output fit the context window.\n\nDuring a multi-turn agent run, Pi checks the canonical projected context after tools finish and their results are appended, before starting the next assistant response. If the threshold is crossed, Pi compacts during `prepareNextTurn`, then performs the existing catch-up steering poll before `turn_start`. It skips this between-turn check when the completed tool batch terminates the run and no queued message requires another response. Pi also checks before a new user prompt and performs final-attempt overflow recovery after the low-level run ends.\n\nA provider context-overflow error or an early final `stopReason: \"length\"` can select one compact-and-retry recovery attempt. Length responses with tool calls retain their synthetic failed tool results and follow the ordinary tool/queue scheduler rather than forcing the run to end.\n\nYou can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary.\n\n### How It Works\n\n1. **Find cut point**: Walk backwards through the finalized session projection, accumulating token estimates until `keepRecentTokens` (default 20k, configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`) is reached\n2. **Extract messages**: Collect projected messages from the previous kept boundary (or session start) up to the cut point\n3. **Generate summary**: Call LLM to summarize with structured format, passing the previous summary as iterative context when present\n4. **Append entry**: Save `CompactionEntry` with summary and `firstKeptEntryId`\n5. **Rebuilds context**: Session rebuilds the context for the next request, using summary + messages from `firstKeptEntryId` onwards\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\nOn repeated compactions, the summarized span starts at the previous compaction's kept boundary (`firstKeptEntryId`), not at the compaction entry itself, falling back to the entry after the previous compaction if that kept entry cannot be found in the path. A retain-none compaction records its own ID as `firstKeptEntryId`; repeated compaction starts after that entry. This preserves messages that survived the earlier compaction by including them in the next summarization pass as well. Pi also recalculates `tokensBefore` from the rebuilt, context-edited session projection before writing the new `CompactionEntry`, so the token count reflects the actual pre-compaction context being replaced. Omitted raw entries remain stored but do not affect cut selection, summaries, checkpoints, or token estimates.\n\n### Overflow and Length Recovery Ordering\n\nRecovery preserves the existing lifecycle and queue order. The completed attempt remains visible to `turn_end` and `agent_end`; post-run recovery then repairs persisted model context before a fresh retry:\n\n```text\npersist final assistant response\n→ extension/public turn_end\n→ extension/public agent_end\n→ append context_edit omissions for the selected attempt\n→ for overflow/length: run session_before_compact and append compaction on success\n→ start the retry as a fresh run\n```\n\nIf recovery compaction fails or is cancelled, Pi keeps the omission edits, appends no compaction, and schedules no internal retry. Existing queued work remains governed by ordinary steering and follow-up rules. `agent_before_settle` sees the repaired projection after recovery processing. Raw transcript history, exports, billing totals, and history-search extensions can still inspect the omitted attempt.\n\n### Split user-message spans\n\nA user-message span starts with a user message and includes all turns until the next user message. Normally, compaction cuts at user-message boundaries.\n\nWhen one user-message span exceeds `keepRecentTokens`, the cut point lands within that span at an assistant message. This is a split user-message span:\n\n```\nSplit user-message span (one span exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no earlier user-message spans)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\nFor split user-message spans, Pi generates two summaries and merges them:\n1. **History summary**: Previous context (if any)\n2. **User-message-span prefix summary**: The early part of the split user-message span\n\n### Cut Point Rules\n\nValid cut points are:\n- User messages\n- Assistant messages\n- BashExecution messages\n- Custom messages (custom_message, branch_summary)\n\nNever cut at tool results (they must stay with their tool call).\n\nPreparation advances the kept boundary into a context-invisible suffix only when that suffix contains an omitted assistant attempt and no unomitted context-producing entries. Recovery `context_edit` omissions satisfy this rule; intrinsically context-invisible metadata may coexist with them. Metadata alone and newly appended custom messages do not move the cut. A replacement edit affecting the candidate input or summarized prefix also blocks advancement because the omitted assistant answered the pre-edit input; replacements of suffix entries that are ultimately omitted remain safe. This allows an over-budget recovered input to be summarized while retaining the edits that keep the abandoned attempt omitted, without making bookkeeping change whether new model input is preserved verbatim.\n\n### CompactionEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions can store any JSON-serializable data in `details`. The default compaction tracks file operations, but custom extension implementations can use their own structure. Generated and extension-provided summaries store their LLM `usage` when available so session totals include summarization work.\n\nSee [`prepareCompaction()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) and [`compact()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) for the implementation. For direct programmatic summarization, `generateSummary()` returns the summary text and `generateSummaryWithUsage()` returns `{ text, usage }`.\n\n## Branch Summarization\n\n### When It Triggers\n\nWhen you use `/tree` to navigate to a different branch, Pi offers to summarize the work you're leaving. This injects context from the left branch into the new branch.\n\n### How It Works\n\n1. **Find common ancestor**: Deepest node shared by old and new positions\n2. **Collect entries**: Walk from old leaf back to common ancestor\n3. **Prepare with budget**: Include messages up to token budget (newest first)\n4. **Generate summary**: Call LLM with structured format\n5. **Append entry**: Save `BranchSummaryEntry` at navigation point\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### Cumulative File Tracking\n\nDefault compaction and branch summarization track files cumulatively. Both extract file operations from tool calls in the messages being summarized. Compaction also carries file lists from the previous Pi-generated compaction. Branch summarization carries file lists from Pi-generated branch summaries in the entries it summarizes.\n\nFile tracking therefore accumulates across default compactions and nested default branch summaries. Pi does not automatically carry file lists from extension-generated summaries whose `fromHook` field is `true`; extensions manage their own `details` format.\n\n### BranchSummaryEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nSame as compaction, extensions can store custom data in `details`.\n\nSee [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), and [`generateBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) for the implementation.\n\n## Summary Format\n\nBoth formats include Goal, Constraints & Preferences, Progress, Key Decisions, and Next Steps. Compaction summaries also include Critical Context. Branch summaries stop after Next Steps. Pi appends file lists to either format when relevant.\n\nCompaction summaries use this format:\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### Message Serialization\n\nBefore summarization, messages are serialized to text via [`serializeConversation()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts):\n\n```\n[User]: What they said\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\nThis prevents the model from treating it as a conversation to continue.\n\nThinking blocks are omitted. Providers return summaries of the real reasoning, and replayed thinking is often not part of the model's live context, so including it could make the summarization request larger than the context it summarizes.\n\nTool results are truncated to 2000 characters during serialization. Content beyond that limit is replaced with a marker indicating how many characters were truncated. This keeps summarization requests within reasonable token budgets, since tool results (especially from `read` and `bash`) are typically the largest contributors to context size.\n\n## Custom Summarization via Extensions\n\nExtensions can intercept and customize both compaction and branch summarization. See [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for event type definitions.\n\n### session_before_compact\n\nFired before auto-compaction or `/compact`. Can cancel or provide custom summary. See `SessionBeforeCompactEvent` and `CompactionPreparation` in the types file.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - user-message-span prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - effective settings after applying model overrides\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### Converting Messages to Text\n\nTo generate a summary with your own model, convert messages to text using `serializeConversation`:\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\nSee [custom-compaction.ts](../examples/extensions/custom-compaction.ts) for a complete example using a different model.\n\n### session_compact_failed\n\nFired when manual or automatic compaction fails or is aborted. This is useful for telemetry extensions that need to pair `session_before_compact` attempts with terminal outcomes.\n\n```typescript\npi.on(\"session_compact_failed\", async (event, ctx) => {\n  const { reason, errorMessage, aborted, willRetry, fromExtension } = event;\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // errorMessage - present for non-abort failures\n  // aborted - true for canceled/aborted compactions\n  // willRetry - whether the aborted turn would have retried after compaction\n  // fromExtension - whether extension-provided compaction content was being used\n});\n```\n\n### session_before_tree\n\nFired before `/tree` navigation. Always fires regardless of whether user chose to summarize. Can cancel navigation or provide custom summary.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\nSee `SessionBeforeTreeEvent` and `TreePreparation` in the types file.\n\n## Settings\n\nConfigure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`:\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `enabled` | `true` | Enable auto-compaction |\n| `reserveTokens` | `16384` | Free context tokens below which auto-compaction starts |\n| `keepRecentTokens` | `20000` | Recent tokens to keep (not summarized) |\n\nDisable auto-compaction with `\"enabled\": false`. You can still compact manually with `/compact`.\n\n### Per-model overrides\n\nUse `compaction.modelOverrides` to tune token budgets for different models:\n\n```json\n{\n  \"compaction\": {\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000,\n    \"modelOverrides\": {\n      \"some-provider/big-model\": {\n        \"reserveTokens\": 400000\n      }\n    }\n  }\n}\n```\n\nFor a model with a 1M context window, this override triggers compaction above 600K tokens and keeps the ordinary 20000 recent tokens. Other models retain the ordinary 16384-token reserve.\n\nKeys are exact, case-sensitive `provider/modelId` values, including any slashes within the model ID. Each `reserveTokens` and `keepRecentTokens` value falls back independently from the model override to the ordinary setting to the built-in default. Values must be non-negative safe integers. Invalid values in the matching model override produce an error when read; only omitted fields fall back to the ordinary setting. Model override entries must be objects. Invalid ordinary token settings produce an error when read, even if the active model has a valid override. Only omitted ordinary values use built-in defaults. `enabled` remains global, not model-specific.\n\nThese resolved values are used for manual compaction, all automatic threshold checks, overflow recovery, and extension-visible `preparation.settings`. Model switches affect subsequent checks and compactions without changing ordinary settings. Compaction already in progress uses the model and settings captured for that operation. Branch summarization settings are unaffected.\n\nOverrides work in both global and project settings. The files merge recursively before lookup, so a global model-specific value beats a project-wide fallback; a project must override that model entry to change it. See [Settings](settings.md#per-model-compaction-overrides) for details.\n","packages/coding-agent/docs/configuration.md":"# Configuration\n\nPi supports user-level and project configuration. User-level configuration lives in the agent directory, which defaults to `~/.pi/agent`. Project configuration lives in `.pi` under the working directory and loads after [project trust](security.md#understand-project-trust) is granted. The only exception is `sessionDir`, which Pi reads before resolving trust so it can locate sessions.\n\nIn interactive mode, use `/settings` to change common preferences. For other options, ask Pi to update the configuration or edit the relevant files directly. Run `/reload` after manually changing settings, keybindings, instructions, or resources.\n\n## Agent directory\n\nThe agent directory is shown as `<agent-dir>` below. Set its location with the `PI_CODING_AGENT_DIR` environment variable or the SDK's [`agentDir`](sdk.md) option.\n\n| Path | Responsibility |\n|---|---|\n| `<agent-dir>/settings.json` | User-level [settings](settings.md), including preferences, defaults, resource paths, and Pi package declarations. |\n| `<agent-dir>/keybindings.json` | Custom terminal UI and application [keybindings](keybindings.md). |\n| `<agent-dir>/mcp.json` | [MCP servers](mcp.md) available in every project. |\n| `<agent-dir>/models.json` | [Compatible endpoints, models, and model overrides](models.md#configure-a-compatible-endpoint). |\n| `<agent-dir>/auth.json` | Saved API keys and OAuth credentials. |\n| `<agent-dir>/AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, or `CLAUDE.MD` | User instructions applied across working directories. |\n| `<agent-dir>/SYSTEM.md` | Replaces Pi’s default system prompt. |\n| `<agent-dir>/APPEND_SYSTEM.md` | Adds instructions to Pi’s system prompt. |\n| `<agent-dir>/extensions/` | User [extensions](extensions.md). |\n| `<agent-dir>/skills/` | User [skills](skills.md) and supporting files. |\n| `<agent-dir>/prompts/` | User [prompt templates](prompt-templates.md) exposed as slash commands. |\n| `<agent-dir>/themes/` | User [theme](themes.md) files. |\n\n## Project `.pi` directory\n\n| Path | Responsibility |\n|---|---|\n| `.pi/settings.json` | Project-level [settings](settings.md), resource paths, and Pi package declarations. |\n| `.pi/mcp.json` | Project [MCP servers](mcp.md). |\n| `.pi/SYSTEM.md` | Replaces the system prompt for the project. |\n| `.pi/APPEND_SYSTEM.md` | Adds project-specific instructions to the system prompt. |\n| `.pi/extensions/` | Project extensions. |\n| `.pi/skills/` | Project skills and supporting files. |\n| `.pi/prompts/` | Project prompt templates exposed as slash commands. |\n| `.pi/themes/` | Project theme files. |\n\nFor `SYSTEM.md` and `APPEND_SYSTEM.md`, the trusted project file takes precedence over the corresponding agent-directory file. Files with the same name are not combined.\n\n## Context files\n\nContext files are separate from project `.pi` configuration. Pi loads them from the agent directory, the working directory, and its parent directories. A context file applies whenever Pi runs in its directory or anywhere below it.\n\nAn `AGENTS.override.md` replaces `AGENTS.md` or `CLAUDE.md` only in the same directory. It does not suppress context files from the agent directory or other directories.\n\nContext-file discovery does not require project trust.\n","packages/coding-agent/docs/containerization.md":"# Run Pi in an isolated environment\n\nUse an isolated environment to limit the files, credentials, processes, and network services that generated commands can access or affect.\n\nYou can isolate the complete Pi process or keep Pi on the host and route selected tools into an isolated environment.\n\n## Choose an isolation method\n\n| Method | Where Pi runs | What is isolated | Credential handling | Best for |\n|---|---|---|---|---|\n| Plain Docker | Container | Pi, built-in tools, `!` commands, and extensions | Credentials passed into the container | A straightforward local container boundary |\n| Docker Sandboxes | Managed sandbox | Pi, built-in tools, `!` commands, and extensions | Provider credentials remain on the host and are substituted by the proxy | Managed local isolation without exposing the real provider key |\n| OpenShell | Local or remote sandbox | Pi, built-in tools, `!` commands, and extensions | Policy-controlled credentials and inference routing | Filesystem, process, network, and credential policies |\n| Gondolin extension | Host | Built-in tools and `!` commands | Stored Pi credentials remain on the host, but commands inherit host environment variables | A local micro-VM for tool execution while retaining the host interface |\n\nThe method changes where extensions run. When the complete Pi process runs inside an isolated environment, its extensions run there too. When host Pi delegates built-in tools through Gondolin, other extension tools still run on the host unless they also delegate their work.\n\n## Decide what Pi can access\n\nAn isolated process can still affect resources you expose to it:\n\n- A read-write host mount lets Pi modify those host files.\n- Mounting `~/.pi/agent` exposes your Pi credentials, settings, extensions, and sessions.\n- Environment variables passed into a container are available to processes inside it.\n- Network access may allow code or tool output to leave the environment.\n- Tool-only isolation does not constrain the host Pi process or extension tools that do not use the isolated backend.\n\nExpose only the working folder, credentials, and network destinations needed for the task. Use read-only mounts or copy files into and out of the environment when you do not want writes to affect the host.\n\n## Run Pi in plain Docker\n\nPlain Docker provides the simplest whole-process container boundary.\n\n### Build the image\n\nCreate `Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\nBuild it from the directory containing the file:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n```\n\n### Start Pi\n\nFrom the working folder you want Pi to access, run:\n\n```bash\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\nReplace `ANTHROPIC_API_KEY` with the credential required by your provider. The named `pi-agent-home` volume keeps container-local settings, credentials, and sessions between runs.\n\nDo not mount the host's `~/.pi/agent` unless the container should have access to your host Pi configuration and credentials.\n\n### Verify the workspace\n\nInside Pi, run:\n\n```text\n!pwd\n```\n\nThe command should report `/workspace`. Changes under `/workspace` write through to the mounted host folder. Remove the bind mount or use a read-only mount when that is not acceptable.\n\n## Run Pi with Docker Sandboxes\n\n[Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) runs the complete Pi process inside a managed sandbox. Its proxy can keep the real provider credential on the host and substitute it when requests leave the sandbox.\n\nConfigure credentials before creating the sandbox. Do not run `/login` inside the sandbox because that writes a real credential into it.\n\n### Use a Claude Pro or Max token\n\nGenerate the token with `claude setup-token` on a machine with Claude Code. If an `anthropic` secret is already configured, remove it first so the proxy does not add an API-key header alongside the bearer token:\n\n```bash\nsbx secret rm anthropic\n\nsbx secret set-custom \\\n  --host api.anthropic.com \\\n  --env ANTHROPIC_OAUTH_TOKEN \\\n  --placeholder 'sk-ant-oat01-{rand}'\n```\n\n`sbx secret set-custom` reads the real token from standard input. The sandbox receives an OAuth-shaped placeholder, which the proxy replaces only for requests to the configured host.\n\nFor an Anthropic API key, use `sbx secret set anthropic` instead.\n\n### Start Pi\n\nRun this from the working folder you want mounted:\n\n```bash\nsbx run --kit \"docker.io/sbx/pi-kit:latest\" pi\n```\n\nFor an existing sandbox, run Pi non-interactively with:\n\n```bash\nsbx exec <sandbox-name> -- pi -p \"list the failing tests\"\n```\n\nSee the [Pi kit documentation](https://github.com/docker/sbx-kits-contrib/tree/main/pi) for other providers, troubleshooting, and image pinning.\n\n## Run Pi with OpenShell\n\n[NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) provides local or remote sandboxes with filesystem, process, network, credential, and inference policies.\n\n### Select a gateway\n\nEvery sandbox requires an active gateway:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\n### Create the sandbox\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nPi, its built-in tools, `!` commands, and extension tools run inside the OpenShell boundary.\n\n### Transfer files to a remote sandbox\n\nA remote gateway does not bind-mount your host working folder. Clone the repository inside the sandbox or transfer files explicitly:\n\n```bash\nopenshell sandbox upload pi-sandbox ./working-folder /workspace\nopenshell sandbox download pi-sandbox /workspace/working-folder ./working-folder-out\n```\n\nOpenShell inference routing can keep raw model credentials outside the sandbox. When configured, point Pi at the corresponding OpenAI-compatible or Anthropic-compatible endpoint exposed by the gateway.\n\n## Route tools through Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) is a local Linux micro-VM. Its example extension keeps the Pi process and file-based provider credentials on the host while routing the built-in tools and user `!` commands into the VM.\n\nCommands inside the VM inherit the host process environment. Provider keys supplied through environment variables can therefore be visible inside the VM. Do not use this pattern as a credential boundary unless you remove sensitive variables or change the extension's environment handling.\n\nGondolin requires Node.js 23.6 or newer and QEMU installed through your operating-system package manager.\n\n### Install the extension\n\nFrom a Pi source checkout:\n\n```bash\nmkdir -p ~/.pi/agent/extensions\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\n### Start Pi\n\nRun Pi from the working folder you want mounted:\n\n```bash\ncd /path/to/working-folder\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nThe extension mounts the host working folder at `/workspace` in the VM and overrides `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls`. File changes under `/workspace` write through to the host.\n\nOther extension tools still run on the host unless they explicitly delegate their operations. Review the [Gondolin example](../examples/extensions/gondolin/) before adding tools that could bypass the VM boundary.\n","packages/coding-agent/docs/custom-provider.md":"# Custom Providers\n\nA provider extension connects Pi to a model service that needs custom authentication, model discovery, request handling, or streaming. If the service already speaks a supported API, configure it in `models.json` instead.\n\nProvider extensions run inside Pi and can inspect credentials, prompts, tool definitions, model responses, and usage. Treat them as trusted code and avoid logging secrets or provider payloads.\n\n## Choose the smallest integration\n\n| Requirement | Use |\n|---|---|\n| Add models behind a supported API | [`models.json`](models.md#configure-a-compatible-endpoint) |\n| Change an existing provider endpoint or headers | `models.json` or a small provider extension |\n| Discover models dynamically | A provider with `refreshModels` |\n| Add a `/login` flow | A provider with native or legacy OAuth configuration |\n| Implement an unsupported wire protocol | A provider with `stream` or `streamSimple` |\n\nA provider extension is an [extension](extensions.md), so it follows the same loading, trust, reload, and error behavior.\n\n## Register a provider\n\nCall `pi.registerProvider()` from the extension factory. Pi waits for asynchronous factories before startup continues, so providers registered there are available to startup model selection and `pi --list-models`.\n\nThere are two registration forms:\n\n- Register a complete `Provider` from `@earendil-works/pi-ai` for native authentication, filtering, discovery, refresh, and streaming behavior.\n- Register a provider name with `ProviderConfig` for the legacy configuration form used by existing extensions.\n\nPrefer a complete provider for new integrations that own more than static endpoint and model metadata. Pi composes `models.json` overrides above a registered native provider.\n\nRegistering only `baseUrl` or `headers` for an existing provider preserves its built-in models. Supplying `models` in the legacy form replaces that provider's models across chat, image, and classifier operations. An omitted `type` means `\"chat\"`; image and classifier models require explicit discriminants and implementations keyed by their `api` values through the `images` and `classifiers` fields.\n\n### Redirect an existing provider\n\nRegister only the fields that should change. This keeps the provider's built-in catalog, authentication, API implementation, and compatibility settings:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://gateway.example.com/anthropic\",\n    headers: {\n      \"X-Gateway-Key\": \"$GATEWAY_API_KEY\",\n    },\n  });\n}\n```\n\nUse this form for a proxy or gateway that preserves the upstream protocol. Do not copy the built-in model list into the extension because doing so turns a small endpoint override into a catalog replacement that can drift.\n\n### Register a compatible endpoint\n\nWhen a service implements a supported protocol but has its own model catalog, register the endpoint and only the models the service actually exposes:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"acme\", {\n    name: \"Acme AI\",\n    baseUrl: \"https://api.acme.example/v1\",\n    apiKey: \"$ACME_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        type: \"chat\",\n        id: \"acme-chat\",\n        name: \"Acme Chat\",\n        reasoning: false,\n        input: [\"text\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 8192,\n      },\n    ],\n  });\n}\n```\n\nPrefer `models.json` for this case when no executable extension behavior is needed. Use an extension when registration depends on runtime discovery, custom authentication, or code shared with other extension features.\n\n### Register mixed-operation models\n\nA mixed-operation provider can register non-chat models and their implementations together:\n\n```typescript\npi.registerProvider(\"media-tools\", {\n  apiKey: \"$MEDIA_TOOLS_API_KEY\",\n  models: [\n    {\n      type: \"image\",\n      id: \"image-v1\",\n      name: \"Image V1\",\n      api: \"media-images\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      output: [\"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n    },\n    {\n      type: \"classifier\",\n      id: \"classifier-v1\",\n      name: \"Classifier V1\",\n      api: \"media-classifier\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 64000,\n    },\n  ],\n  images: {\n    \"media-images\": { generateImages: async (model, context, options) => result },\n  },\n  classifiers: {\n    \"media-classifier\": { classify: async (model, context, options) => result },\n  },\n});\n```\n\nModel-level `baseUrl` values take precedence over the provider endpoint. If no `models` list is supplied, built-in models of every operation remain registered. Equal model IDs in different operations remain distinct, including their model-specific headers.\n\nCalls made after initial extension loading take effect immediately. Use `pi.unregisterProvider()` to remove the dynamic provider and restore built-in behavior that it replaced.\n\nSee the checked [GitLab Duo provider](../examples/extensions/custom-provider-gitlab-duo/) for a complete registration that delegates streaming to built-in API implementations.\n\n## Provide authentication\n\nStatic providers can resolve an API key from a literal, environment interpolation, or a command. These values use the same syntax as `models.json`:\n\n- `$NAME` and `${NAME}` read environment variables.\n- A leading `!command` uses command output.\n- `$$` emits a literal `$`.\n- `$!` emits a literal leading `!`.\n\nUse native provider authentication when the integration needs stored credentials, custom resolution, provider-scoped environment, or multiple login methods.\n\nAn OAuth provider supplies a display name, login flow, token refresh, and access-token resolution. After registration it appears in `/login`, and Pi stores returned credentials in `~/.pi/agent/auth.json`.\n\nOAuth callbacks are UI-neutral. They can open an authorization URL, show a device code, report progress, request input, or ask the user to choose a login method. Honor cancellation and the supplied abort signal during network requests.\n\nPress Enter (`tui.select.confirm`) to open the device-login URL and copy the code, or log in manually. Providers can disable either action with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false` in `onDeviceCode` or native `AuthEvent` notifications.\n\nNever write access tokens, refresh tokens, authorization headers, or complete provider responses to ordinary logs.\n\n## Supply and refresh models\n\nEvery model needs an ID, display name, input capabilities, and cost metadata. Chat and classifier models also need a context window; chat models need an output limit and reasoning support; image models declare their output modalities. Choose the API implementation at the provider level unless one model requires an override.\n\nSet `promptCache.short` or `promptCache.long` to the provider's best-effort cache lifetime in seconds when Pi should keep an idle prompt cache warm. Leave them unset to disable cache warming for that retention tier.\n\nCompatibility flags describe verified differences in an otherwise supported API. Do not enable them based only on an endpoint claiming compatibility.\n\nConfirm the request fields and response behavior against the actual server.\n\nUse `refreshModels` when the available catalog comes from a live service. Pass `context.signal` to blocking I/O so callers can cancel refreshes.\n\nThe two registration forms have different refresh contracts:\n\n- A complete `Provider` returns nothing. It calls `context.publish({ update })` to install provider-owned model state, after which its synchronous `getModels()` exposes the latest list.\n- Legacy `ProviderConfig.refreshModels` returns mixed-operation model definitions. Pi replaces that registration’s live models with the returned list and applies any requested persistence.\n\nPublish persisted catalog data only when it should survive across runs. A live service such as llama.cpp can update its in-memory list without persisting it; a remote catalog can retain a snapshot for offline startup.\n\n## Reuse a supported streaming API\n\nUse one of Pi AI’s API implementations whenever the provider protocol matches it.\n\nSupported implementations cover Anthropic Messages, OpenAI Chat Completions and Responses, Google Generative AI and Vertex, Azure OpenAI Responses, Mistral Conversations, and Bedrock Converse.\n\nThe provider can still customize authentication, base URLs, headers, model filtering, and discovery while delegating request conversion and streaming to an existing API implementation.\n\nThis is safer than copying a stream implementation because it preserves Pi’s message conversion, tool handling, usage accounting, cancellation, and compatibility behavior.\n\n## Implement custom streaming\n\nImplement `streamSimple` only when no existing API implementation can represent the service. Study the implementations under [`packages/ai/src/api`](https://github.com/earendil-works/pi/tree/main/packages/ai/src/api) first.\n\nThe stream receives a normalized `TranscriptContext`. System prompts and tool declarations live in transcript system messages, so read them with `getCurrentSystemPrompt(context.messages)` and `getCurrentTools(context.messages)` rather than expecting `context.systemPrompt` or `context.tools`. A model that supports mid-conversation system messages can receive them in place; otherwise call `collapseSystemMessages(context)` to fold later system messages into the leading one.\n\nA custom stream must:\n\n1. Create an assistant message with provider, model, timestamp, pending stop reason, content, and zeroed usage.\n2. After request setup succeeds, emit one `start` event before content events.\n3. Update the message while emitting balanced text, thinking, and tool-call events.\n4. Finalize usage, cost, content, and stop reason.\n5. Emit exactly one terminal `done` or `error` event and close the stream.\n6. Convert cancellation into an aborted result.\n\nRequest setup can fail before `start`; in that case the stream can terminate directly with `error`. Missing request authentication may also throw synchronously before a stream is returned.\n\nContent indexes refer to blocks in the assistant message. Update each block before emitting the event whose `partial` field exposes that state. Tool-call arguments must contain valid parsed input by `toolcall_end`.\n\nThe stream must also honor request instrumentation supplied through `SimpleStreamOptions`:\n\n- Call `options.onPayload` before sending the provider request and use any replacement payload it returns.\n- Call `options.onResponse` after receiving the response but before consuming its body.\n- Await `options.onProviderStreamEvent?.(providerEvent, model)` for each parsed provider event before normalizing it.\n- Pass through the abort signal and provider-scoped environment.\n\nThese hooks power extension request inspection, response-header events, and provider-stream observation. Omitting them makes the provider behave differently from Pi’s built-in providers.\n\n## Report failures and usage\n\nSet a concrete terminal stop reason. Error and aborted messages need an `errorMessage`; successful messages need accurate input, output, cache, total-token, and cost values.\n\nPi can compact and retry after recognized context-overflow errors. If the service uses an unknown message, normalize only that provider’s overflow response to `context_length_exceeded` in a guarded `message_end` handler.\n\nDo not rewrite rate limits or transient provider failures as context overflow. Those failures use Pi’s normal retry behavior instead.\n\n## Test the integration\n\nTest at least:\n\n- ordinary and empty text responses\n- tool calls and tool results\n- image input and image tool results when supported\n- usage and cost accounting\n- abort behavior\n- context overflow\n- malformed or partial streams\n- Unicode boundaries\n- cross-provider session handoff\n- authentication refresh and cancellation\n\nThe provider tests under [`packages/ai/test`](https://github.com/earendil-works/pi/tree/main/packages/ai/test) define the behavior expected from built-in providers. Adapt the relevant suites rather than relying only on manual prompts.\n\nRun the extension directly while developing, then move it to a discovered extension location or distribute it through a [Pi package](packages.md). Use `/reload` after changing a discovered provider extension in an active session.\n","packages/coding-agent/docs/docs.json":"{\n  \"navigation\": [\n    {\n      \"title\": \"Get Started\",\n      \"items\": [\n        {\n          \"title\": \"Overview\",\n          \"path\": \"index.md\"\n        },\n        {\n          \"title\": \"Quickstart\",\n          \"path\": \"quickstart.md\"\n        },\n        {\n          \"title\": \"How Pi Works\",\n          \"path\": \"how-pi-works.md\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Guides\",\n      \"items\": [\n        {\n          \"title\": \"Run Pi\",\n          \"items\": [\n            {\n              \"title\": \"Use Pi in the Terminal\",\n              \"path\": \"usage.md\"\n            },\n            {\n              \"title\": \"Choose a Model\",\n              \"path\": \"models.md\"\n            },\n            {\n              \"title\": \"Manage Sessions and Context\",\n              \"path\": \"sessions.md\"\n            },\n            {\n              \"title\": \"Run Pi Safely\",\n              \"path\": \"security.md\"\n            },\n            {\n              \"title\": \"Isolate Pi\",\n              \"path\": \"containerization.md\"\n            },\n            {\n              \"title\": \"Run Local Models\",\n              \"path\": \"llama-cpp.md\"\n            },\n            {\n              \"title\": \"Run Local Models on NVIDIA RTX\",\n              \"path\": \"nvidia-rtx-llama-cpp.md\"\n            },\n            {\n              \"title\": \"Configure Your Terminal\",\n              \"path\": \"terminal-setup.md\"\n            },\n            {\n              \"title\": \"Configure Shell Commands\",\n              \"path\": \"shell-aliases.md\"\n            },\n            {\n              \"title\": \"Run Pi in tmux\",\n              \"path\": \"tmux.md\"\n            },\n            {\n              \"title\": \"Run Pi on Windows\",\n              \"path\": \"windows.md\"\n            },\n            {\n              \"title\": \"Run Pi on Android\",\n              \"path\": \"termux.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Customize Pi\",\n          \"items\": [\n            {\n              \"title\": \"Configure Pi\",\n              \"path\": \"configuration.md\"\n            },\n            {\n              \"title\": \"Create Prompt Templates\",\n              \"path\": \"prompt-templates.md\"\n            },\n            {\n              \"title\": \"Add Skills\",\n              \"path\": \"skills.md\"\n            },\n            {\n              \"title\": \"Create Themes\",\n              \"path\": \"themes.md\"\n            },\n            {\n              \"title\": \"Use Pi Packages\",\n              \"path\": \"packages.md\"\n            },\n            {\n              \"title\": \"Connect MCP Servers\",\n              \"path\": \"mcp.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Build on Pi\",\n          \"items\": [\n            {\n              \"title\": \"Build Extensions\",\n              \"path\": \"extensions.md\"\n            },\n            {\n              \"title\": \"Add Custom Providers\",\n              \"path\": \"custom-provider.md\"\n            },\n            {\n              \"title\": \"Route with Virtual Models\",\n              \"path\": \"virtual-models.md\"\n            },\n            {\n              \"title\": \"Build Terminal UI Components\",\n              \"path\": \"tui.md\"\n            },\n            {\n              \"title\": \"Integrate with the CLI\",\n              \"path\": \"cli-integration.md\"\n            },\n            {\n              \"title\": \"Use the SDK\",\n              \"path\": \"sdk.md\"\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"title\": \"Reference\",\n      \"items\": [\n        {\n          \"title\": \"CLI\",\n          \"path\": \"cli.md\"\n        },\n        {\n          \"title\": \"Codemode\",\n          \"path\": \"codemode.md\"\n        },\n        {\n          \"title\": \"Slash Commands\",\n          \"path\": \"slash-commands.md\"\n        },\n        {\n          \"title\": \"Settings\",\n          \"path\": \"settings.md\"\n        },\n        {\n          \"title\": \"Models Configuration\",\n          \"path\": \"models-configuration.md\"\n        },\n        {\n          \"title\": \"Environment Variables\",\n          \"path\": \"environment-variables.md\"\n        },\n        {\n          \"title\": \"Keybindings\",\n          \"path\": \"keybindings.md\"\n        },\n        {\n          \"title\": \"Providers\",\n          \"path\": \"providers.md\"\n        },\n        {\n          \"title\": \"Session File Format\",\n          \"path\": \"session-format.md\"\n        },\n        {\n          \"title\": \"Compaction and Branch Summaries\",\n          \"path\": \"compaction.md\"\n        },\n        {\n          \"title\": \"JSON Event Stream\",\n          \"path\": \"json.md\"\n        },\n        {\n          \"title\": \"RPC Protocol\",\n          \"path\": \"rpc.md\"\n        },\n        {\n          \"title\": \"RPC Commands\",\n          \"path\": \"rpc-commands.md\"\n        },\n        {\n          \"title\": \"RPC Extension UI\",\n          \"path\": \"rpc-extension-ui.md\"\n        },\n        {\n          \"title\": \"Message Types\",\n          \"path\": \"message-types.md\"\n        }\n      ]\n    }\n  ],\n  \"redirects\": [\n    {\n      \"from\": \"development.md\",\n      \"to\": \"index.md\"\n    },\n    {\n      \"from\": \"session.md\",\n      \"to\": \"session-format.md\"\n    },\n    {\n      \"from\": \"tree.md\",\n      \"to\": \"sessions.md\"\n    },\n    {\n      \"from\": \"process-integration.md\",\n      \"to\": \"cli-integration.md\"\n    }\n  ]\n}\n","packages/coding-agent/docs/environment-variables.md":"# Environment Variables\n\nPi uses environment variables in three ways:\n\n- Variables such as `PI_OFFLINE` configure the Pi process.\n- Pi sets process markers so child processes can identify Pi as the launching agent.\n- Commands run by the LLM-callable shell tools receive `PI_*` variables describing the current session.\n\nProvider API-key variables are documented separately in [Providers](providers.md#use-an-api-key-from-the-environment).\n\n## Process Marker\n\nThe CLI and RPC entry points set two process markers:\n\n- `AI_AGENT=pi` is a generic marker that lets tooling identify Pi as the agent that launched the process.\n- `PI_CODING_AGENT=true` is Pi-specific and lets child processes detect that they run inside Pi.\n\nChild processes inherit both markers. They are not session-specific and are not set automatically when Pi is embedded through the SDK.\n\n## Shell Tool Session Environment\n\nCommands run by the `bash` and `powershell` tools receive the current Pi session state:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_SESSION_ID` | Current session ID |\n| `PI_SESSION_FILE` | Absolute path to the current session JSONL file; unset for ephemeral sessions |\n| `PI_PROVIDER` | Currently selected model provider |\n| `PI_MODEL` | Currently selected model ID |\n| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |\n\nThe values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next shell command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.\n\nWhen asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt:\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\nThe session file can be inspected directly when the session is persistent:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nThese variables are injected into the LLM-callable `bash` and `powershell` tools. They are not injected into user-entered `!` or `!!` commands.\n\n### Custom Shell Tools\n\nTools created with `createBashTool()` or `createPowerShellTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nDisable session metadata independently of the spawn hook:\n\n```typescript\nconst powershellTool = createPowerShellTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nWhen disabled, Pi removes inherited values for these variables so nested Pi processes do not expose stale parent-session metadata.\n\n## Pi Process Configuration\n\nThese variables are read by Pi itself:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Override the config directory; default is `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Override session storage; overridden by `--session-dir` |\n| `PI_PACKAGE_DIR` | Override the package directory, useful for Nix/Guix store paths |\n| `PI_OFFLINE` | Disable automatic network activity, including model catalog refreshes |\n| `PI_SKIP_VERSION_CHECK` | Disable the `pi.dev` latest-version request |\n| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no` |\n| `PI_CODEMODE_MAX_WORKERS` | Maximum codemode workers alive at once across all sessions in the process; default `2`. Each worker's script may use up to 256 MB. Raise it when one process runs several sessions, such as in-process subagents. Further scripts wait for a free worker. See [Codemode](codemode.md) |\n| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |\n| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |\n| `PI_RADIUS_GATEWAY` | Override the Radius gateway origin used by `/bug` uploads |\n| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |\n| `PI_CLIPBOARD_OSC52` | Set to `1` to also send copied text to the terminal with OSC 52 after the local clipboard write, as Pi does over SSH. Use it when a remote client reaches Pi without SSH variables, such as a multiplexer attached from another machine or a remote IDE daemon |\n| `PI_HYPERLINKS` | Override OSC 8 hyperlink detection with `1`, `0`, or `auto` |\n| `PI_PROGRAM_STATUS` | Override OSC 7501 program status detection: `1` always reports, `0` never reports; otherwise Pi reports only after the terminal confirms support. See [Terminal setup](terminal-setup.md#program-status) |\n| `PI_IMAGE_PROTOCOL` | Override inline image detection with `kitty`, `iterm2`, `none`, or `auto` |\n| `PI_TRUE_COLOR` | Override truecolor detection with `1`, `0`, or `auto` |\n| `PI_TUI_ESC_TIMEOUT` | How long to wait after a lone ESC before treating it as Escape, in milliseconds; defaults to `100` over SSH and `10` otherwise. Increase if Alt-key input is misread as Escape |\n| `VISUAL`, `EDITOR` | External editor fallback when `externalEditor` is unset |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |\n\nProvider credentials such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and provider-specific configuration are listed in [Providers](providers.md#use-an-api-key-from-the-environment).\n","packages/coding-agent/docs/extensions.md":"# Extensions\n\nExtensions are TypeScript modules that add executable behavior to Pi. Use one when a workflow needs tools, commands, event handlers, model providers, session state, or terminal UI rather than instructions alone.\n\nAn extension runs inside the Pi process with the same operating-system permissions. It can inspect prompts, tool calls, files, credentials, and session history, so load extensions only from sources you trust.\n\nTypical extensions add an agent tool, protect paths, confirm dangerous commands, react to session events, modify context, expose a command, or display persistent status.\n\n<a id=\"quick-start\"></a>\n<a id=\"writing-an-extension\"></a>\n<a id=\"create-an-extension\"></a>\n\n## Create and load an extension\n\nAn extension exports a default factory that receives `ExtensionAPI`. The factory registers capabilities for the current extension runtime.\n\nCreate `~/.pi/agent/extensions/hello.ts`:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"hello\", {\n    description: \"Show a greeting\",\n    handler: async (name, ctx) => {\n      ctx.ui.notify(`Hello, ${name || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\nStart Pi and run `/hello`. During development, load a file directly:\n\n```bash\npi --extension ./hello.ts\n```\n\nPi uses `jiti`, so local TypeScript extensions do not need a separate compilation step. Use [Pi packages](packages.md) for distributed extensions and dependencies.\n\n<a id=\"extension-locations\"></a>\n<a id=\"available-imports\"></a>\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the extension in your user or project extensions directory. Pi loads direct TypeScript or JavaScript files and subdirectories containing an `index.ts` or `index.js` entry point.\n\nUse a single file for a small extension and a directory for a multi-file implementation. Put npm dependencies in a nearby `package.json`. See [Configuration](configuration.md) for conventional locations and [Settings](settings.md#resources) for additional paths.\n\nReload replaces the extension runtime, so code after `await ctx.reload()` must not reuse state from the old runtime. Only personal and explicit command-line extensions can participate in the `project_trust` event that runs before project extensions load.\n\n<a id=\"understand-the-lifecycle\"></a>\n\n## Understand the runtime lifecycle\n\nThe factory can be synchronous or asynchronous. Pi waits for an asynchronous factory before startup continues, allowing it to fetch configuration or register providers needed during startup.\n\nDo not start processes, sockets, watchers, or timers in the factory because some invocations load extensions without starting a session.\nStart long-lived resources from `session_start` or from the command or tool that needs them.\nClose session-scoped resources from an idempotent `session_shutdown` handler.\n\nA normal prompt starts a run. The run repeats turns, then checks whether more work starts another low-level agent run:\n\n```mermaid\nflowchart TD\n    Input[\"input\"] --> Before[\"before_agent_start\"]\n    Before --> AgentStart[\"agent_start\"]\n    AgentStart --> Turns[[\"turns\"]]\n    Turns --> AgentEnd[\"agent_end\"]\n    AgentEnd --> MoreRuns{\"More work?\"}\n    MoreRuns -->|yes| AgentStart\n    MoreRuns -->|no| BeforeSettle[\"agent_before_settle\"]\n    BeforeSettle --> Continue{\"Continuation requested?\"}\n    Continue -->|yes| AgentStart\n    Continue -->|no| Settled[\"agent_settled\"]\n```\n\nMore work means a retry, recovery, compaction retry, or queued work.\n\nEach turn sends one model request and executes the tool calls in the assistant response:\n\n```mermaid\nflowchart TD\n    TurnStart[\"turn_start\"] --> Request[[\"model request\"]]\n    Request --> ToolDecision{\"Tool calls?\"}\n    ToolDecision -->|yes| Tools[[\"tool execution\"]]\n    Tools --> TurnEnd[\"turn_end\"]\n    ToolDecision -->|no| TurnEnd\n    TurnEnd --> MoreTurns{\"Another turn?\"}\n    MoreTurns -->|yes| TurnStart\n    MoreTurns -->|no| AgentEnd([\"agent_end\"])\n```\n\nThe model request runs these hooks in order:\n\n1. `context`\n2. `context_with_system`\n3. `before_provider_headers`\n4. `before_provider_request`\n5. `after_provider_response`\n6. `provider_stream_event` and assistant message events\n\nTool execution runs these hooks in order for each tool call:\n\n1. `tool_execution_start`\n2. `tool_call`\n3. `tool_execution_update` while the tool executes\n4. `tool_result`\n5. `tool_execution_end`\n\nThe lifecycle involves these event groups:\n\n| Area | Events |\n|---|---|\n| Input preparation | `input`, `before_agent_start` |\n| Agent run | `agent_start`, `agent_end` |\n| Model turn | `turn_start`, `context`, `context_with_system`, `turn_end` |\n| Provider call | `before_provider_headers`, `before_provider_request`, `after_provider_response`, `provider_stream_event` |\n| Message lifecycle | `message_start`, `message_update`, `message_end` |\n| Tool execution | `tool_execution_start`, `tool_call`, `tool_execution_update`, `tool_result`, `tool_execution_end` |\n| Settlement | `agent_before_settle`, `agent_settled` |\n\nPi counts as busy while a run starts, including the compaction check and `before_agent_start`, so messages sent meanwhile go into the starting run. Message lifecycle events also cover user and tool-result messages; the model request list places only the assistant stream where it occurs in each turn.\n\nTool hooks run for each tool call, and one assistant response can start another turn by returning tool calls. Provider hooks run only when that transport exposes the corresponding stage. See the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event payloads and result types.\n\nSession startup and replacement follow a separate lifecycle:\n\n```mermaid\nflowchart TD\n    Startup[\"Process startup\"] --> Trust[\"project_trust when needed\"]\n    Trust --> SessionStart[\"session_start\"]\n    Switch[\"/new or /resume\"] --> BeforeSwitch[\"session_before_switch\"]\n    Fork[\"/fork or /clone\"] --> BeforeFork[\"session_before_fork\"]\n    BeforeSwitch --> Shutdown[\"session_shutdown\"]\n    BeforeFork --> Shutdown\n    Reload[\"reload\"] --> Shutdown\n    Shutdown --> Replacing{\"Replacing the runtime?\"}\n    Replacing -->|yes| Replacement[\"load replacement extension runtime\"]\n    Replacement --> SessionStart\n    Replacing -->|no| Stop[\"process exits\"]\n    SessionStart --> Discover[\"resources_discover\"]\n```\n\n`project_trust` runs before trusted project resources load and can run again when session replacement enters a new working directory. Retries, recovery, compaction, and queued messages can start another agent run after `agent_end`, so `agent_end` is not a final-idle signal. Reload and session replacement invalidate the old runtime. Code after `await ctx.reload()` still belongs to the old runtime and must not reuse its session-bound state.\n\n### Choose an event by intent\n\n| Goal | Event |\n|---|---|\n| Inspect, transform, or consume raw user input | `input` |\n| Change prompt sections, active tools, or add context before a run | `before_agent_start` |\n| Observe one low-level agent run | `agent_start`, `agent_end` |\n| Act after one model response and its tool calls | `turn_end` |\n| Filter conversation messages for one provider request | `context` |\n| Transform the complete transcript, including system messages | `context_with_system` |\n| Change request headers or the serialized provider payload | `before_provider_headers`, `before_provider_request` |\n| Observe provider response metadata or parsed stream events | `after_provider_response`, `provider_stream_event` |\n| Authorize or modify a tool call | `tool_call` |\n| Redact, annotate, or replace a tool result | `tool_result` |\n| Append final entries or request one last continuation | `agent_before_settle` |\n| Run notification work after automatic processing has stopped | `agent_settled` |\n| Initialize or release session-scoped resources | `session_start`, `session_shutdown` |\n\n<a id=\"agent_start--agent_end--agent_before_settle--agent_settled\"></a>\n\nUse `agent_before_settle` when an extension must append entries or request one final continuation. Use `agent_settled` for notification-only work that should run after Pi has no automatic continuation remaining.\n\n<a id=\"extensionapi-methods\"></a>\n\n## Choose an integration point\n\n| Capability | Main API |\n|---|---|\n| Observe or modify lifecycle behavior | `pi.on()` |\n| Add a model-callable operation | `pi.registerTool()` |\n| Add a `/` command | `pi.registerCommand()` |\n| Add a shortcut or CLI flag | `pi.registerShortcut()` or `pi.registerFlag()` |\n| Send user or custom messages | `pi.sendUserMessage()` or `pi.sendMessage()` |\n| Persist non-context session data | `pi.appendEntry()` |\n| Change active tools, model, or thinking level | Session control methods on `pi` |\n| Add a model provider | `pi.registerProvider()` |\n| Add an MCP server | `pi.registerMcpServer()` |\n| Route each request to a model | [`pi.registerVirtualModel()`](virtual-models.md) |\n| Add terminal rendering | Renderer registration and `ctx.ui` |\n| Communicate with another extension | `pi.events` |\n\nUse the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event, context, tool, and result types.\n\n## Follow the extension contracts\n\n<a id=\"events\"></a>\n<a id=\"work-with-events\"></a>\n\n### Events and concurrency\n\nHandlers run in extension load and registration order. `pi.on()` returns a function that unsubscribes that registration; changes do not affect a dispatch already in progress.\nSome events notify; others transform data, replace results, or cancel an operation.\nUse each event’s declared result type rather than assuming every return value has an effect.\n\nEvents cover resource discovery, sessions, agent and message lifecycle, providers, tools, and raw input.\n\n#### Understand handler composition\n\n| Pattern | Behavior |\n|---|---|\n| Notification | Return values are ignored. Use the event for observation only. |\n| In-place mutation | Later handlers see changes to values such as `before_provider_headers.headers`, `before_agent_start.systemPromptOptions`, and `tool_call.input`. |\n| Chained transformation | Later handlers receive the value returned by earlier `input`, `context`, `context_with_system`, `message_end`, `tool_result`, and `before_provider_request` handlers. |\n| Gate | A handled input, blocked tool call, cancelled session operation, or handled `user_bash` command stops that operation's normal fallback path. |\n| Actionable boundary | Each `turn_end` or `agent_before_settle` handler sees the entries and continuation decision proposed so far. |\n| Last override | The last `cache_warming_decision` handler that returns an action wins. |\n\nNotification handlers should not perform slow work on a streaming event. Transform handlers should preserve fields they do not intentionally own, and gate handlers should fail closed when their safety decision cannot be made.\n\n`before_agent_start` exposes both the current prompt and its structured `systemPromptOptions`. `event.messageType` says what started the run: `\"user\"` for a prompt or `pi.sendUserMessage()`, with `prompt` and `images`; `\"custom\"` for `pi.sendMessage(..., { triggerTurn: true })`, with the starting `message` and no `prompt`. A returned `message` is added to either kind of run; check `messageType` to skip runs started by custom messages. Prefer changing prompt sections, selected tools, or guidelines so Pi can append a transcript delta. Returning `systemPrompt`, or setting `forceSystemPrompt`, replaces the whole prompt for that run while the transcript continues recording the structured sections. Providers receive the forced text as their leading system prompt.\n\n`message_end` can replace a finalized message while preserving its role. `tool_call` can mutate input or block execution. `tool_result` handlers compose, with each handler seeing prior changes.\n\n<a id=\"provider_stream_event\"></a>\n\n`provider_stream_event` fires for each parsed provider stream event before Pi normalizes it. The event identifies the provider, API, and model; `event.data` is the earliest structured value available to Pi, not necessarily the original HTTP bytes or SSE frame. Treat it as read-only because mutation can affect normalization. The event is notification-only and is not persisted.\n\nHandlers are awaited in stream order, so slow handlers delay stream consumption. Handler errors are reported without changing the provider response. See [`debug-provider.ts`](../examples/extensions/debug-provider.ts) for an opt-in viewer that groups raw events by assistant message.\n\n<a id=\"context_with_system\"></a>\n\n`context` transforms conversation messages without prompt and tool system messages; Pi restores that state afterward. Use `context_with_system` only when a request-local transformation must own the complete transcript, and keep a system message at index zero.\n\n`turn_end` and `agent_before_settle` are actionable boundaries. Their handlers can chain proposed `custom`, `custom_message`, `context_edit`, or `compaction` entries and return `continue: true` for one next model request. Guard continuation conditions because an unconditional continuation can loop. Use the exported event declarations for the complete validation and ordering contract.\n\n<a id=\"cache_warming_decision\"></a>\n\n`cache_warming_decision` can override an idle prompt-cache refresh with `{ action: \"warm\" }` or `{ action: \"stop\" }`. The last handler that returns an action wins.\n\nTool calls from one assistant message can run in parallel.\nDo not assume a sibling call or result exists when another tool event runs.\nUse `ctx.signal` for nested work owned by an active turn; commands and idle session events often have no operation signal.\n\nA `user_bash` handler that returns `undefined` passes the command to the next handler and then to local execution if no handler handles it. Returning `operations` or `result` stops propagation. A handler failure blocks the command rather than falling through to local execution.\n\n<a id=\"custom-tools\"></a>\n<a id=\"register-tools\"></a>\n\n### Tools\n\nA custom tool defines a name, model-facing description, TypeBox parameter schema, and `execute()` function.\nIts result requires model-facing `content` and a `details` field for rendering or state reconstruction.\nUse `details: undefined` when there are no structured details. If the tool makes nested model calls, include their `usage` in the result so session totals remain accurate.\n\nThrow from `execute()` to produce a failed tool result.\nReturning an object does not mark it as an error.\nReturn `terminate: true` only when the agent should skip its automatic follow-up after every completed tool in that batch agrees to terminate.\n\nUse sequential execution when tools share mutable in-memory state.\nFile-mutating tools should wrap the complete read-modify-write operation with `withFileMutationQueue()`.\nTruncate large model-facing results and tell the model where to read the complete output.\n\nDeclare `outputSchema` and return a matching `structuredContent` when the result is data. The model still receives `content`; programmatic callers such as codemode scripts receive `structuredContent` instead of the text. Tools without `outputSchema` are passed to scripts as their text content. To report a failure that still carries data, return the result with `isError: true` instead of throwing: the model sees an error, and scripts still receive `structuredContent`.\n\nA tool can run other tools with `ctx.executeTool(name, args, { signal, onUpdate })`. Nested calls go through argument validation and the `tool_call` and `tool_result` handlers like model-issued calls, and emit `tool_execution_start`, `tool_execution_update`, and `tool_execution_end`; all of these events carry `parentToolCallId`, and their `toolCallId` is assigned by pi as `<parent id>/<n>`. These ids do not appear as tool calls or tool results in the transcript. Nested calls do not add transcript entries: their results only reach the calling tool, which reports them itself, for example through `onUpdate` and `details`. The session keeps a bounded record of them (name, arguments, status, duration, error; never results) as `nestedCalls` on the calling tool's result message. It is used for compaction file lists and shown in HTML exports. Arguments over 8 KiB per call or 32 KiB per tool result are omitted, at most 256 calls are kept, and `complete: false` marks a record that lost anything. The `usage` of nested results, at every depth, is added to the calling tool's result `usage`, so a tool reports only its own usage, not that of the tools it called. `ctx.tools` lists the tools `ctx.executeTool()` can call. `tool_result` handlers that redact `content` should also replace `structuredContent`; replacing only `content` drops it.\n\nSee [`hello.ts`](../examples/extensions/hello.ts), [`todo.ts`](../examples/extensions/todo.ts), [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts), and [`truncated-tool.ts`](../examples/extensions/truncated-tool.ts).\n\n### Tool exposure\n\n`exposure` controls how the model reaches a tool. \"Callable\" means callable from other tools through `ctx.executeTool()` (`ctx.tools`), as the `codemode` tool's scripts do:\n\n- `direct` (default): declared to the model while active, and callable while active.\n- `model-only`: declared to the model while active, never callable. Use it for tools that orchestrate other tools or ask the user.\n- `codemode`: callable whenever registered, and listed by the `codemode` tool. Not declared to the model unless activated explicitly.\n- `deferred`: like `codemode`, but codemode tools do not list it; `tool_search` can find and activate it.\n- `hidden`: registered but unreachable. Re-register a tool with `exposure: \"hidden\"` to withdraw it, since tools cannot be unregistered.\n\n`namespace: { name, aliases, description, instructions }` groups related tools, as MCP servers do. Codemode tools list a namespace under one heading with its `description`. `instructions` holds longer usage guidance; it is not listed, and codemode scripts read it with `describeNamespace(name)`. `aliases` are other names scripts may look the namespace up by, for example the server name `docs` for `mcp__docs`; a name and the same name as a script identifier (`my-docs`, `my_docs`) match alike.\n\nRegistering a `direct` or `model-only` tool activates it; the other exposures are not activated on registration. The active set (`pi.getActiveTools()`, `pi.setActiveTools()`) is the set of tools declared to the model. `pi.getAllTools()` reports each tool's `exposure`, `namespace`, `annotations`, and `reachesTools`.\n\n`annotations` are hints about what a tool does, with the meaning of MCP tool annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. MCP tools carry the hints their server declares. Missing hints take the MCP defaults: a tool is not read-only, and may be destructive and reach an open world. The hints are not verified, but a permission extension can use them to decide which calls to confirm. This confirms the calls Codex asks approval for:\n\n```typescript\npi.on(\"tool_call\", async (event, ctx) => {\n  const hints = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;\n  const needsApproval =\n    hints?.destructiveHint === true ||\n    (!hints?.readOnlyHint && ((hints?.destructiveHint ?? true) || (hints?.openWorldHint ?? true)));\n  if (needsApproval && !(await ctx.ui.confirm(\"Allow tool call?\", event.toolName))) {\n    return { block: true, reason: `${event.toolName} was not approved` };\n  }\n});\n```\n\nA tool that orchestrates other tools can adjust what the model sees while it is active with `prepareLoadout(loadout)`. It runs whenever the active tools change and receives the declared tools, the callable tools, and every registered tool with its exposure, namespace, and prompt guidelines. It returns replacement `descriptions` for declared tools (including its own) and `hiddenDeclarations`: active tools whose declarations requests leave out while they stay active and callable. The default system prompt leaves hidden tools out of its tool list and rules, and names no tool in the skills hint when the file reader is hidden, so the orchestrating tool should present their guidelines itself. `codemode` uses only this hook, `exposure`, `reachesTools`, and `ctx.executeTool()`, so another tool can implement the same behavior under a different name.\n\nA tool that reaches other tools declares which ones a call may reach with `reachesTools(input)`: `\"all\"`, or the words of the input that may name them, where a word refers to every tool or namespace whose name it contains. `codemode` returns `\"all\"` for scripts that search or list tools and the words of the script otherwise; `tool_search` always returns `\"all\"`. Extensions that register tools late use it from their `tool_call` handler to finish registering the tools a call needs before it runs, as the MCP extension does for servers that are still connecting. The MCP extension also activates `codemode` or `tool_search` for MCP tools only when the tool of that name declares `reachesTools`.\n\n### Activate tools dynamically\n\nRegister every tool first, keep optional tools inactive, and use `pi.setActiveTools()` from a loader tool to select the desired active tools. Names must already be registered; unknown names are ignored.\n\nPi records the initial prompt and tool set in the transcript's first system message, then appends tool and prompt changes before the next model request. Providers that cannot represent the transition receive a complete transcript checkpoint, which can invalidate the cached prefix.\n\n### Tool rendering\n\nA tool's `renderCall` and `renderResult` draw its calls in the interactive transcript and in HTML exports. `pi.registerToolRenderer((toolName, next) => renderers)` chooses renderers for calls to any tool, including tools that are not registered yet, such as MCP tools in a resumed session before their server connected. `next()` returns what the remaining resolvers (in extension load order), then the registered tool, would use, so `next() ?? mine` only fills in.\n\n### MCP servers\n\n`pi.registerMcpServer(name, config)` adds an MCP server for the current session. `config` has the shape of an `mcpServers` entry in [`mcp.json`](mcp.md): `command`, `args`, `env`, and `cwd` for stdio servers, `url`, `headers`, and `oauth` for HTTP servers, plus `exposure`, `toolExposure`, `description`, `enabled`, and `timeout`.\n\n```typescript\npi.registerMcpServer(\"jira\", { url: \"https://mcp.example.com/jira\", exposure: \"codemode\" });\npi.unregisterMcpServer(\"jira\");\n```\n\nServers registered while the extension loads connect when the session starts, together with the `mcp.json` servers; servers registered later connect right away, and `pi.unregisterMcpServer()` closes the connection and makes the server's tools unreachable. Registrations are not saved: register again on every load, for example based on the extension's own settings. A server in `mcp.json` with the same name takes precedence, and `/mcp` shows the override. Registering the same name again replaces the extension's earlier registration; names registered by another extension, invalid names, and invalid configs throw.\n\nThe built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#other-mcp-extensions)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `pi.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.\n\n<a id=\"extensioncontext\"></a>\n<a id=\"extensioncommandcontext\"></a>\n<a id=\"use-extension-context\"></a>\n\n### Context and session changes\n\n`ExtensionContext` provides the working directory, mode, UI, session manager, model runtime, abort signal, context usage, and controls for compaction and shutdown.\nUse `ctx.modelRegistry.streamSimple()` for provider-neutral nested model calls.\n\nCommand handlers receive `ExtensionCommandContext`, which adds operations for waiting until idle, reloading, tree navigation, and session replacement.\nThese operations are command-only because calling them from lifecycle handlers can deadlock the runtime.\n\nSession replacement invalidates the old context. Capture only plain data before switching, then use the fresh context supplied to `withSession` for session-bound work.\n\n<a id=\"state-management\"></a>\n<a id=\"persist-state\"></a>\n\n### State\n\nChoose storage based on how state participates in the conversation:\n\n| State | Storage |\n|---|---|\n| Tool state that follows the active branch | Tool-result `details` |\n| Durable data excluded from model context | `pi.appendEntry()` |\n| Custom content stored and sent to the model | `pi.sendMessage()` |\n| Data outside one session | External storage |\n\nReconstruct branch-sensitive state from `ctx.sessionManager.getBranch()` during `session_start`.\nDo not rebuild it from every file entry because abandoned branches represent alternative histories.\nRegister an entry or message renderer when custom stored content should appear in the transcript.\n\n<a id=\"custom-ui\"></a>\n<a id=\"mode-behavior\"></a>\n<a id=\"interact-with-the-user\"></a>\n<a id=\"account-for-each-mode\"></a>\n\n### UI and modes\n\n`ctx.ui` provides dialogs, notifications, status text, widgets, titles, editor access, and custom components.\nUse `ctx.ui.custom()` only when the interaction needs its own rendering and input.\nSee [Terminal UI](tui.md) for component, focus, overlay, theme, and performance guidance.\n\nExtensions load in interactive, RPC, JSON, and print modes.\nInteractive mode provides the complete terminal UI.\nRPC can forward supported dialogs and notifications through the [RPC Extension UI protocol](rpc-extension-ui.md), but not custom terminal components; JSON and print modes have no UI.\nGuard terminal-only behavior with `ctx.mode === \"tui\"` and use `ctx.hasUI` for interactions supported by interactive and RPC clients.\n\nKeep tool and event behavior independent from rendering so non-interactive modes remain functional.\n\n<a id=\"error-handling\"></a>\n<a id=\"handle-errors-and-shutdown\"></a>\n\n### Errors and cleanup\n\nPi reports handler errors and continues where possible. A `tool_call` handler failure blocks the tool as a fail-safe; a tool execution failure becomes an error result for the model.\n\nRelease resources in `session_shutdown` even when normal operation attempted cleanup.\nKeep cleanup idempotent because cancellation, reload, session replacement, and process exit can converge on the same path.\nUse `ctx.shutdown()` to request an orderly process shutdown.\n\n<a id=\"examples-reference\"></a>\n<a id=\"use-examples-as-the-implementation-reference\"></a>\n\n## Examples and reference\n\nThe checked [extension examples](../examples/extensions/) are the implementation reference. Start with the smallest example matching the behavior you need:\n\n| Goal | Start with | Why |\n|---|---|---|\n| Confirm or block tool calls | [`permission-gate.ts`](../examples/extensions/permission-gate.ts) | Combines `tool_call`, mode-aware UI, and fail-closed behavior. |\n| Modify prompt sections for a run | [`prompt-customizer.ts`](../examples/extensions/prompt-customizer.ts) | Uses structured prompt options instead of replacing the complete prompt. |\n| Keep branch-aware tool state | [`todo.ts`](../examples/extensions/todo.ts) | Reconstructs state from persisted entries and renders it in the TUI. |\n| Register tools after startup | [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts) | Shares registration logic between lifecycle and command handlers. |\n| Run cancellable model work from a command | [`qna.ts`](../examples/extensions/qna.ts) | Combines nested model calls, cancellation, and custom UI. |\n| Replace the main editor | [`modal-editor.ts`](../examples/extensions/modal-editor.ts) | Extends `CustomEditor` while preserving application controls. |\n| Run finalization at shutdown | [`auto-commit-on-exit.ts`](../examples/extensions/auto-commit-on-exit.ts) | Uses the session shutdown hook for finalization work. |\n| Route built-in tools to another machine | [`ssh.ts`](../examples/extensions/ssh.ts) | Reuses tool operation interfaces instead of recreating tool contracts. |\n| Add OAuth and provider-specific streaming | [`custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/) | Shows a complete provider integration while delegating supported protocols. |\n\nUse [Custom Providers](custom-provider.md) for model-service integrations, [Terminal UI](tui.md) for custom components, and [Pi Packages](packages.md) to install or distribute extensions with other resources. Use the exported declarations for exact signatures, payloads, and result schemas; this guide focuses on lifecycle choices, composition, and durable implementation patterns.\n","packages/coding-agent/docs/how-pi-works.md":"# How Pi Works\n\nPi coordinates model requests, tool execution, context assembly, and session storage. A session is Pi's record of a conversation, including messages, tool calls and results, model changes, compactions, and other events.\n\nMessages and events in a session form a tree. Each path through that tree is a branch. The branch ending at the current entry is the active branch and supplies the history for the next model request.\n\n## Agent loop\n\nA submitted message is added to the active branch. Pi builds a model request from the system prompt, active branch, available tools, and model settings, then sends it through the selected provider.\n\nThe provider streams an assistant response, which can contain text and tool calls. Pi records the response, executes each tool call, and records the results. That completes one turn. If tool results or queued messages require another model request, Pi starts another turn. Otherwise, the run ends.\n\nSteering messages enter after the current assistant turn. Follow-up messages enter after the agent has finished its pending work. Aborting stops the current run and returns queued messages to the editor.\n\n## Context\n\nThe active branch supplies conversation history. Pi converts its session entries into model-compatible user, assistant, and tool-result messages.\n\nPi builds the system prompt from its base instructions and discovered context files. The request also carries tool definitions and skill descriptions.\n\nFull skill instructions are loaded on demand. Extensions can add instructions or transform context.\n\nPrompt templates expand editor input before it becomes a user message. Selected files, images, pasted text, and shell output can become message content.\n\n## Sessions\n\nPersistent sessions are JSONL files. Each tree entry has an ID and refers to its parent. The current entry identifies the active branch.\n\nContinuing from an earlier entry creates another branch in the same file. Forking and cloning copy selected history into a new session file.\n\nModel context is reconstructed from the active branch. Compaction inserts a summary entry that replaces older messages in subsequent model requests. The original entries remain in the session tree.\n\n## Interfaces\n\nInteractive mode renders session and agent events in the terminal. Print mode runs a prompt and writes the final response. JSON mode writes agent events as JSONL.\n\nRPC mode accepts JSONL commands on stdin and writes responses and events to stdout. The TypeScript SDK creates and controls agent sessions in process.\n\nAll interfaces use the same agent and session mechanisms.\n\n## Extensions and resources\n\nExtensions are TypeScript modules loaded into the Pi process. Their factory functions register tools, commands, shortcuts, providers, event handlers, renderers, and terminal UI.\n\nSkills provide on-demand instructions and supporting files. Prompt templates provide reusable message text. Themes provide terminal colors. Pi packages distribute these resources through npm or git.\n\n## Trust and permissions\n\nPi resolves project trust before loading project settings and resources. After the trust decision and project-resource loading, Pi loads context files. Enabled tools use the operating-system permissions of the Pi process. Extensions execute inside that process.\n","packages/coding-agent/docs/index.md":"# Pi\n\nPi is an extensible AI agent that works from your terminal. Give it a goal and a working folder, and it can inspect files, run commands, edit content, and work through multi-step tasks.\n\nUse Pi for software development, research notes, writing projects, data files, or hobby work. You can use Pi as is, prompt it to adapt itself to your workflow, or build other applications powered by Pi using the SDK.\n\n## Start using Pi\n\nNew to Pi? Follow the [Quickstart](quickstart.md) to install Pi, connect a model, and complete your first task.\n\nIf Pi is already installed, choose what you want to do:\n\n- [Use Pi interactively](usage.md) to add files, run commands, direct ongoing work, and export results.\n- [Choose a model](models.md) or connect a subscription, API key, local model, or compatible endpoint.\n- [Continue or branch a session](sessions.md) to resume work or explore another approach without losing history.\n- [Configure Pi](configuration.md) for your preferences, working folders, instructions, and reusable resources.\n- [Understand how Pi works](how-pi-works.md), including tools, context, sessions, and the agent loop.\n\n## Customize Pi\n\nPi can reuse prompts, load specialized instructions, add executable integrations, change its terminal interface, connect model services, and distribute these resources as packages.\nUse the [Quickstart customization chooser](quickstart.md#choose-how-to-customize-pi) to select the smallest mechanism that meets your need.\n\n## Automate or embed Pi\n\n- Use [print mode](cli.md#invocation-and-output) for one-off and scripted tasks.\n- Use [JSON event stream mode](json.md) to consume structured events from one run.\n- Use [RPC mode](rpc.md) to control a separate Pi process.\n- Use the [TypeScript SDK](sdk.md) to run Pi inside an application.\n\n## Find reference and setup information\n\nUse the reference pages to look up [CLI options](cli.md), [settings](settings.md), [providers](providers.md), [keybindings](keybindings.md), and [environment variables](environment-variables.md).\n\nFor platform-specific help, see [Terminal Setup](terminal-setup.md), [Windows](windows.md), [tmux](tmux.md), [Termux on Android](termux.md), or [Containerization](containerization.md).\n\n## Work safely\n\nPi's tools and extensions run with the permissions of the Pi process. Project trust controls which project resources Pi loads, but it does not sandbox tool calls. Review [Security](security.md) before using untrusted files, repositories, extensions, or unattended automation.\n","packages/coding-agent/docs/json.md":"# JSON Event Stream\n\nJSON mode emits structured progress for one invocation:\n\n```bash\npi --mode json \"Review this repository\"\n```\n\nPi writes one session header followed by session events, then exits after the supplied prompts finish. RPC mode emits the same session-event shapes but has no session header because it is a bidirectional, long-lived protocol. See [RPC Mode](rpc.md).\n\nThis page is the canonical reference for events shared by JSON and RPC mode. Message values use the [shared message types](message-types.md).\n\n## Framing and process I/O\n\nThe stream uses strict JSONL framing. Each record is one JSON object terminated by LF (`\\n`). Split records only on LF and strip an optional preceding carriage return. Unicode line and paragraph separators are valid inside JSON strings and are not record boundaries.\n\nNode.js `readline` is not suitable for this stream because it also recognizes those Unicode separators. Use a byte or UTF-8 stream decoder and split on LF.\n\nRead stdout continuously. A reader that stops consuming records can stall Pi when the pipe buffer fills. Stdout is reserved for JSONL; diagnostics and application logging go to stderr.\n\n## Session header\n\nThe first JSON-mode record is the current [session header](session-format.md#sessionheader):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path\"}\n```\n\nRPC mode does not emit this record. Use [`get_state`](rpc-commands.md#get_state) for its current session ID and file.\n\n## Event sequence\n\nA basic run produces records like these:\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],\"stopReason\":\"pending\",\"...\":\"...\"}}\n{\"type\":\"message_update\",\"usage\":{\"...\":\"...\"},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"}}\n{\"type\":\"turn_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[{\"...\":\"...\"}],\"willRetry\":false}\n{\"type\":\"agent_settled\",\"aborted\":false}\n```\n\n`agent_end` closes one low-level agent run. Automatic retry, overflow recovery, compaction retry, steering, or follow-up work can still continue. `agent_settled` means Pi has no remaining automatic work for that session-level run.\n\n## Agent and turn events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `agent_start` | None | A low-level agent run started. |\n| `agent_end` | `messages`, `willRetry` | That low-level run ended. `messages` contains messages generated by the run. |\n| `agent_settled` | `aborted` | Pi will not continue automatically through retries, compaction recovery, or queued messages. `aborted` is `true` when the run ended because it was aborted. |\n| `turn_start` | None | One assistant turn started. |\n| `turn_end` | `message`, `toolResults` | One assistant response and its resulting tool calls finished. |\n\nA turn is one assistant response plus any tool calls and tool results produced by that response.\n\n## Message events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `message_start` | `message` | A message started. |\n| `message_update` | `usage`, `assistantMessageEvent` | An assistant message emitted a content-block update. |\n| `message_end` | `message` | A message completed. This is the authoritative final message. |\n\n### Reconstruct streaming messages\n\nWire `message_update` records are delta-only. They omit the SDK event's cumulative `message` field and every `assistantMessageEvent.partial` snapshot so stream size remains linear.\n\nThe nested event is one of:\n\n| Type | Fields in addition to `type` | Meaning |\n|---|---|---|\n| `start` | None | The provider stream started; its cumulative `partial` field is removed on the wire. |\n| `text_start` | `contentIndex` | A text block started. |\n| `text_delta` | `contentIndex`, `delta` | Append text to the block. |\n| `text_end` | `contentIndex`, `content` | The text block ended with authoritative content. |\n| `thinking_start` | `contentIndex` | A thinking block started. |\n| `thinking_delta` | `contentIndex`, `delta` | Append thinking text to the block. |\n| `thinking_end` | `contentIndex`, `content` | The thinking block ended with authoritative content. |\n| `toolcall_start` | `contentIndex`, `id`, `toolName` | A tool-call block started. |\n| `toolcall_delta` | `contentIndex`, `delta` | Append serialized argument data. |\n| `toolcall_end` | `contentIndex`, `toolCall` | The tool call ended with the complete `ToolCall`. |\n| `done` | `reason`, `message` | The provider stream completed successfully. |\n| `error` | `reason`, `error` | The provider stream ended with an error or abort message. |\n\nThe normal agent loop translates provider-level `start`, `done`, and `error` into `message_start` and `message_end` session events rather than emitting them as `message_update`. They remain admitted by the exported `JsonAgentSessionEvent` transformation for callers that construct a matching session event.\n\nUse `contentIndex` to identify the content block. Buffer `delta` fields for a live display, but replace reconstructed data with the completed content in `text_end`, `thinking_end`, or `toolcall_end`. Replace the whole partial message with `message_end.message` when it arrives.\n\nThe top-level `usage` is the latest cumulative provider-reported usage for the assistant response. It can remain zero until completion when a provider does not report usage while streaming.\n\n```json\n{\"type\":\"message_update\",\"usage\":{\"input\":100,\"output\":1,\"cacheRead\":0,\"cacheWrite\":0,\"totalTokens\":101,\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0,\"total\":0}},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello \"}}\n```\n\n## Tool execution events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `tool_execution_start` | `toolCallId`, `toolName`, `args` | Tool execution started. |\n| `tool_execution_update` | `toolCallId`, `toolName`, `args`, `partialResult` | The tool reported a partial result. |\n| `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError`, `durationMs` | Tool execution finished. `durationMs` is how long the tool's `execute()` took, measured with a monotonic clock; absent when the tool did not run. |\n\nUse `toolCallId` to correlate the lifecycle. `partialResult` is the latest partial result supplied by the tool. Whether it replaces or extends an earlier update depends on that tool's result contract.\n\n```json\n{\"type\":\"tool_execution_start\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"}}\n{\"type\":\"tool_execution_update\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"},\"partialResult\":{\"content\":[{\"type\":\"text\",\"text\":\"partial output\"}],\"details\":{}}}\n{\"type\":\"tool_execution_end\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"complete output\"}],\"details\":{}},\"isError\":false}\n```\n\n## Queue and state events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `queue_update` | `steering`, `followUp` | The pending steering or follow-up queue changed. Both fields contain the complete current queue. |\n| `entry_appended` | `entry` | An extension appended a custom session entry through `pi.appendEntry()`. |\n| `session_info_changed` | `name` | The session display name changed. An absent `name` means it was cleared. |\n| `thinking_level_changed` | `level` | The active thinking level changed. |\n\nThe `entry` value uses a persisted [session entry type](session-format.md#entry-types).\n\n## Compaction events\n\n`compaction_start` reports why compaction began:\n\n```json\n{\"type\":\"compaction_start\",\"reason\":\"threshold\"}\n```\n\n`reason` is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n`compaction_end` contains the result when compaction succeeds:\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\"...\": \"...\"},\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\nIf compaction was aborted, `result` is absent and `aborted` is true. If it failed, `result` is absent, `aborted` is false, and `errorMessage` describes the failure. Successful overflow recovery sets `willRetry` to true before Pi retries the prompt.\n\nSee [Compaction and Branch Summaries](compaction.md) for result semantics.\n\n## Retry events\n\nAssistant-turn retry emits:\n\n```json\n{\"type\":\"auto_retry_start\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"529 overloaded\"}\n{\"type\":\"auto_retry_end\",\"success\":true,\"attempt\":2}\n```\n\nOn final failure, `auto_retry_end` has `success: false` and a `finalError` string.\n\nCompaction and branch-summary retry emit:\n\n```json\n{\"type\":\"summarization_retry_scheduled\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"terminated\"}\n{\"type\":\"summarization_retry_attempt_start\",\"source\":\"compaction\",\"reason\":\"threshold\"}\n{\"type\":\"summarization_retry_finished\"}\n```\n\nFor a branch summary, `source` is `\"branchSummary\"` and `reason` is absent. The `reason` on a compaction retry is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n## RPC-only events\n\nA direct RPC [`bash`](rpc-commands.md#bash) command emits one `bash_execution_update` for each output chunk. Its optional `id` matches the command ID. The final command response can contain truncated output, but these events stream all output:\n\n```json\n{\"type\":\"bash_execution_update\",\"id\":\"req-1\",\"delta\":\"total 48\\n\"}\n```\n\nRPC also adds `extension_error` when an extension handler throws:\n\n```json\n{\"type\":\"extension_error\",\"extensionPath\":\"/path/to/extension.ts\",\"event\":\"tool_call\",\"error\":\"Error message\"}\n```\n\nExtension UI records are a separate RPC subprotocol, not `AgentSessionEvent` values. See [RPC Extension UI](rpc-extension-ui.md).\n\n## TypeScript types\n\nThe SDK's `AgentSessionEvent` contains cumulative streaming snapshots for in-process consumers. JSON and RPC transform only `message_update`:\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAssistantMessageEvent<T> = T extends { type: \"toolcall_start\"; partial: unknown }\n  ? WithoutPartial<T> & { id: string; toolName: string }\n  : WithoutPartial<T>;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      usage: Usage;\n      assistantMessageEvent: JsonAssistantMessageEvent<AssistantMessageEvent>;\n    };\n```\n\nUse the exported `JsonAgentSessionEvent` type from `@earendil-works/pi-coding-agent`. Its implementation is in [`json-event.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/json-event.ts).\n\n## Example\n\nPrint completed messages from a one-shot run:\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```\n","packages/coding-agent/docs/keybindings.md":"# Keybindings Reference\n\nPi exposes named actions, such as `app.session.new`, that can be assigned keybindings. You can change default assignments or bind unassigned actions in Pi's [user configuration](configuration.md#agent-directory).\n\nRun `/hotkeys` to see the active shortcuts for the main editor and application.\n\n## Assign keybindings\n\nCreate `<agent-dir>/keybindings.json`. The agent directory defaults to `~/.pi/agent` and is described in [Agent directory](configuration.md#agent-directory).\n\nMap each action identifier to one key or a list of keys:\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/keybindings.schema.json\",\n  \"app.session.new\": \"ctrl+shift+n\",\n  \"app.session.tree\": [\"ctrl+shift+t\", \"alt+shift+t\"]\n}\n```\n\nA configured value replaces the default for that action. Use an empty list to disable an action's keybindings:\n\n```json\n{\n  \"tui.altScreen.pageUp\": []\n}\n```\n\nAfter editing the file, run `/reload` to apply the changes to the active session.\n\n## Key syntax\n\nWrite a key as `modifier+key`. Modifiers are `ctrl`, `shift`, `alt`, and `super`. You can combine modifiers. Valid keys are:\n\n- **Letters:** `a-z`\n- **Digits:** `0-9`\n- **Special:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Function:** `f1`-`f12`\n- **Symbols:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nExamples: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, and `ctrl+1`.\n\n`super` bindings require a terminal that reports the modifier separately, typically through the Kitty keyboard protocol. They may not work in terminals without that support.\n\n## Actions\n\n### Terminal UI\n\n#### Cursor movement\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.cursorUp` | `up` | Move cursor up, browsing older history at the top |\n| `tui.editor.cursorDown` | `down` | Move cursor down, browsing newer history at the bottom |\n| `tui.editor.historyPrevious` | None | Select the previous prompt history entry |\n| `tui.editor.historyNext` | None | Select the next prompt history entry |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Move cursor left |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Move cursor right |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Move cursor word left |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Move cursor word right |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+a` | Move to line start |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+e` | Move to line end |\n| `tui.editor.jumpForward` | `ctrl+]` | Jump forward to character |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Jump backward to character |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Scroll up by page |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Scroll down by page |\n\nThe dedicated history actions browse prompt history regardless of cursor position and take precedence over application actions using the same key.\n\n#### Text editing\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.deleteCharBackward` | `backspace` | Delete character backward |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Delete character forward |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Delete word backward |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Delete word forward |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Delete to line start |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Delete to line end |\n| `tui.editor.yank` | `ctrl+y` | Paste most recently deleted text |\n| `tui.editor.yankPop` | `alt+y` | Cycle through deleted text after yank |\n| `tui.editor.undo` | `ctrl+-` (`ctrl+z` on Windows; `alt+z` on WSL) | Undo last edit |\n\n#### Input and selection\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insert new line |\n| `tui.input.submit` | `enter` | Submit input |\n| `tui.input.tab` | `tab` | Tab or autocomplete |\n| `tui.input.copy` | `ctrl+c` | Copy selection |\n| `tui.select.up` | `up` | Move selection up |\n| `tui.select.down` | `down` | Move selection down |\n| `tui.select.pageUp` | `pageUp` | Page up in list |\n| `tui.select.pageDown` | `pageDown` | Page down in list |\n| `tui.select.confirm` | `enter` | Confirm selection |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Cancel selection |\n\n#### Fullscreen\n\nIn fullscreen mode, these actions control the transcript and take precedence over editor actions using the same key.\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.altScreen.pageUp` | `pageUp` | Scroll the transcript up by one page |\n| `tui.altScreen.pageDown` | `pageDown` | Scroll the transcript down by one page |\n| `tui.altScreen.halfPageUp` | None | Scroll the transcript up by half a page |\n| `tui.altScreen.halfPageDown` | None | Scroll the transcript down by half a page |\n| `tui.altScreen.lineUp` | None | Scroll the transcript up by one line |\n| `tui.altScreen.lineDown` | None | Scroll the transcript down by one line |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up`, `ctrl+up` (`ctrl+up` only on Windows and WSL) | Jump to the previous marked message |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down`, `ctrl+down` (`ctrl+down` only on Windows and WSL) | Jump to the next marked message |\n| `tui.altScreen.search` | `ctrl+shift+f` (`ctrl+f` on Windows and WSL) | Search the rendered transcript |\n| `tui.altScreen.searchNext` | `enter`, `ctrl+g` | Select the next search match while searching |\n| `tui.altScreen.searchPrevious` | `shift+enter`, `ctrl+shift+g` | Select the previous search match while searching |\n| `tui.altScreen.searchClose` | `escape` | Close transcript search |\n| `tui.altScreen.top` | `ctrl+home` | Scroll to the beginning of the transcript |\n| `tui.altScreen.bottom` | `ctrl+end` | Scroll to the transcript end and follow new output |\n\n### Application\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Cancel / abort |\n| `app.clear` | `ctrl+c` | Clear editor (first) / exit (second) |\n| `app.exit` | `ctrl+d` | Exit (when editor empty) |\n| `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background |\n| `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard |\n\nOn native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior.\n\n### Sessions\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.session.new` | None | Start a new session (`/new`) |\n| `app.session.tree` | None | Open session tree navigator (`/tree`) |\n| `app.session.fork` | None | Fork current session (`/fork`) |\n| `app.session.resume` | None | Open session resume picker (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Toggle path display |\n| `app.session.toggleSort` | `ctrl+s` | Toggle sort mode |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Toggle named-only filter |\n| `app.session.rename` | `ctrl+r` | Rename session |\n| `app.session.delete` | `ctrl+d` | Delete session |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Delete session when query is empty |\n\n### Models and Thinking\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Open model selector |\n| `app.model.cycleForward` | `ctrl+p` | Cycle to next model |\n| `app.model.cycleBackward` | `shift+ctrl+p` (`alt+p` on Windows and WSL) | Cycle to previous model |\n| `app.models.save` | `ctrl+s` | Save the selected default model or scoped model configuration to settings |\n| `app.thinking.cycle` | `shift+tab` | Cycle thinking level |\n| `app.thinking.save` | `ctrl+s` | Save current thinking level to settings |\n| `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks |\n\n### Display and Message Queue\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Collapse or expand tool output |\n| `app.message.copy` | `ctrl+x` | Copy the selected message in `/tree`; in fullscreen mode, copy the active selection when `fullscreenCopyOnSelect` is `false`; otherwise copy the last assistant message. On OAuth sign-in screens, copy the sign-in URL |\n| `app.message.followUp` | `alt+enter` (`ctrl+q` on Windows and WSL) | Queue follow-up message |\n| `app.message.dequeue` | `alt+up` (`alt+q` on Windows and WSL) | Restore queued messages to editor |\n\n### Tree Navigation\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Fold current branch segment, or jump to the previous segment start |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Unfold current branch segment, or jump to the next segment start or branch end |\n| `app.tree.editLabel` | `shift+l` | Edit the label on the selected tree node |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Toggle label timestamps in the tree |\n| `app.tree.filter.default` | `ctrl+d` | Set tree filter to default view |\n| `app.tree.filter.noTools` | `ctrl+t` | Toggle tree filter that hides tool results |\n| `app.tree.filter.userOnly` | `ctrl+u` | Toggle tree filter that shows only user messages |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Toggle tree filter that shows only labeled entries |\n| `app.tree.filter.all` | `ctrl+a` | Toggle tree filter that shows all entries |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Cycle tree filter forward |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Cycle tree filter backward |\n\n### Scoped Models Selector\n\nUsed inside the scoped models selector (opened via `/scoped-models`).\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.models.enableAll` | `ctrl+a` | Enable all models (or all matching the current search) |\n| `app.models.clearAll` | `ctrl+x` | Clear all models (or all matching the current search) |\n| `app.models.toggleProvider` | `ctrl+p` | Toggle all models for the current provider |\n| `app.models.reorderUp` | `alt+up` | Move the selected model up in the cycle order |\n| `app.models.reorderDown` | `alt+down` | Move the selected model down in the cycle order |\n","packages/coding-agent/docs/llama-cpp.md":"# Local Models with llama.cpp\n\nPi supports the [llama.cpp](https://github.com/ggml-org/llama.cpp) router server. The router discovers multiple GGUF models and loads or unloads them on demand.\n\nUse a current llama.cpp build with router support. Follow the [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) or install a [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) for your platform.\n\n## Start the router\n\nStart `llama-server` without `--model` or `-m`. Passing a model starts single-model mode instead of router mode.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\nImportant options:\n\n- `--models-dir ~/models` discovers local GGUF files.\n- `--no-models-autoload` keeps loading explicit through `/llama`.\n- `--jinja` enables compatible chat templates and tool calling.\n- `-ngl 999` offloads as many layers as possible to the GPU.\n- `-c 32768` sets the context window for each loaded model. Omit it to use the model's native context, which may require substantially more memory.\n\nA single-file model can sit directly in the model directory. Put multimodal and multi-shard models in separate subdirectories:\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\nRestart the router after manually adding files. For per-model context sizes and other options, use [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets). For settings tuned to a specific NVIDIA GPU and model, see [Run Pi with NVIDIA RTX and llama.cpp](nvidia-rtx-llama-cpp.md).\n\n## Configure Pi\n\nStart Pi and configure the provider:\n\n```text\n/login llama.cpp\n```\n\nEnter the router URL and optional API key. The default URL is `http://127.0.0.1:8080`.\n\nIf you start the router with `--no-models-autoload`, `/login llama.cpp` only stores the connection. Run `/llama` to load a model, then `/model` to select the loaded model for the current session.\n\nEnvironment variables can configure the same values without `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nIf the server uses an API key, start `llama-server` with the matching `--api-key` value. Keep `--host 127.0.0.1` for local-only access.\n\n## Manage models\n\nRun:\n\n```text\n/llama\n```\n\n- Select an unloaded model to load it.\n- Select a loaded model to unload it.\n- Select **Download model…**, search Hugging Face, then choose a repository and quantization. Exact `owner/repository[:quant]` values also work.\n- Press Escape during a load or download to confirm cancellation.\n\nHugging Face search uses `HF_TOKEN` when set, then checks `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token`, and `~/.cache/huggingface/token`. Search also works without authentication, subject to lower rate limits. Pi warns before downloading gated repositories and links to their access page. The llama.cpp server performs the download, so its process must also have `HF_TOKEN` when the selected repository requires access.\n\nIf other models are loaded, Pi asks whether to unload them first or keep them loaded. Pi does not silently unload models and never deletes model files. The router may be shared with other clients, so `/llama` always displays the router's current state.\n\nLoaded and sleeping models appear in `/model`. Sleeping models wake automatically when selected. With router autoload enabled, unloaded preset models also appear and load when selected. With `--no-models-autoload`, load a model through `/llama` before selecting it.\n\nIf the router disconnects, `/llama` shows **Retry** and **Close**. Retry reconnects and refreshes model state without replaying the interrupted operation.\n\n## Classification\n\nClassifier models answer typed `choice`, `bool`, and `score` questions about JSON state, like TypeSafe's Jev models. The model reaches them from [`codemode`](cli.md#enable-codemode) scripts, and extensions through `ctx.modelRegistry.classify()`; see [Classifier models](models.md#use-classifier-models). Pi lists llama.cpp models as classifiers in two ways:\n\n- **Decision models** such as [Julia-1, Laya, Kev, lev, and OpenJev](https://huggingface.co/collections/ggml-org/decision-models-6abf80cca3c83f127060a769) answer natively through llama.cpp's `/v1/systemone` endpoint. They appear only as classifiers, with the `typesafe-system-one` API, and not in `/model`.\n- **Chat models** are also listed as classifiers with the same ID and the `llama-cpp-classify` API, which reads answers from next-token probabilities as described below.\n\nllama.cpp 0.6.0 and later report decision models in the router's model list: their `architecture.output_modalities` contains `decisions`. The router reads this from the GGUF metadata without loading the model, so Pi recognizes unloaded and sleeping decision models too. Older llama.cpp builds do not report it, and Pi lists their decision models as chat models.\n\n### Chat models as classifiers\n\nThe model does not generate an answer. Each question becomes one chat prompt: the state, every question of the request, the state again, and then the question with its answers under single-token labels. Labels are letters for a choice (up to 62 options), `Yes`/`No` for a bool, and digits for a score (up to 10 levels). The second copy of the state is read with the questions in view, which improved accuracy on JevBench with small models. Pi reads the probabilities of the labels as the next token and normalizes them. A choice returns every option's probability and a confidence of `(n * peak - 1) / (n - 1)`; a score returns the expected level.\n\n- Raw label probabilities are usually overconfident. The per-request `temperature` option divides the label logits before normalizing; values above 1 soften the distribution. It changes no answer.\n- Questions run one after another. Everything before the final question is the same for all questions of a request, so the server's prompt cache evaluates it once. The state appears twice, so it needs twice its size in context.\n- Small models may follow instructions written inside the state. The prompt tells the model to judge the state as data, but that is not a guarantee.\n- Hybrid models such as Qwen3.5 cannot rewind a partially cached prompt without context checkpoints. If each question reprocesses the whole state, start the router with `--ctx-checkpoints 32 --checkpoint-min-step 0`.\n\n## Troubleshooting\n\nCheck that the router is reachable:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **No models in `/llama`:** Check `--models-dir`, the directory layout, and restart the router.\n- **Model missing from `/model` with `--no-models-autoload`:** Load it with `/llama` first.\n- **Load fails or uses too much memory:** Lower `-c` or unload another model.\n- **Server is not in router mode:** Start it without `--model`, `-m`, or `-hf`.\n\nTo remove the `llama.cpp` provider and `/llama`, disable `llama.cpp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:llama.cpp\"]` in [settings](settings.md#resources).\n","packages/coding-agent/docs/mcp.md":"# MCP Servers\n\nPi connects to [Model Context Protocol](https://modelcontextprotocol.io) servers over stdio or streamable HTTP and makes their tools and resources available to the model.\n\n## Quick setup\n\nAdd a local stdio server, check the connection, then start Pi:\n\n```bash\npi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .\npi mcp list\npi\n```\n\nFor a remote server:\n\n```bash\npi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN\npi mcp list\n```\n\nThese commands add user-level servers by default. Add `--local` or `-l` to write the project configuration instead:\n\n```bash\npi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp\n```\n\nUse `/mcp` inside an interactive session to inspect connections, sign in, reconnect, change exposure, or enable and disable servers. Run `/reload` after adding, removing, or changing a server outside the session.\n\n## Configure servers\n\nPi reads user-level servers from `~/.pi/agent/mcp.json` and project servers from `.pi/mcp.json`. Project configuration is read only after [project trust](security.md#understand-project-trust) is granted. A project entry replaces a user-level entry with the same name.\n\nA project entry without `command`, `url`, or `type` overrides only `enabled`, `exposure`, and `toolExposure` of the user-level server with the same name and keeps the rest, including `env`, `headers`, and `auth`. For example, this turns off a user-level server in one project:\n\n```json\n{\n  \"mcpServers\": {\n    \"internal-tools\": { \"enabled\": false }\n  }\n}\n```\n\nThe format matches other MCP clients:\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \".\"]\n    },\n    \"docs\": {\n      \"url\": \"https://example.com/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer ${DOCS_TOKEN}\" },\n      \"description\": \"Search and read the product documentation\"\n    }\n  }\n}\n```\n\nStdio servers use `command`, `args`, `env`, and `cwd`. Relative `cwd` values resolve against the session directory. A leading `~/` in `command`, an argument, or `cwd` names the home directory.\n\nHTTP servers use `url`, `headers`, and `oauth` (see [Authenticate with OAuth](#authenticate-with-oauth)). The legacy SSE transport is not supported.\n\nBoth server types support:\n\n- `timeout`: per-request timeout in seconds (default 60). Progress notifications reset it.\n- `enabled: false`: keep the entry without connecting to it.\n- `exposure` and `toolExposure`: control how tools reach the model (see [Control tool exposure](#control-tool-exposure)).\n- `description`: what the server offers, in a sentence. It lists the server in the system prompt (see [Control tool exposure](#control-tool-exposure)), tool search ranks the server's tools by it, and codemode's `describeNamespace()` returns it. Without it, the first line of the server instructions is used once the server connects.\n\nKeep personal servers and servers with credentials in the user-level file. Use the project file only for servers the project requires, and only in trusted projects.\n\n### Configuration rules\n\n- Server names may contain only letters, digits, `_`, and `-`. Tools are named `mcp__<server>__<tool>`, with every character other than letters, digits, and `_` replaced by `_`; tools of a server whose names then collide all get a hash suffix. Server names that differ only in `-` and `_` count as the same server: a second one is rejected, and a `mcp.json` server overrides a registered one.\n- `type` is optional. A `command` selects stdio and a `url` selects streamable HTTP. When present, `type` must be `stdio`, `http`, or `streamable-http`.\n- `sse` is rejected. Servers that document an SSE endpoint often also provide streamable HTTP, commonly at `/mcp` instead of `/sse`.\n- `command` is one executable and `args` contains its arguments. It is not a shell command string.\n- `env` and `headers` values can use environment variables such as `${GITHUB_TOKEN}`. They can also run a command with `!command`, but the command must make up the whole value, for example `\"Authorization\": \"!echo Bearer $(gh auth token)\"`.\n- Invalid entries are reported and skipped without preventing other servers from connecting.\n\n`pi mcp add` and `pi mcp remove` cover common changes from a shell. See [MCP commands](cli.md#mcp-commands) for their options.\n\n### Inspect or change a server\n\n`/mcp` lists configured servers with their state, tool count, exposure, and configuration source. Servers that need attention appear first. Select a server to inspect its tools and connection details, reconnect, sign in or out, change exposure, or enable and disable it.\n\nExposure and enabled-state changes are saved to the file that defines the server without replacing unrelated content. In a trusted project, \"Enable in this project\" and \"Disable in this project\" add a project override for a user-level server; later changes to that server are saved to the override. Disabled servers remain listed. Outside the interactive TUI, `/mcp` prints server status; `/mcp login <server>`, `/mcp logout <server>`, and `/mcp reconnect <server>` perform those actions directly.\n\nShell commands work without a session: `pi mcp add`, `pi mcp remove`, `pi mcp list`, `pi mcp login`, and `pi mcp logout`. Shell commands do not load extensions.\n\n### Diagnose connection problems\n\nRun `pi mcp list` to connect to every enabled server and print its state, tools, and errors. It exits with status 1 when an entry is invalid or an enabled server is not connected. `/mcp` shows the full connection error and the tail of stderr from a failed stdio server.\n\nPi reports configuration errors, failed connections, and required sign-ins once after startup. Server logging notifications are appended to `~/.pi/agent/mcp.log` as `<time> [<server>] <level> <logger>: <message>`. The file moves to `mcp.log.1` after it grows past 5 MB.\n\nPi connects every enabled server in the background when a session starts. A server's tools appear once it connects; the `codemode` description does not list them, so it does not change when servers connect. The first prompt waits up to 10 seconds only for servers with `direct` tools, which must be declared in its request. Other servers are waited for when they are needed: a codemode script waits for the servers it names (`mcp__<server>`) and, when it calls `searchTools()` or reads `ALL_TOOLS`, for all of them; `tool_search` and the resource tools also wait for all of them. HTTP network errors and transient statuses (408, 429, and 5xx) are retried twice. A dropped connection is shown as disconnected and reconnects on the next call. When a server announces a changed tool list, new tools are added and withdrawn tools become unreachable.\n\nStopping a stdio server closes its stdin, sends SIGTERM, then sends SIGKILL to its process group. This also stops servers launched through wrappers such as `npx` or `uvx`.\n\n## Migrate configuration from another client\n\nMove the converted entry under `mcpServers` in `mcp.json`, then run `pi mcp list` to validate it.\n\n| Client | Conversion |\n|---|---|\n| Claude Desktop, Claude Code, or Cursor | Copy the existing `mcpServers` entry. |\n| VS Code | Move an entry from the top-level `servers` object and replace `${input:...}` prompts with `${NAME}` environment variables. |\n| Codex | Convert `[mcp_servers.<name>]` TOML fields such as `command`, `args`, `env`, and `url` to JSON. |\n| OpenCode | Convert `\"type\": \"local\"` to a stdio entry, split its `command` array into `command` and `args`, rename `environment` to `env`, and replace `{env:NAME}` with `${NAME}`. Convert `\"type\": \"remote\"` to a URL entry. |\n\n## Authenticate with OAuth\n\nRemote servers that use OAuth, such as Sentry, need no credentials in `mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"sentry\": { \"url\": \"https://mcp.sentry.dev/mcp\" }\n  }\n}\n```\n\nWhen the server rejects an unauthenticated connection, `/mcp` shows that it needs sign-in. Select \"Sign in\", run `/mcp login sentry`, or run `pi mcp login sentry`. Pi opens the authorization page and waits for approval. If the browser runs on another machine, such as over SSH, paste its redirected URL into the sign-in screen. A running session uses the new credentials on its next turn.\n\nPi registers itself with the authorization server, stores tokens in `~/.pi/agent/mcp-auth.json`, and refreshes access tokens when they expire or the server rejects them. If a server later requests additional scope, Pi asks for sign-in again. Signing out deletes the stored credentials.\n\nCredentials belong to a server name and URL. Servers with the same URL under different names, such as one per account, sign in separately; servers with the same name and URL in different `mcp.json` files share one sign-in.\n\nOAuth applies to HTTP servers without an `Authorization` header. For a server that does not support dynamic client registration, configure a registered client:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"clientId\": \"my-client\", \"clientSecret\": \"${EXAMPLE_SECRET}\", \"callbackPort\": 8765 }\n    }\n  }\n}\n```\n\nThe redirect URI must match the registered URI. `callbackPort` uses `http://127.0.0.1:<port>/callback`. To use another URI, set `callbackUrl`; it must use HTTP on `localhost`, `127.0.0.1`, or `[::1]`. Pi sends it exactly as written. When `callbackUrl` omits a port, Pi uses `callbackPort` or a free port and adds it to the URI, as allowed for loopback redirects by RFC 8252. `clientSecret` is optional and can use an environment variable or command.\n\nSet `scope` to a space-separated list for servers that do not advertise their required scopes. Otherwise, Pi requests the advertised scopes. Later scope requests are added to the configured value.\n\nPi registers as `pi`. Some servers only accept registrations from known clients. Set `clientName` to send another name:\n\n```json\n{\n  \"mcpServers\": {\n    \"figma\": { \"url\": \"https://mcp.figma.com/mcp\", \"oauth\": { \"clientName\": \"Claude Code\" } }\n  }\n}\n```\n\nThe name is only sent when Pi registers a client. To register again under a new name, sign out first.\n\nSome authorization servers allow clients by their Client ID Metadata Document URL instead of registering them. Set `clientRegistration` to `cimd` to identify as Pi's document on pi.dev instead of registering:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": { \"url\": \"https://mcp.example.com/mcp\", \"oauth\": { \"clientRegistration\": \"cimd\" } }\n  }\n}\n```\n\nThe client ID is `https://pi.dev/oauth/client.json` with the redirect URI `http://127.0.0.1:<port>/callback`. If the authorization server does not send the `iss` parameter in authorization responses (RFC 9207), Pi uses a document and redirect path specific to the MCP server instead: `https://pi.dev/oauth/<id>/client.json` with `http://127.0.0.1:<port>/callback/<id>`. The authorization server must advertise Client ID Metadata Document support and public clients, or sign-in fails. `cimd` cannot be combined with `clientId` or `clientName`, and a `callbackUrl` must use `localhost` or `127.0.0.1` with the path `/callback`.\n\nPi finds the authorization server through the server's protected resource metadata (RFC 9728) and checks that the authorization server's metadata names the expected issuer (RFC 8414). Some servers advertise the wrong authorization server or none, so sign-in opens a page that does not exist. Set `authServerMetadataUrl` to the metadata document of the right authorization server:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"authServerMetadataUrl\": \"https://example.okta.com/.well-known/openid-configuration\" }\n    }\n  }\n}\n```\n\nPi uses that document instead of discovery and trusts it as configured, so only point it at a document you trust. The URL must use HTTPS, except on `localhost`, `127.0.0.1`, or `[::1]`.\n\n## Control tool exposure\n\nEach server tool is registered as `mcp__<server>__<tool>`. The server's `exposure` determines how the model reaches it:\n\n| Exposure | Behavior | Typical use |\n|---|---|---|\n| `codemode` (default) | Callable from [`codemode`](cli.md#tools) scripts, but neither declared to the model nor listed in the codemode description. Scripts find tools with `searchTools()`, `describeTool()`, or `ALL_TOOLS`. | General MCP servers, especially when scripts should combine or filter calls. |\n| `deferred` | Not declared until [`tool_search`](cli.md#tools) loads a match for the next model call. | Large servers whose tools should be called directly after discovery. |\n| `direct` | Declared to the model like a built-in tool and also callable from codemode. | Small, frequently used tool sets. |\n| `hidden` | Registered but unreachable. | Servers or tools that should remain unavailable. |\n\n`codemode-deferred` is accepted as an alias for `codemode`.\n\nServers with `codemode` or `deferred` tools are listed in the `mcp_servers` section of the system prompt, with how their tools are reached and one line from the configured `description` or, once connected, from the server instructions. Pi updates the section when a prompt starts, after waiting for servers with `direct` tools. When it changed, for example because a server connected and its summary became available, Pi appends the new section to the conversation instead of changing tool declarations, so earlier messages stay cached. `describeNamespace()` and the `namespace` option of `searchTools()` accept `mcp__dev-radius`, `mcp__dev_radius`, `dev-radius`, or `dev_radius`.\n\nPi activates `codemode` when a server with `codemode` exposure connects. It activates `tool_search` for a server with `deferred` exposure. To make the model see a tool without searching, give it `direct` exposure with `toolExposure`.\n\n`toolExposure` overrides the server exposure for individual tools. Keys are exact server tool names or patterns where `*` matches any characters. Exact names win over patterns; among patterns, the first match wins. A server with `hidden` exposure can expose only selected tools:\n\n```json\n{\n  \"mcpServers\": {\n    \"github\": {\n      \"url\": \"https://api.githubcopilot.com/mcp/\",\n      \"exposure\": \"deferred\",\n      \"toolExposure\": {\n        \"search_code\": \"direct\",\n        \"get_*\": \"codemode\",\n        \"delete_*\": \"hidden\"\n      }\n    }\n  }\n}\n```\n\n`pi mcp list` marks tools whose exposure differs from their server. The Tools view in `/mcp` also shows the effective exposure.\n\nTools with `codemode` or `deferred` exposure can be reached through either indirect mechanism: codemode scripts can call them, and `tool_search` can load them. Codemode calls do not depend on the active tool set, so they remain available after `/tree`, resume, and fork. Tools loaded by `tool_search` are recorded in the transcript and remain declared on that branch.\n\n`--tools` does not remove MCP tools unless one of its entries starts with `mcp__`; `pi --tools read,codemode,'mcp__radius__*'` keeps only the tools of `radius`. `--exclude-tools` accepts the same patterns, and `--no-mcp` disables MCP for one run (see [Tools](cli.md#mcp-tools)).\n\nTo keep `codemode` active without MCP servers, add `\"defaultTools\": [\"+codemode\"]` to [settings](settings.md#tools). To prevent automatic codemode activation, set `\"autoEnableCodemode\": false` beside `mcpServers`. A project value overrides the user-level value. Pi warns once when neither `codemode` nor `tool_search` is active and non-direct tools cannot be called.\n\nText results over 20 KB reach the model with their middle removed around a `…N chars truncated…` marker. The full text is saved to a temporary file named in the result. Codemode scripts receive the complete result and can reduce it before returning output to the model.\n\nCodemode scripts receive the complete MCP `CallToolResult`, including `content`, `structuredContent`, and `isError`. A result with `isError` resolves inside scripts but is reported as an error for direct calls. `image(result.content[0])` forwards an image block. Server instructions are not part of any tool description; scripts read them with `describeNamespace(\"mcp__<server>\")`, which also returns the server's tool names.\n\n## Use resources\n\nWhen a connected server offers [resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources), Pi adds the resource tools used by Codex and OpenCode:\n\n- `list_mcp_resources` lists resources as JSON: `{ server?, resources: [{ server, uri, name, ... }], nextCursor? }`. With `server`, it lists one page; `cursor` continues with the next page. Without `server`, it lists every resource from every server.\n- `list_mcp_resource_templates` lists URI templates for resources the servers do not list directly.\n- `read_mcp_resource` reads a resource by `server` and `uri`. Text reaches the model as text and images as images. Other binary resources are saved to temporary files, and the model receives the path. Scripts receive `{ server, uri, contents }`.\n\nThese tools reach every enabled, non-hidden server with resources. Their exposure is the widest exposure among those servers: `direct`, then `codemode` or `deferred`. Resource links in tool results identify `read_mcp_resource` and the server.\n\nResources for MCP Apps, identified by `ui://` URIs or `text/html;profile=mcp-app`, are omitted because Pi does not render them. Resource icons are also omitted.\n\nReading and listing resources is retried once after a transient HTTP error (408, 429, or 5xx). Tool calls are not retried because the server may already have performed them.\n\n## Permissions\n\nEvery MCP call passes through Pi's tool pipeline. Extension `tool_call` and `tool_result` handlers, including permission gates, therefore apply to MCP tools. Calls made from codemode scripts carry the codemode call ID as `parentToolCallId`.\n\n`pi.getAllTools()` reports the annotations declared by each server: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. Permission extensions can use these hints to decide which calls require confirmation (see [Tool exposure](extensions.md#tool-exposure)). Resource tools are marked read-only.\n\n## Extensions and SDK\n\n### Add servers from extensions\n\nExtensions can add servers for the current session with `pi.registerMcpServer(name, config)`, using the same shape as an `mcpServers` entry (see [MCP servers in extensions](extensions.md#mcp-servers)). Registered servers connect like configured servers and appear in `/mcp` with the extension as their source.\n\nChanges to enabled state or exposure apply only to the current session. A file-configured server with the same name takes precedence, and `/mcp` lists the overridden registration. `pi mcp` shell commands do not load extensions and only see file-configured servers.\n\n### Replace the built-in MCP support\n\nAn installed extension that registers `/mcp`, such as `pi-mcp-adapter`, replaces the built-in MCP support for sessions. Pi then does not read `mcp.json` or connect its servers in a session, and `/mcp` belongs to the extension. Remove the extension to restore the built-in behavior. To disable built-in MCP support without a replacement, disable `mcp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:mcp\"]` in [settings](settings.md#resources). `--no-mcp` disables it for one run.\n\nAn extension that registers `codemode` or `tool_search` similarly replaces the built-in tool with that name. Pi activates the replacement for MCP servers and waits for the servers its calls need only when it declares [`reachesTools`](extensions.md#tool-exposure). Shell-level `pi mcp` commands always use the built-in implementation.\n\n### Use MCP from the SDK\n\nSDK sessions do not load built-in extensions. Add the MCP extension, the codemode extension for `codemode` servers, and the tool-search extension for `deferred` servers to the resource loader. See [Codemode and MCP](sdk.md#codemode-mcp).\n","packages/coding-agent/docs/message-types.md":"# Message Types\n\nPi uses `AgentMessage` values in SDK state, lifecycle events, RPC responses, and persisted session message entries. This page defines those shared messages and their content blocks.\n\nMessage timestamps are Unix timestamps in milliseconds. They are different from the ISO 8601 timestamps on [session entries](session-format.md#entry-base).\n\nSource definitions:\n\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/ai/src/types.ts) defines provider-facing messages and content blocks.\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts) defines the extensible `AgentMessage` union.\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/messages.ts) adds coding-agent message roles.\n\n## Content blocks\n\n### TextContent\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n  textSignature?: string;\n}\n```\n\n`textSignature` contains provider-specific message metadata. Treat it as opaque.\n\n### ImageContent\n\n```typescript\ninterface ImageContent {\n  type: \"image\";\n  data: string;\n  mimeType: string;\n}\n```\n\n`data` is base64-encoded image data. `mimeType` identifies its media type, such as `image/png` or `image/jpeg`.\n\n### ThinkingContent\n\n```typescript\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n  thinkingSignature?: string;\n  redacted?: boolean;\n}\n```\n\nThinking signatures contain provider-specific replay data. Treat them as opaque. A redacted block can have no visible thinking text while retaining an encrypted payload in `thinkingSignature`.\n\n### ToolCall\n\n```typescript\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n  thoughtSignature?: string;\n  namespace?: string;\n}\n```\n\n`thoughtSignature` is provider-specific. `namespace` identifies an OpenAI Responses namespace for dynamically loaded or namespaced tools.\n\n## Usage\n\nAssistant messages always contain usage. Tool results can contain usage when the tool performed nested model work.\n\n```typescript\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  cacheWrite1h?: number;\n  reasoning?: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\nWhen present, `reasoning` is already included in `output`; do not add it again. `cacheWrite1h` is the subset of `cacheWrite` written with one-hour retention.\n\n## Base messages\n\n### SystemMessage\n\n```typescript\ninterface SystemMessage {\n  role: \"system\";\n  content: string | TextContent[];\n  sections?: Record<string, string | null>;\n  toolsAdded?: Tool[];\n  toolsRemoved?: ToolReference[];\n  timestamp: number;\n}\n```\n\nThe leading system message declares the initial prompt and tools. Later system messages can append instructions, replace or remove named prompt sections, and add or remove tools. Replaying them in order yields the current state.\n\n### UserMessage\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;\n}\n```\n\n### AssistantMessage\n\n```typescript\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  responseModel?: string;\n  responseId?: string;\n  providerThinkingLevel?: string;\n  thinkingLevel?: ModelThinkingLevel;\n  diagnostics?: AssistantMessageDiagnostic[];\n  usage: Usage;\n  stopReason: \"pending\" | \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\" | \"deferred\";\n  deferred?: DeferredHandle;\n  errorMessage?: string;\n  rawStopReason?: string;\n  endTurn?: boolean;\n  timestamp: number;\n}\n```\n\n`responseModel` records a concrete provider response model when it differs from the requested model. `responseId`, `providerThinkingLevel`, `thinkingLevel`, `diagnostics`, and `rawStopReason` preserve provider or runtime details.\n\n`\"pending\"` is used for a partial assistant message while it streams. The completed message in `message_end` has a terminal stop reason, and Pi does not persist `\"pending\"` assistant messages in session JSONL.\n\nA `\"deferred\"` response has a `DeferredHandle` with the provider data needed to retrieve it:\n\n```typescript\ninterface DeferredHandle {\n  provider: string;\n  modelId: string;\n  api: string;\n  id: string;\n  expiresAt?: number;\n  pollAfterMs?: number;\n  data?: JsonValue;\n}\n```\n\n### ToolResultMessage\n\n```typescript\ninterface ToolResultMessage<TDetails = any> {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: TDetails;\n  usage?: Usage;\n  nestedCalls?: NestedToolCalls;\n  isError: boolean;\n  timestamp: number;\n}\n```\n\n`details` is tool-specific. Optional `usage` reports nested model work performed by the tool and contributes to full-session statistics, but it is not part of the main model-call usage. `nestedCalls` records bounded metadata about calls this tool made to other tools:\n\n```typescript\ninterface NestedToolCalls {\n  calls: NestedToolCallRecord[];\n  complete: boolean;\n}\n\ninterface NestedToolCallRecord {\n  id: string;\n  name: string;\n  arguments?: JsonObject;\n  argumentsBytes?: number;\n  status: \"ok\" | \"error\" | \"unfinished\";\n  durationMs?: number;\n  error?: string;\n}\n```\n\n## Coding-agent messages\n\nThe coding-agent package extends `AgentMessage` with four roles.\n\n### BashExecutionMessage\n\nCreated by direct shell commands, including the RPC [`bash`](rpc-commands.md#bash) command. It is not an LLM tool result.\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;\n  timestamp: number;\n}\n```\n\nUnless `excludeFromContext` is true, Pi converts this message to user-role text before the next model request.\n\n### CustomMessage\n\nCreated when an extension sends a context message.\n\n```typescript\ninterface CustomMessage<T = unknown> {\n  role: \"custom\";\n  customType: string;\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;\n  details?: T;\n  timestamp: number;\n}\n```\n\nPi converts its content to a user message for model requests. `display` controls terminal rendering; `details` is not sent to the model.\n\n### BranchSummaryMessage\n\n```typescript\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string | null;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `branch_summary` entry.\n\n### CompactionSummaryMessage\n\n```typescript\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `compaction` entry.\n\n## AgentMessage union\n\nIn the coding agent, the union is equivalent to:\n\n```typescript\ntype AgentMessage =\n  | SystemMessage\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\nAt the lower-level agent package, `AgentMessage` is `Message | CustomAgentMessages[keyof CustomAgentMessages]`. Applications can add roles through TypeScript declaration merging, so consumers should tolerate unknown custom roles when they accept messages from an augmented host.\n","packages/coding-agent/docs/models-configuration.md":"# Models Configuration\n\n<!-- This file is generated from TypeScript source. Do not edit it manually; run npm run generate:references. -->\n\nThis reference lists the fields of `models.json`. See [Configure a compatible endpoint](models.md#configure-a-compatible-endpoint) for examples and [Configuration](configuration.md#agent-directory) for the file location.\n\nAdd `\"$schema\": \"https://pi.dev/schemas/models.schema.json\"` to `models.json` for editor validation and completion.\n\n## Providers\n\nEach key under `providers` is a provider ID. A built-in provider ID changes that provider. Any other ID adds a provider, which needs `baseUrl` and an `api` at provider or model level.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name. |\n| `baseUrl` | string |  | API base URL. Also applies to the provider's built-in models. |\n| `apiKey` | string |  | Literal key, \"$NAME\" environment interpolation, or \"!command\" output. |\n| `api` | string |  | API for the provider's models, such as \"openai-completions\", \"openai-responses\", \"anthropic-messages\", or \"google-generative-ai\". |\n| `oauth` | `\"radius\"` |  | OAuth login flow. Requires baseUrl. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n| `authHeader` | boolean | `false` | Whether to send the resolved API key as Authorization: Bearer. |\n| `models` | [`object[]`](#models) |  |  |\n| `modelOverrides` | [object](#model-overrides) |  |  |\n\n## Models\n\nEach `models` entry adds a model to the provider. An entry whose `id` matches an existing model replaces it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `id` | string |  | Required. Model ID sent to the API. |\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `api` | string |  | Overrides the provider api. |\n| `baseUrl` | string |  | Overrides the provider baseUrl. |\n| `reasoning` | boolean | `false` | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` | `[\"text\"]` |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number | `128000` | Context window in tokens. |\n| `maxTokens` | number | `16384` | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Model overrides\n\nEach `modelOverrides` key is the ID of a built-in or extension model. Unknown IDs are ignored. Object fields, such as `cost`, merge with the model's value field by field; other fields replace it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `reasoning` | boolean |  | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` |  |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Input cost in USD per million tokens. |\n| `cost.output` | number |  | Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number |  | Context window in tokens. |\n| `maxTokens` | number |  | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Input limits\n\nSee [Describe model input and caching](models.md#describe-model-input-and-caching).\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `maxRequestBytes` | integer |  | Maximum serialized provider request size in bytes. |\n| `images` | object |  |  |\n| `images.resize` | object |  | Cache-safe resize profile applied before a new image enters conversation history. |\n| `images.resize.maxWidth` | integer |  |  |\n| `images.resize.maxHeight` | integer |  |  |\n| `images.resize.maxBytes` | integer |  | Maximum base64-encoded payload size in bytes. |\n| `images.resize.jpegQuality` | integer |  |  |\n| `images.maxPerMessage` | integer |  | Maximum images accepted in one provider message. |\n| `images.maxPerRequest` | integer |  | Maximum images accepted across one provider request. |\n\n## Compatibility\n\n`compat` describes verified differences from an API's standard request and response behavior. Model values override provider values per field. The fields that apply depend on the model's `api`.\n\n### `openai-completions`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStore` | boolean |  | Whether the provider supports the store field. Default: auto-detected from URL. |\n| `supportsDeveloperRole` | boolean |  | Whether the provider supports the developer role instead of system. Default: auto-detected from URL. |\n| `supportsReasoningEffort` | boolean |  | Whether the provider supports reasoning_effort. Default: auto-detected from URL. |\n| `supportsUsageInStreaming` | boolean | `true` | Whether the provider supports stream_options.include_usage for token usage in streaming responses. |\n| `supportsFinishReason` | boolean | `true` | Whether streamed responses include finish_reason. When false, pi infers stop or toolUse when the stream ends. |\n| `maxTokensField` | `\"max_completion_tokens\" \\| \"max_tokens\"` |  | Which field to use for max tokens. Default: auto-detected from URL. |\n| `requiresToolResultName` | boolean |  | Whether tool results require the name field. Default: auto-detected from URL. |\n| `requiresAssistantAfterToolResult` | boolean |  | Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. |\n| `requiresThinkingAsText` | boolean |  | Whether thinking blocks must be converted to text blocks with &lt;thinking> delimiters. Default: auto-detected from URL. |\n| `requiresReasoningContentOnAssistantMessages` | boolean |  | Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. |\n| `thinkingFormat` | `\"openai\" \\| \"openrouter\" \\| \"deepseek\" \\| \"together\" \\| \"baseten\" \\| \"zai\" \\| \"qwen\" \\| \"chat-template\" \\| \"qwen-chat-template\" \\| \"string-thinking\" \\| \"ant-ling\"` |  | Format for reasoning or thinking parameters. When omitted, Pi auto-detects the format from the provider URL. \"openai\" uses reasoning_effort, \"openrouter\" uses reasoning.effort, \"deepseek\" uses thinking.type plus reasoning_effort when supported, \"together\" uses reasoning.enabled plus reasoning_effort when supported, \"baseten\" uses configurable chat_template_args plus reasoning_effort when supported, \"zai\" uses thinking.type, \"qwen\" uses top-level enable_thinking, \"qwen-chat-template\" uses chat_template_kwargs.enable_thinking and preserve_thinking, \"chat-template\" uses configurable chat_template_kwargs, \"string-thinking\" uses top-level thinking, and \"ant-ling\" uses reasoning.effort only when the mapped effort is non-null. |\n| `chatTemplateKwargs` | object |  | Kwargs sent as chat_template_kwargs when thinkingFormat is \"chat-template\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `chatTemplateArgs` | object |  | Arguments sent as chat_template_args when thinkingFormat is \"baseten\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `openRouterRouting` | object |  | OpenRouter provider routing preferences. Controls which upstream providers OpenRouter routes requests to. Sent as the provider field in the OpenRouter API request body. See https://openrouter.ai/docs/guides/routing/provider-selection. |\n| `vercelGatewayRouting` | object |  | Vercel AI Gateway routing preferences. Controls which upstream providers the gateway routes requests to. See https://vercel.com/docs/ai-gateway/models-and-providers/provider-options. |\n| `zaiToolStream` | boolean | `false` | Whether z.ai supports top-level tool_stream for streaming tool call deltas. |\n| `thinkingTokenBudgetField` | `\"thinking_token_budget\" \\| \"thinking_budget\" \\| \"thinking_budget_tokens\"` |  | Top-level request field used to cap reasoning tokens from thinkingBudgets. Reasoning and the answer share max_tokens on these endpoints. \"thinking_token_budget\" is vLLM, \"thinking_budget\" is Qwen, DashScope, or SGLang, and \"thinking_budget_tokens\" is llama.cpp. Off by default and not set on the generated catalog. |\n| `supportsThinkingTokenBudget` | boolean | `false` | Alias for thinkingTokenBudgetField: \"thinking_token_budget\" (vLLM). Prefer thinkingTokenBudgetField. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether the provider supports OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system or developer messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `supportsMidConvoToolAdditions` | boolean | `false` | Whether system messages can introduce additional tools mid-conversation. Requires supportsMidConvoSystemMessages. The generated catalog enables this for capable models. |\n| `supportsStrictMode` | boolean | `false` | Whether the provider supports the strict field in tool definitions. Generated capable models enable it explicitly. |\n| `cacheControlFormat` | `\"anthropic\"` |  | Cache control convention for prompt caching. Anthropic applies cache_control markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send session-affinity data from options.sessionId. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id, x-client-request-id, and x-session-affinity; openai-nosession sends x-client-request-id and x-session-affinity; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean |  | Whether the provider supports long prompt cache retention (prompt_cache_retention: \"24h\" or Anthropic-style cache_control.ttl: \"1h\", depending on format). Default: auto-detected from provider and URL. |\n| `vllmPriority` | number |  | vLLM scheduler priority sent as the top-level priority request field. Lower values are handled earlier and the server default is 0. Only meaningful with --scheduling-policy priority. Off by default and not set on the generated catalog. |\n\n### `openai-responses`\n\nAlso applies to `azure-openai-responses` and `openai-codex-responses`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsDeveloperRole` | boolean | `true` | Whether the provider supports the developer role instead of system. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts developer or system messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id and x-client-request-id; openai-nosession sends x-client-request-id; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports long prompt cache retention. This uses prompt_cache_options.ttl: \"30m\" on GPT-5.6+ and prompt_cache_retention: \"24h\" on earlier models. |\n| `supportsStrictMode` | boolean |  | Whether the provider supports strict JSON-schema function tools. Defaults are API-specific; generated OpenAI models enable it explicitly. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether to emit OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsAdditionalTools` | boolean | `false` | Whether the model supports message-anchored additional_tools input items. |\n| `supportsToolSearch` | boolean | `false` | Whether the model supports client-executed tool search for transcript-anchored additions. |\n| `supportsExplicitPromptCacheMode` | boolean | `false` | Whether the model accepts prompt_cache_options. Older OpenAI models reject the parameter. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether thinking level changes are sent as configuration_update input items instead of changing reasoning.effort, so a change keeps the prompt cache. Only used by the openai-responses API, and ignored with Sign in with ChatGPT on the openai provider. |\n| `supportsMaxOutputTokens` | boolean | `true` | Whether the provider accepts max_output_tokens. Some Codex-protocol gateways reject it. |\n\n### `anthropic-messages`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsEagerToolInputStreaming` | boolean | `true` | Whether the provider accepts per-tool eager_input_streaming. When false, the Anthropic provider omits tools[].eager_input_streaming and sends the legacy fine-grained-tool-streaming-2025-05-14 beta header for tool-enabled requests. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports Anthropic long cache retention through cache_control.ttl. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send x-session-affinity from options.sessionId when caching is enabled. Required for providers like Fireworks that use session affinity for prompt cache routing; requests to the same replica maximize cache hits. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openrouter\"` |  | Session-affinity format. openrouter sends x-session-id; when unset, sends x-session-affinity. |\n| `supportsCacheControlOnTools` | boolean | `true` | Whether the provider supports Anthropic-style cache_control markers on tool definitions. When false, cache_control is omitted from tool parameters. Some Anthropic-compatible providers, such as Fireworks, do not support this field on tools and may reject or ignore it. |\n| `supportsTemperature` | boolean | `true` | Whether the model accepts the Anthropic temperature request field. Claude Opus 4.7+ rejects non-default values. |\n| `forceAdaptiveThinking` | boolean | `false` | Whether to force adaptive thinking (thinking.type: adaptive plus output_config.effort) regardless of model ID. Built-in models that require adaptive thinking set this in generated metadata. Custom Anthropic-compatible providers can set this to true for any model whose upstream requires the adaptive format. Set false to opt out on overridden built-in models. |\n| `allowEmptySignature` | boolean | `false` | Whether to replay empty thinking signatures instead of converting thinking to text. |\n| `supportsStrictTools` | boolean | `false` | Whether the provider supports Anthropic strict tool schemas. Generated Anthropic models enable it explicitly. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether the exact model transport supports effort-only system messages and thinking binding controls. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system-role messages inside the conversation. When false, later system messages are folded into the top-level system prompt. |\n| `supportsMidConvoToolChanges` | boolean | `false` | Whether the exact model accepts mid-conversation tool_addition and tool_removal blocks. Requires supportsMidConvoSystemMessages. |\n| `allowedFallbackModels` | `object[]` |  | Models Anthropic accepts for server-side refusal fallback, with local pricing metadata for returned fallback responses. When absent or empty, callers must omit fallbacks; Anthropic rejects the field for models with no permitted fallback targets. |\n\n### `bedrock-converse-stream`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStrictMode` | boolean | `false` | Whether the model supports Bedrock strict tool schemas. |\n\n### `mistral-conversations`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system messages after the conversation has started. When false, later system messages are folded into the leading system message. |\n\n### `typesafe-system-one`\n\nAlso applies to `cloudflare-workers-ai-system-one`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `imageInput` | `\"assets\" \\| \"images\" \\| \"state-image-urls\"` |  | How the service takes classifier images. \"assets\" sends TypeSafe's assets map (PNG and JPEG) and references the images from the state, as TypeSafe and Jev on Workers AI expect. \"images\" sends Clef's images list of data URLs (PNG, JPEG, and WebP, at most four), as Clef on Workers AI expects. \"state-image-urls\" places image_url parts before the state, as OpenRouter expects; OpenRouter drops assets without an error. Auto-detected from the provider, baseUrl, and model ID. Classifier models read it from the provider-level compat; models and modelOverrides entries apply to chat models only. |\n| `sendImageOptInHeader` | boolean |  | Whether requests with images send the TypeSafe-Danger-Zone-Images: true header, which TypeSafe's own API requires for its experimental image input. Defaults to true for the typesafe-system-one API unless the model is detected as OpenRouter. |\n","packages/coding-agent/docs/models.md":"# Choose a Model\n\nFor a built-in provider, start with `/login`, then choose a model with `/model`. Use custom model configuration only when Pi does not already include the provider or endpoint you need.\n\n## Choose a connection\n\n| What you have | Recommended setup |\n|---|---|\n| A supported subscription | Sign in through `/login` |\n| A provider API key | Store it through `/login` or set its environment variable |\n| A local GGUF model | Connect Pi to the llama.cpp router |\n| An OpenAI-, Anthropic-, or Google-compatible endpoint | Add it to `models.json` |\n| A provider with a custom protocol or authentication flow | Build or install a provider extension |\n\nBrowse the [model catalog](https://pi.dev/models) for current providers, model IDs, capabilities, context limits, and pricing. Pi starts with its bundled catalog and can overlay newer catalog data from pi.dev. Cached catalog data remains available offline; run `pi update --models` to force a refresh.\n\n## Authenticate\n\nRun `/login` and select a provider. Pi stores credentials in [`auth.json`](configuration.md#agent-directory). Run `/logout` to remove stored credentials for a provider.\n\nYou can instead provide an API key through the provider's environment variable. This is useful in CI and other environments where Pi should not write credentials. [Providers](providers.md) lists the variables and provider-specific setup.\n\nWhen several credential sources are configured, Pi uses a runtime `--api-key` first, then a stored `auth.json` credential, an `apiKey` from `models.json`, and finally the provider's environment variables or ambient cloud credentials. Provider extensions can define their own authentication behavior.\n\nKeep `auth.json` and any credential commands private. Project settings and extensions can execute inside the Pi process after you trust a project. Review [Security](security.md) before loading configuration from an untrusted directory.\n\n## Select a model\n\nRun `/model` to search available models. The picker shows models whose providers have usable authentication. Press `Ctrl+S` on a model to save it as the default for new sessions.\n\nRun `/thinking` to select the thinking level for the current model. Press `Ctrl+S` there to save the startup level. Pi limits the choices to levels supported by the selected model.\n\n`Ctrl+P` cycles through available models. Use `/scoped-models` to control that cycle and save the selection, or configure model patterns through [Settings](settings.md#model-cycling).\n\nA session records model and thinking-level changes. Resuming the session restores them without changing defaults for new sessions.\n\n## Connect local models\n\nPi integrates directly with the llama.cpp router. The router discovers GGUF files and loads models on demand. Pi's `/llama` command manages the router, while `/model` selects one of its loaded models.\n\nFollow [Local Models with llama.cpp](llama-cpp.md) for server startup, model layout, downloads, and connection troubleshooting.\n\nFor Ollama, LM Studio, vLLM, SGLang, and other compatible servers, [configure a compatible endpoint](#configure-a-compatible-endpoint) in `models.json`.\n\n## Configure a compatible endpoint\n\nUse [`models.json`](configuration.md#agent-directory) when an endpoint speaks an API Pi already supports. This includes most Ollama, LM Studio, vLLM, SGLang, and proxy deployments.\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/models.schema.json\",\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\nThe dummy key makes the model available to Pi; Ollama ignores it. For an authenticated endpoint, `apiKey` and header values can use `$NAME` or `${NAME}` environment interpolation, a literal value, or a leading `!command`. `$$` and `$!` produce a literal `$` and `!`. Commands in `models.json` run at request time and are not cached by Pi.\n\nOpening `/model` reloads the file. A `models` entry adds or replaces a model with the same ID on that provider. Use `modelOverrides` to change metadata for an existing built-in or extension-provided model without replacing the provider's model list. Unknown override IDs are ignored.\n\nTo route a built-in provider through a proxy, set only its `baseUrl`. Its models and authentication stay unchanged:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": { \"baseUrl\": \"https://proxy.example.com/v1\" }\n  }\n}\n```\n\n[Models Configuration](models-configuration.md) lists every field.\n\n### Describe model input and caching\n\nUse `inputLimits.images.resize` to control how Pi encodes new image attachments, `read` results, and tool-result images before storing them in conversation history:\n\n```json\n{\n  \"id\": \"vision-model\",\n  \"input\": [\"text\", \"image\"],\n  \"inputLimits\": {\n    \"images\": {\n      \"resize\": {\n        \"maxWidth\": 1568,\n        \"maxHeight\": 1568,\n        \"maxBytes\": 524288,\n        \"jpegQuality\": 75\n      }\n    }\n  }\n}\n```\n\n`maxBytes` limits the base64-encoded payload. Omitted resize fields use conservative defaults of 2000 by 2000 pixels, 4.5 MiB encoded, and JPEG quality 80. Images are encoded once; changing models does not rewrite historical images. The catalog can also describe hard request limits with `inputLimits.maxRequestBytes`, `images.maxPerMessage`, and `images.maxPerRequest`, but Pi does not yet rewrite or reject history based on them.\n\n<a id=\"prompt-cache-lifetimes\"></a>\n\nUse `promptCache` to declare the provider's best-effort cache lifetime in seconds for the `short` or `long` retention tier:\n\n```json\n{ \"id\": \"claude-sonnet-5\", \"promptCache\": { \"short\": 300, \"long\": 3600 } }\n```\n\nChoose the conservative end of any published range. A model without a lifetime for the active tier is not eligible for cache warming. A `modelOverrides` entry can set `inputLimits` or `promptCache` for a built-in or extension model, including a model accessed through a validated proxy. See [`cacheWarming`](settings.md#model-and-thinking).\n\n### Configure sampling by thinking level\n\nOpenAI-compatible APIs support free-form `samplingParams` model defaults and `samplingParamsByThinkingLevel` overrides. The latter uses Pi thinking-level keys (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`), not provider values from `thinkingLevelMap`:\n\n```json\n{\n  \"id\": \"qwen-thinking-model\",\n  \"reasoning\": true,\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95\n  },\n  \"samplingParamsByThinkingLevel\": {\n    \"off\": {\n      \"temperature\": 0.7,\n      \"top_p\": 0.8\n    },\n    \"high\": {\n      \"top_k\": 20\n    }\n  }\n}\n```\n\nPi first clamps unsupported thinking levels, then merges model `samplingParams`, the effective level's override, and request-level `samplingParams` in that order. Later values win per key. Missing levels inherit the model defaults. `modelOverrides` merges per-level entries per key with the base model. These fields apply only to `openai-completions`, `openai-responses`, and `azure-openai-responses`; other APIs ignore them.\n\nCompatibility settings should describe verified differences in the endpoint's request or response behavior. Do not enable them based only on an endpoint advertising OpenAI or Anthropic compatibility.\n\n## Use classifier models\n\nClassifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef, Clef Flash, and Clef Omni models from Workers AI and OpenRouter, and OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions):\n\n| Provider | Model IDs | Authentication |\n|---|---|---|\n| `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |\n| `openrouter` | `typesafe/jev-1.13`, `typesafe/jev-1.14`, `~typesafe/jev-latest`, `cloudflare/clef`, `cloudflare/clef-flash`, `cloudflare/clef-omni` | `OPENROUTER_API_KEY` or `/login` |\n| `cloudflare-workers-ai` | `typesafe/jev`, `typesafe/jev-1.14.0`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`, `@cf/cloudflare/clef-omni` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |\n| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |\n| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |\n| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |\n\nChat models on a [llama.cpp router](llama-cpp.md#classification) are also listed as classifier models.\n\nOpenAI's Decisions API needs an API key. Sign in with ChatGPT credentials do not work with it, so `gpt-6-luna` is not listed as available while `openai` is logged in through `/login`, even when `OPENAI_API_KEY` is set; log out of `openai` to use the key. The API rejects inputs above 922K tokens, but requests that run longer than about five seconds, currently above roughly 600K input tokens, fail with a gateway timeout.\n\nClassifier models do not appear in `/model`. The model reaches them through the [`codemode`](cli.md#enable-codemode) tool, which is off unless an MCP server turned it on. Enable it with `\"defaultTools\": [\"+codemode\"]` in [settings](settings.md#tools). Scripts then list classifier models with `models.getAvailableOfType(\"classifier\")` and call `models.classify(model, { state, questions })`:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst result = await models.classify(jev, {\n  state: { message: \"The change works, thanks.\" },\n  questions: {\n    approved: {\n      type: \"bool\",\n      instructions: \"Does the user approve of the result?\",\n      criteria: { true: \"Approval\", false: \"No approval\" },\n    },\n  },\n});\nreturn result.answers;\n```\n\n[Codemode](codemode.md#classify) describes the question and answer types.\n\nClassifier models whose `input` includes `\"image\"` also judge images passed in `images`; other classifier models return an error for them. Image input works with GPT-6 Luna, with Jev from `typesafe` and `cloudflare-workers-ai`, with Clef from `cloudflare-workers-ai`, and with the OpenRouter models whose catalog lists image input. Jev takes PNG and JPEG images, and image input is experimental on TypeSafe's own API. Clef takes PNG, JPEG, and WebP images, at most four per request. Jev from `vercel-ai-gateway` and `opencode` takes text only.\n\nEach System One service takes images in its own request format, set by the model's `compat.imageInput`. Built-in models set it, so overriding a provider's `baseUrl` keeps it. A custom provider that proxies another service's classifiers sets it in its provider-level `compat` in `models.json` (see [System One compatibility](models-configuration.md#typesafe-system-one)). OpenRouter in particular answers requests in TypeSafe's format without an error but drops the images.\n\nWhen the service reports token counts, as all System One services do, `result.usage` carries them with their cost. Pi adds the usage of a script's classifier calls to the `codemode` tool result, so it counts toward the session cost in the footer and `/session`. The cost uses the model's catalog price; models without one, such as TypeSafe's direct `jev-latest`, report tokens at no cost.\n\nExtensions call classifiers through `ctx.modelRegistry.classify()`, without codemode. [Virtual models](virtual-models.md#route-requests) can use them to route requests; see the `jev-router.ts` example.\n\n## Use image models\n\nImage models generate images from a prompt and optional input images. Pi lists OpenRouter's image models, such as `google/gemini-2.5-flash-image` and `black-forest-labs/flux.2-pro`, under the `openrouter` provider; they use the same `OPENROUTER_API_KEY` or `/login` credential as its chat models.\n\nLike classifier models, image models do not appear in `/model`; the model reaches them through the [`codemode`](cli.md#enable-codemode) tool. Scripts list them with `models.getAvailableOfType(\"image\")` and call `models.generateImages(model, { input })`. The result's `output` holds base64 image blocks, which `image()` attaches to the `codemode` result so the model sees them:\n\n```js\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) if (block.type === \"image\") image(block);\n```\n\n`input` can also contain `{ type: \"image\", data, mimeType }` blocks to edit or use as references. Pi adds the usage of a script's image calls to the `codemode` tool result, like classifier calls. Generated images are not saved to disk. [Codemode](codemode.md#generate-images) describes the full API.\n\nExtensions generate images through `ctx.modelRegistry.generateImages()`, without codemode.\n\n## Add a custom provider\n\nUse an extension when the provider needs custom streaming, model discovery, or authentication behavior. See [Custom Providers](custom-provider.md) for the extension workflow.\n\n## Troubleshooting\n\n### A model does not appear\n\nConfirm that its provider has usable authentication. Custom models can load from `models.json` but remain unavailable in `/model` until Pi can resolve credentials. For llama.cpp, only models currently loaded by the router appear.\n\n### Authentication works in one shell only\n\nCheck whether the key came from an environment variable rather than `auth.json`. Environment variables must be present in the process that starts Pi.\n\n### Sign-in opens a browser on a remote machine\n\nComplete the provider's headless authentication flow when available. Some providers let you paste the final redirect URL or authorization code back into Pi. See [Authenticate interactively](providers.md#authenticate-interactively).\n\n### A compatible endpoint rejects requests\n\nCheck its API type and compatibility settings in `models.json`. The upstream server must support the corresponding request fields and behavior.\n","packages/coding-agent/docs/nvidia-rtx-llama-cpp.md":"# Run Pi with NVIDIA RTX and llama.cpp\n\n[NVIDIA Local AI Inference Recipes](https://nvidia.github.io/local-ai-inference-recipes/index.html) provides llama.cpp settings tuned for specific NVIDIA hardware and models. Each recipe gives a single-model `llama-server` command. Pi works with the llama.cpp router, which can serve and manage several models, so this guide converts the recipe command into a router preset.\n\nThe examples below use Windows PowerShell. See [Local Models with llama.cpp](llama-cpp.md) for Pi's complete router and model-management reference.\n\n## Before you begin\n\n1. Open the [recipe catalog](https://nvidia.github.io/local-ai-inference-recipes/index.html), select your hardware, operating system, and model, then choose **llama.cpp**. Check the prerequisites and memory requirements. Keep the recipe's launch command for the conversion below.\n2. Install llama.cpp with the recipe's setup instructions. The recipe's settings depend on its build, for example CUDA support or speculative decoding options.\n3. Run `llama-server --help` and check that it lists `--models-preset`. Without it, the build cannot load the recipe as a router preset. Use a newer build that still meets the recipe's requirements.\n4. Install Pi with the [Quickstart](quickstart.md). On native Windows, also see [Run Pi on Windows](windows.md).\n\n## Convert the recipe to a preset\n\nA preset lists the settings for one or more models. Each section name becomes the model ID shown in Pi. Save the preset next to `llama-server`, or pass its full path when starting the router.\n\nConvert each option from the recipe command into one preset line:\n\n- Remove the leading dashes: `--ctx-size 65536` becomes `ctx-size = 65536`.\n- Write options without a value as `true`: `--jinja` becomes `jinja = true`.\n- Remove quotes and PowerShell line-continuation backticks around values.\n- Leave out `--host` and `--port`. They belong on the router command line.\n- Add `load-on-startup = true` so the router loads the model when it starts.\n- Replace short model-file options with their long preset keys:\n\n| Recipe option | Preset key |\n|---|---|\n| `-hf` | `hf-repo` |\n| `-hff` | `hf-file` |\n| `-hfrd` | `hf-repo-draft` |\n| `--spec-draft-model` | `spec-draft-model` |\n\nFor example, this recipe command:\n\n```powershell\nllama-server.exe -hf \"unsloth/Qwen3.8-27B-GGUF\" `\n  -hff \"Qwen3.8-27B-UD-Q4_K_M.gguf\" `\n  -hfrd \"z-lab/Qwen3.8-27B-DFlash2-GGUF\" `\n  --spec-draft-model \"Qwen3.8-27B-DFlash2-Q4_K_M.gguf\" `\n  --no-mmproj `\n  --host 127.0.0.1 `\n  --port 9931 `\n  --batch-size 4096 `\n  --ubatch-size 1024 `\n  --ctx-size 65536 `\n  --parallel 1 `\n  -ngl 999 `\n  --jinja `\n  --chat-template-kwargs '{\"preserve_thinking\":true}' `\n  --spec-type draft-dflash `\n  --spec-draft-n-max 7 `\n  --spec-draft-backend-sampling\n```\n\nbecomes `qwen3.8-27b.ini`:\n\n```ini\nversion = 1\n\n[qwen3.8-27b]\nload-on-startup = true\nhf-repo = unsloth/Qwen3.8-27B-GGUF\nhf-file = Qwen3.8-27B-UD-Q4_K_M.gguf\nhf-repo-draft = z-lab/Qwen3.8-27B-DFlash2-GGUF\nspec-draft-model = Qwen3.8-27B-DFlash2-Q4_K_M.gguf\nno-mmproj = true\nbatch-size = 4096\nubatch-size = 1024\nctx-size = 65536\nparallel = 1\nngl = 999\njinja = true\nchat-template-kwargs = {\"preserve_thinking\":true}\nspec-type = draft-dflash\nspec-draft-n-max = 7\nspec-draft-backend-sampling = true\n```\n\n## Start the router\n\nOpen PowerShell in the llama.cpp folder and run:\n\n```powershell\n.\\llama-server.exe --models-preset qwen3.8-27b.ini --host 127.0.0.1 --port 8080\n```\n\nIf the recipe requires a Hugging Face token, set `$env:HF_TOKEN` in the same PowerShell window before starting the router. On its first start, the router downloads the model files.\n\nUse the [router health checks](llama-cpp.md#troubleshooting) to confirm that the server is ready. In Windows PowerShell, use `curl.exe` rather than `curl`, which PowerShell maps to a different command. The model is ready when `/models` lists `qwen3.8-27b` with the status `loaded`.\n\n## Use the model in Pi\n\nFollow [Configure Pi](llama-cpp.md#configure-pi) and connect to `http://127.0.0.1:8080`. The preset section name is the model ID, so select `qwen3.8-27b` in `/model` for this example.\n\nNVIDIA recipe memory settings assume that only the selected model uses the GPU. If `/llama` shows other loaded models, unload them before using the recipe model.\n","packages/coding-agent/docs/packages.md":"# Pi Packages\n\nPi packages install and distribute extensions, skills, prompt templates, and themes as one unit. Use a package when a customization should be shared through npm or git, or when several resources belong together.\n\nA package is an ordinary directory or npm package. It can expose conventional resource directories, declare explicit paths under the `pi` key in `package.json`, and carry its own runtime dependencies.\n\n## Install and manage packages\n\nInstall from npm, git, or a local path:\n\n```bash\npi install npm:@example/pi-tools@1.0.0\npi install git:github.com/example/pi-tools@v1\npi install ./local-package\n```\n\n`pi list` shows configured packages. Use `pi remove <source>` to remove one and `pi update --extensions` to reconcile package installations. See [Command Line](cli.md#package-commands) for every package command and option.\n\nPersonal installs are written to `~/.pi/agent/settings.json`. Add `--local` or `-l` to write the package declaration to `.pi/settings.json`. Pi reads declarations from that file only after project trust is granted.\n\nProject packages are installed and loaded only after project trust is resolved. Packages can execute extension code and can include skills that instruct the model to run programs. Review third-party package source before installing it. Review project package declarations before granting project trust.\n\nUse `--extension` or `-e` to try a package for one invocation without adding it to settings:\n\n```bash\npi -e npm:@example/pi-tools\n```\n\n## Choose a source\n\n| Source | Example | Behavior |\n|---|---|---|\n| npm | `npm:@example/pi-tools@1.0.0` | Installed under the Pi npm directory |\n| git | `git:github.com/example/pi-tools@v1` | Cloned and reconciled to the selected ref |\n| URL | `https://github.com/example/pi-tools` | Treated as a git source |\n| Local | `./pi-tools` | Loaded from the resolved path without copying |\n\nVersioned npm specifications are pinned. Git tags and commits are also pinned; package updates reconcile the checkout but do not move a configured ref.\n\nRelative local paths resolve from the settings file that contains them. A file path loads one extension. A directory follows normal package discovery rules.\n\n## Create a package\n\nThe simplest package uses conventional directories:\n\n```text\nmy-pi-package/\n├── package.json\n├── extensions/\n├── skills/\n├── prompts/\n└── themes/\n```\n\nWithout a `pi` manifest, Pi discovers TypeScript and JavaScript extensions, skill directories, Markdown prompts, and JSON themes from those directories.\n\nUse an explicit manifest when resources live elsewhere or need filtering:\n\n```json\n{\n  \"name\": \"my-pi-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./src/extension.ts\"],\n    \"skills\": [\"./resources/skills\"],\n    \"prompts\": [\"./resources/prompts/*.md\"],\n    \"themes\": [\"./resources/themes/*.json\"]\n  }\n}\n```\n\nPaths are relative to the package root. Arrays accept glob patterns and exclusions. List dot-prefixed or symlinked resource roots directly when traversal through a glob would not discover them.\n\nThe `pi-package` keyword makes an npm package eligible for discovery in the [Pi package gallery](https://pi.dev/packages). Optional `pi.image` and `pi.video` fields add gallery previews.\n\n## Declare dependencies\n\nPut runtime packages imported by extensions in `dependencies`. Pi installs package dependencies when it installs an npm or git source.\n\nPi supplies these packages to extensions and skills:\n\n- `@earendil-works/pi-ai`\n- `@earendil-works/pi-agent-core`\n- `@earendil-works/pi-coding-agent`\n- `@earendil-works/pi-tui`\n- `typebox`\n\nDeclare the host-provided packages listed above in `peerDependencies` with a `\"*\"` range and do not bundle them. Pi suppresses automatic peer installation for managed npm packages and git packages installed with npm, pnpm, or Bun. Local packages are not installed or modified, so their dependency tree remains the package author's responsibility.\n\nDo not list host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping in compiled ESM and create duplicate classes, registries, and initialization work. Pi reports an extension warning when it detects this manifest configuration. Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths.\n\nInstalled packages load with separate module roots. Do not rely on two packages sharing one dependency instance or one package resolving another package’s undeclared dependency.\n\n## Select package resources\n\nThe object form in settings narrows which resources load from a package:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"npm:@example/pi-tools\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"]\n    }\n  ]\n}\n```\n\nFor each resource type:\n\n- Omit the property to load everything allowed by the package.\n- Use `[]` to load none of that type.\n- Use `!pattern` to exclude glob matches.\n- Use `+path` to include one exact allowed path.\n- Use `-path` to exclude one exact path.\n\nFilters narrow the package manifest. They do not expose resources that the package itself did not declare.\n\nRun `pi config` to enable or disable discovered resources and pi's built-in extensions. It starts with personal configuration; press Tab to switch scope, or run `pi config --local` to start with project overrides.\n\n## Understand scope and identity\n\nThe same package can appear in personal and project settings. A project entry normally replaces the personal entry. With `autoload: false`, the project entry instead acts as a filtering delta over the personal package.\n\nPi identifies npm packages by package name, git packages by repository URL without the ref, and local packages by resolved absolute path. This prevents the same package from loading twice through equivalent declarations.\n\nUse [Extensions](extensions.md), [Skills](skills.md), [Prompt Templates](prompt-templates.md), and [Themes](themes.md) to design each resource before packaging it.\n","packages/coding-agent/docs/prompt-templates.md":"# Prompt Templates\n\nPrompt templates turn Markdown files into reusable `/` commands. Use one when you want to reuse the same prompt without adding executable behavior or a larger set of supporting instructions.\n\nA template can accept arguments and appear in command completion. Pi can load templates from personal configuration, project configuration, an explicit path, or a Pi package. Project configuration loads only after project trust is granted.\n\n## Create a template\n\nCreate `~/.pi/agent/prompts/review.md`:\n\n```markdown\n---\ndescription: Review staged git changes\nargument-hint: \"[focus]\"\n---\nReview the staged changes. Focus on ${1:-correctness, security, and error handling}.\n```\n\nThe filename becomes the command name, so this template is available as `/review`. The `description` appears in command completion. If it is omitted, Pi uses the first non-empty line.\n\n`argument-hint` is optional. Use `<angle brackets>` for required arguments and `[square brackets]` for optional arguments.\n\nRun `/reload` after adding or changing a template in an active session.\n\n<a id=\"invoke-a-template\"></a>\n\n## Use a template\n\nType the template command in the editor:\n\n```text\n/review\n/review concurrency\n```\n\nPi expands the template before the resulting text enters the agent. Extensions receive the raw input first through the `input` event unless an extension command with the same name handles it.\n\nTemplates support these substitutions:\n\n| Syntax | Result |\n|---|---|\n| `$1`, `$2`, … | One positional argument |\n| `$@` or `$ARGUMENTS` | All arguments joined with spaces |\n| `${1:-default}` | First argument, or a default value |\n| `${@:-default}` | All arguments, or a default value |\n| `${@:N}` | Arguments starting at position `N` |\n| `${@:N:L}` | `L` arguments starting at position `N` |\n\nArguments follow shell-like quoting, so `/review \"API compatibility\"` supplies one argument containing a space.\n\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the template in your user or project prompt directory. Conventional prompt directories load direct `.md` children only.\n\nSettings and packages can select nested Markdown files; a package manifest can narrow discovery with explicit paths and globs. See [Settings](settings.md#resources) and [Pi Packages](packages.md) for these options.\n\nProject templates become commands in the editor after trust is granted. Review their content before trusting an unfamiliar project. See [Security](security.md#understand-project-trust).\n","packages/coding-agent/docs/providers.md":"# Providers\n\nMost hosted providers support one or both of these authentication methods:\n\n- Sign in through a browser or device flow backed by OAuth.\n- Provide an API key.\n\nUse `/login [provider]` to see the methods supported by a provider. Amazon Bedrock and Google Vertex AI can also use ambient cloud credentials.\n\n## Authenticate interactively\n\nRun `/login` and select a provider. Pi guides you through its OAuth or API-key flow and saves the resulting credential in [`auth.json`](configuration.md#agent-directory).\n\nOn a remote or headless machine, an OAuth callback may not reach the local process. When prompted, paste the final redirect URL or authorization code back into Pi.\n\nRun `/logout` and select a provider to remove its stored credential. This does not unset environment variables, remove authentication from `models.json`, or revoke the credential at the provider.\n\n`auth.json` can contain API keys and OAuth tokens. Keep it private and do not commit it.\n\n## Use an API key from the environment\n\nEnvironment variables are useful in CI and anywhere Pi should not store the key. Set the variable before starting Pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nThis table covers providers with a single primary API-key variable. Providers that need additional configuration or support ambient credentials are covered under [Provider Specific Config](#provider-specific-config).\n\n| Provider | Environment variable |\n|---|---|\n| Anthropic | `ANTHROPIC_API_KEY` |\n| Ant Ling | `ANT_LING_API_KEY` |\n| OpenAI | `OPENAI_API_KEY` |\n| DeepSeek | `DEEPSEEK_API_KEY` |\n| NVIDIA NIM | `NVIDIA_API_KEY` |\n| Google Gemini | `GEMINI_API_KEY` |\n| GitHub Copilot | `COPILOT_GITHUB_TOKEN` |\n| Mistral | `MISTRAL_API_KEY` |\n| Groq | `GROQ_API_KEY` |\n| Cerebras | `CEREBRAS_API_KEY` |\n| xAI | `XAI_API_KEY` |\n| OpenRouter | `OPENROUTER_API_KEY` |\n| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |\n| ZAI Coding Plan (Global) | `ZAI_API_KEY` |\n| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` |\n| OpenCode Zen and Go | `OPENCODE_API_KEY` |\n| Radius | `RADIUS_API_KEY` |\n| TypeSafe ([classifier models](models.md#use-classifier-models)) | `TYPESAFE_API_KEY` |\n| Hugging Face | `HF_TOKEN` |\n| Fireworks | `FIREWORKS_API_KEY` |\n| Together AI | `TOGETHER_API_KEY` |\n| Baseten | `BASETEN_API_KEY` |\n| Kimi For Coding | `KIMI_API_KEY` |\n| Meta | `META_API_KEY` |\n| MiniMax | `MINIMAX_API_KEY` |\n| MiniMax (China) | `MINIMAX_CN_API_KEY` |\n| Moonshot AI (Global and China) | `MOONSHOT_API_KEY` |\n| Qwen Token Plan and Individual | `QWEN_TOKEN_PLAN_API_KEY` |\n| Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |\n| Xiaomi MiMo | `XIAOMI_API_KEY` |\n| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |\n| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |\n| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` |\n\nAnthropic also recognizes `ANTHROPIC_OAUTH_TOKEN` as an API credential and `ANTHROPIC_AUTH_TOKEN` as bearer authentication.\n\nWith no key or token set, Anthropic uses workload identity federation when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` and `ANTHROPIC_IDENTITY_TOKEN_FILE` are set: the Anthropic SDK exchanges the identity token for a short-lived access token and refreshes it itself (re-reading the identity token file, so keep that file fresh for long sessions). `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID` are passed through when set.\n\n## Load an API key from a command\n\nTo use a secret manager without writing the resolved key to disk, set a provider's `key` in `auth.json` to a command prefixed with `!`:\n\n```json\n{\n  \"anthropic\": {\n    \"type\": \"api_key\",\n    \"key\": \"!security find-generic-password -ws 'anthropic'\"\n  }\n}\n```\n\nPi runs the command when the key is first needed and caches its standard output for the process lifetime. Empty output, a timeout, or a nonzero exit leaves the key unresolved until Pi restarts.\n\n## Provider Specific Config\n\nThe providers below have additional setup, need additional settings, or can use credentials supplied by their platform.\n\nA stored API-key credential can include an `env` object. Its values take priority over the process environment for that provider:\n\n```json\n{\n  \"cloudflare-workers-ai\": {\n    \"type\": \"api_key\",\n    \"key\": \"...\",\n    \"env\": {\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\"\n    }\n  }\n}\n```\n\n### Radius\n\nRadius is a service crafted for Pi by the builders of Pi, Earendil Works. It provides a customizable AI gateway with organization-level controls and analytics built in, and artifacts for sharing what you create with Pi.\n\nTo get started, run `/login radius` in Pi. This adds Radius as a provider, and its models appear in `/model` like any other provider's.\n\nRadius also has an MCP server, so Pi can manage Radius for you.\n\nRadius is currently in early alpha and evolving quickly. See [radius.earendil.com](https://radius.earendil.com) for more.\n\nRadius authentication uses its gateway catalog and caches refreshed model metadata for later offline startup. A custom Radius gateway configured in `models.json` uses its own catalog rather than inheriting the public `radius.pi.dev` catalog.\n\n### OpenRouter\n\nAuthenticate with `/login openrouter` or `OPENROUTER_API_KEY`.\n\nWith a key, Pi lists only the OpenRouter chat models that key may use, with the key's context size, max output, and prices. These come from OpenRouter's `/models/user` endpoint and reflect guardrails and provider restrictions set in OpenRouter. Image and classifier models are not filtered.\n\nPi refreshes the list in the background at startup in interactive mode, when `/model` opens, after `/login`, and on `pi update --models`. The last list is saved and used offline and in `pi -p` and RPC mode. After switching keys outside `/login`, the previous key's list stays until the next refresh.\n\nRegional endpoints filter models to their region. To use one, set the base URL in [`models.json`](models.md#configure-a-compatible-endpoint):\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": { \"baseUrl\": \"https://us.openrouter.ai/api/v1\" }\n  }\n}\n```\n\nIf the base URL rejects the key at `/models/user` or does not have that endpoint, for example a proxy configured as the OpenRouter base URL, Pi lists every OpenRouter model.\n\n### Azure OpenAI\n\nThe provider ID is `azure` (formerly `azure-openai-responses`). Use it as the key in `auth.json`, `models.json`, and `settings.json`, and in model references such as `--model azure/gpt-5.4`.\n\nThe `azure` provider serves OpenAI models through the Responses API and Microsoft Foundry models through Chat Completions, such as `azure/deepseek-v4-pro`.\n\nSet an API key plus either a base URL or resource name:\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# Or:\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n```\n\nResource root URLs under `ai.azure.com`, `cognitiveservices.azure.com`, and `openai.azure.com` are normalized to the OpenAI API path.\n\nPi sends the model ID as the deployment name. If a deployment has a different name, map it with `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`:\n\n```bash\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-5.4=my-gpt-deployment,deepseek-v4-pro=my-deepseek\n```\n\n`AZURE_OPENAI_API_VERSION` overrides the API version for OpenAI models (default `v1`).\n\nTo use a Foundry model that Pi does not include, add it under `azure` in [`models.json`](models.md#configure-a-compatible-endpoint) with `api: \"openai-completions\"`. Custom models require a `baseUrl`; `AZURE_OPENAI_BASE_URL` and `AZURE_OPENAI_RESOURCE_NAME` take priority over it when set:\n\n```json\n{\n  \"providers\": {\n    \"azure\": {\n      \"baseUrl\": \"https://your-resource.services.ai.azure.com\",\n      \"models\": [\n        { \"id\": \"your-deployment\", \"api\": \"openai-completions\" }\n      ]\n    }\n  }\n}\n```\n\n### Amazon Bedrock\n\nBedrock can use a bearer token or an ambient AWS credential source:\n\n```bash\n# Named profile\nexport AWS_PROFILE=your-profile\n\n# IAM keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n# Required for temporary credentials\nexport AWS_SESSION_TOKEN=...\n\n# Bedrock bearer token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Region, when not supplied by the profile or AWS SDK configuration\nexport AWS_REGION=us-west-2\n# AWS_DEFAULT_REGION is also supported\n```\n\nPi also supports ECS task credentials and IRSA through the standard `AWS_CONTAINER_CREDENTIALS_*` and `AWS_WEB_IDENTITY_TOKEN_FILE` variables.\n\n### Cloudflare AI Gateway\n\nThe gateway requires a token, account ID, and gateway ID:\n\n```bash\nexport CLOUDFLARE_API_KEY=...\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...\n```\n\nThe account and gateway IDs can come from the process environment or the credential's `env` object in `auth.json`.\n\n`CLOUDFLARE_API_KEY` authenticates Pi to the gateway. Upstream access can use Cloudflare unified billing, credentials stored in the gateway, or an `Authorization` header configured for the provider in `models.json`.\n\n### Cloudflare Workers AI\n\nWorkers AI requires a token and account ID:\n\n```bash\nexport CLOUDFLARE_API_KEY=...\nexport CLOUDFLARE_ACCOUNT_ID=...\n```\n\nThe account ID can also be stored in the credential's `env` object.\n\n### Google Vertex AI\n\nUse a Google Cloud API key:\n\n```bash\nexport GOOGLE_CLOUD_API_KEY=...\n```\n\nTo use Application Default Credentials, configure a project and location:\n\n```bash\nexport GOOGLE_CLOUD_PROJECT=your-project\n# GCLOUD_PROJECT is also supported\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nThen authenticate:\n\n```bash\ngcloud auth application-default login\n```\n\nTo use a service-account key file instead, set `GOOGLE_APPLICATION_CREDENTIALS` along with the project and location.\n","packages/coding-agent/docs/quickstart.md":"# Quickstart\n\nPi runs in your terminal and works with files on your machine. To use it, you need access to a model through a supported provider. This can be a subscription, an API key, or a local model.\n\nFor native Windows setup, read [Windows Setup](windows.md). For Android, read [Termux Setup](termux.md).\n\n## 1. Install Pi\n\nOn macOS or Linux, you can use the installer:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nThe installer pins all dependencies and updates Pi with `pi update`. Alternatively, install Pi from npm, which does not pin transitive dependencies. This requires Node.js 22.19 or newer:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\nPi does not require dependency lifecycle scripts for a normal npm installation.\n\nWith Nix on macOS or Linux, install the latest release from Pi's flake. Nix builds Pi from source:\n\n```bash\nnix profile add github:earendil-works/pi/stable\n```\n\nOlder Nix versions use `nix profile install` instead. Update with `nix profile upgrade pi`; `pi update` cannot update a Nix installation. To pin a release, use a tag such as `github:earendil-works/pi/v1.0.0`.\n\nVerify the installation:\n\n```bash\npi --version\n```\n\n## 2. Start Pi\n\nChange to the folder you want Pi to work with, then start it:\n\n```bash\ncd /path/to/folder\npi\n```\n\nThe working folder helps Pi discover relevant files, instructions, and configuration. Pi also uses it to group saved sessions.\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Pi running in a terminal with a conversation, input editor, and status footer\" width=\"750\"></p>\n\nThe interface shows your conversation, an editor for prompts and commands, and a footer with the current folder, model, and session status. See [Use Pi in the terminal](usage.md) to learn how to add files, run commands, direct ongoing work, and manage results.\n\n## 3. Choose a model\n\nA **model** generates Pi's responses. A **provider** is the service or account Pi uses to access that model.\n\nIn Pi, run:\n\n```text\n/login\n```\n\nChoose a provider, then follow the prompts to use a subscription or store an API key. Run `/model` afterward if you want to select a different available model.\n\nSee [Choose a model and provider](models.md) for supported providers, environment-variable authentication, local models, and custom endpoints.\n\n## 4. Give Pi a task\n\nPi shows each file read, search, command, and edit it performs. It does not ask before every tool call.\n\nEnter a task that matches your work, for example:\n\n```text\nSummarize @meeting-notes.md and save the action items to action-items.md.\n```\n\n```text\nExplain how this repository is structured and how to run its checks.\n```\n\n```text\nCompare @previous.csv with @current.csv and summarize the important changes.\n```\n\nType `@` in the editor to search for a file instead of entering its full path. When Pi finishes, review its response and any changed files. Use version control or backups for important work. For untrusted or unattended work, use a container or another sandbox. See [Security](security.md).\n\n## Continue later\n\nPi saves sessions automatically. Exit Pi, then resume the most recent session for the same working folder with:\n\n```bash\npi --continue\n```\n\nUse `/resume` to choose another saved session. See [Continue or branch a session](sessions.md) for session naming, branching, compaction, export, and sharing.\n\n## Next steps\n\n- [Use Pi interactively](usage.md) to learn input, commands, shortcuts, and queued messages.\n- [Add instructions](configuration.md#context-files) that Pi should follow whenever it works in a folder.\n- [Choose a model and provider](models.md).\n\n### Choose how to customize Pi\n\nStart with the least powerful mechanism that meets your need:\n\n| Need | Start with |\n|---|---|\n| Give Pi persistent instructions for a folder | [`AGENTS.md`](configuration.md#context-files) |\n| Reuse a prompt from the `/` menu | [Prompt template](prompt-templates.md) |\n| Add task-specific instructions and supporting files | [Skill](skills.md) |\n| Add executable tools, commands, or event handlers | [Extension](extensions.md) |\n| Build a custom terminal component | [Terminal UI](tui.md) |\n| Connect an unsupported model service | [Custom provider](custom-provider.md) |\n| Install or distribute several resources | [Pi package](packages.md) |\n\n## Uninstall Pi\n\nIf you installed Pi with npm, run:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nIf you used the installer, run it again and choose **Uninstall Pi**:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nIf you installed Pi with Nix, run:\n\n```bash\nnix profile remove pi\n```\n\nNone of these methods removes configuration, credentials, sessions, or installed Pi packages from `~/.pi/agent/`.\n","packages/coding-agent/docs/rpc-commands.md":"# RPC Commands\n\nThis reference lists commands accepted on stdin in [RPC mode](rpc.md). Each command and response is one JSON object. Shared message values use the [message types](message-types.md).\n\n## Prompting\n\n### prompt\n\nSend a user prompt to the agent. The command response is emitted after the prompt is accepted, queued, or handled. Events continue streaming asynchronously after acceptance.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nWith images:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**During streaming**: If the agent is already streaming, you must specify `streamingBehavior` to queue the message:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: Queue the message while the agent is running. It is delivered after the current assistant turn finishes executing its tool calls, before the next LLM call.\n- `\"followUp\"`: Wait until the agent finishes. Message is delivered only when agent stops.\n\nIf the agent is streaming and no `streamingBehavior` is specified, the command returns an error.\n\n**Extension commands**: If the message is an extension command (e.g., `/mycommand`), it executes immediately even during streaming. Extension commands manage their own LLM interaction via `pi.sendMessage()`.\n\n**Input expansion**: Skill commands (`/skill:name`) and prompt templates (`/template`) are expanded before sending/queueing.\n\nResponse:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true, \"data\": {\"disposition\": \"started\"}}\n```\n\n`data.disposition` is `\"handled\"` if an extension command or input handler consumed the prompt, `\"queued\"` if Pi queued it during a run, or `\"started\"` if Pi accepted it to start a run. This describes the submitted prompt, not independent work started by an extension or a guarantee of completion.\n\n`success: true` means the prompt was accepted, queued, or handled immediately. `success: false` means the prompt was rejected before acceptance. Failures after acceptance are reported through the normal event and message stream, not as a second `response` for the same request id.\n\nThe `images` field is optional. Each image uses `ImageContent` format: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n### steer\n\nQueue a steering message while the agent is running. It is delivered after the current assistant turn finishes executing its tool calls, before the next LLM call. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nWith images:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nThe `images` field is optional. Each image uses `ImageContent` format (same as `prompt`).\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true, \"data\": {\"disposition\": \"queued\"}}\n```\n\n`data.disposition` is `\"handled\"` if an input handler consumed this steer, or `\"queued\"` if Pi queued it (including after a handler transformed it). It does not guarantee this message remains queued.\n\nSee [set_steering_mode](#set_steering_mode) for controlling how steering messages are processed.\n\n### follow_up\n\nQueue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nWith images:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nThe `images` field is optional. Each image uses `ImageContent` format (same as `prompt`).\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true, \"data\": {\"disposition\": \"queued\"}}\n```\n\n`data.disposition` has the same `\"handled\"` or `\"queued\"` meaning as for `steer`, applied to this follow-up.\n\nSee [set_follow_up_mode](#set_follow_up_mode) for controlling how follow-up messages are processed.\n\n### abort\n\nAbort the current operation and wait for the session to become idle before responding.\n\n```json\n{\"type\": \"abort\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n### clear_queue\n\nRemove queued steering and follow-up messages and return their text.\n\n```json\n{\"type\": \"clear_queue\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clear_queue\",\n  \"success\": true,\n  \"data\": {\n    \"steering\": [\"Change direction\"],\n    \"followUp\": [\"Summarize when finished\"]\n  }\n}\n```\n\nTo implement interactive Esc behavior, send `clear_queue` before `abort`, then restore the returned text in the client editor. `abort` continues queued messages when they remain in the session.\n\n### new_session\n\nStart a fresh session. Can be canceled by a `session_before_switch` extension event handler.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nWith optional parent session tracking:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nIf an extension canceled:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n## State\n\n### get_state\n\nGet current session state.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\nThe `model` field is a full [Model](#model-object) object, or omitted when no model is selected. The `sessionName` field is the display name set via `set_session_name`, or omitted if not set.\n\n### get_messages\n\nGet all messages in the conversation.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nMessages are `AgentMessage` objects (see [Message Types](message-types.md)).\n\n## Model\n\n### set_model\n\nSwitch to a specific model.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nResponse contains the full [Model](#model-object) object:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n### cycle_model\n\nCycle to the next available model. Returns `null` data if only one model available.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\nThe `model` field is a full [Model](#model-object) object.\n\n### get_available_models\n\nList all configured models.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nResponse contains an array of full [Model](#model-object) objects:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n## Thinking\n\n### set_thinking_level\n\nSet the reasoning/thinking level for models that support it.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nLevels: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` and `\"max\"` are exposed only when supported by the selected model. Some models, including GPT-5.6, expose both.\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n### cycle_thinking_level\n\nCycle through available thinking levels. Returns `null` data if model doesn't support thinking.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n### get_available_thinking_levels\n\nList the thinking levels supported by the current model. Returns `[\"off\"]` for a model without reasoning support.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n## Queue modes\n\n### set_steering_mode\n\nControl how steering messages (from `steer`) are delivered.\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: Deliver all steering messages after the current assistant turn finishes executing its tool calls\n- `\"one-at-a-time\"`: Deliver one steering message per completed assistant turn (default)\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n### set_follow_up_mode\n\nControl how follow-up messages (from `follow_up`) are delivered.\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: Deliver all follow-up messages when agent finishes\n- `\"one-at-a-time\"`: Deliver one follow-up message per agent completion (default)\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n## Compaction\n\n### compact\n\nManually compact conversation context to reduce token usage.\n\n```json\n{\"type\": \"compact\"}\n```\n\nWith custom instructions:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter` is a heuristic estimate over the rebuilt message context immediately after compaction, not a provider-exact token count. `usage` reports the LLM call or calls that generated the summary and may be omitted by custom compaction handlers.\n\n### set_auto_compaction\n\nEnable or disable automatic compaction when context is nearly full.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n## Retry\n\n### set_auto_retry\n\nEnable or disable automatic retry on transient errors (overloaded, rate limit, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n### abort_retry\n\nAbort an in-progress retry (cancel the delay and stop retrying).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n## Bash\n\n### bash\n\nExecute a shell command and add output to conversation context. Output streams as `bash_execution_update` events while the command runs; the response contains the final result.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nSet `excludeFromContext` to `true` when the command output should be stored in the session but omitted from the model context on the next prompt.\n\nInclude an `id` to associate streamed `bash_execution_update` events with this command.\n\nResponse:\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\nIf output was truncated, includes `fullOutputPath`:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**How bash results reach the LLM:**\n\nThe `bash` command executes immediately and returns a `BashResult`. Internally, a `BashExecutionMessage` is created and stored in the agent's message state.\n\nWhen the next `prompt` command is sent, Pi transforms context messages before sending them to the model. Unless `excludeFromContext` is true, the `BashExecutionMessage` becomes a `UserMessage` with this format:\n\n````\nRan `ls -la`\n```\ntotal 48\ndrwxr-xr-x ...\n```\n````\n\nThis means:\n1. Included bash output reaches the model on the **next prompt**, not immediately.\n2. Multiple bash commands can run before a prompt; Pi includes each output that does not set `excludeFromContext`.\n\n### abort_bash\n\nAbort a running bash command.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n## Session\n\n### get_session_stats\n\nGet token usage, cost statistics, and current context window usage.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens` and `cost` include assistant messages, usage reported by tools, and compaction/branch-summary generation across the full session. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.\n\n`contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.\n\n### export_html\n\nExport session to an HTML file.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nWith custom path:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n### switch_session\n\nLoad a different session file. Can be canceled by a `session_before_switch` extension event handler.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nIf an extension canceled the switch:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### fork\n\nCreate a new fork from a previous user message on the active branch. Can be canceled by a `session_before_fork` extension event handler. Returns the text of the message being forked from.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nIf an extension canceled the fork:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n### clone\n\nDuplicate the current active branch into a new session at the current position. Can be canceled by a `session_before_fork` extension event handler.\n\n```json\n{\"type\": \"clone\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nIf an extension canceled the clone:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n### get_fork_messages\n\nGet user messages available for forking.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n### get_entries\n\nGet all session entries in append order (excluding the session header). The session is an append-only tree of entries with stable ids, so an entry id works as a durable cursor: pass the last entry id you have seen as `since` to get only entries strictly after it, even across client restarts. Unlike `get_messages`, this includes pre-compaction history and abandoned branches.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nWith a cursor:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId` is the id of the current leaf entry (`null` for an empty session), so a client can tell in one round trip whether the active branch moved. If `since` does not match any entry id, the response is `success: false`.\n\n### get_tree\n\nGet the session as a tree of entries. Each node is `{entry, children, label?, labelTimestamp?}`. The result is an array because navigation APIs can create multiple roots; orphaned entries with broken parent chains also appear as roots.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n### get_last_assistant_text\n\nGet the text content of the last assistant message.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\nThe `text` value is `null` if no assistant text exists.\n\n### set_session_name\n\nSet a display name for the current session. The name appears in session listings and helps identify sessions.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nThe current session name is available via `get_state` in the `sessionName` field. To set the initial name when starting RPC mode, pass `--name <name>` or `-n <name>` to the `pi --mode rpc` process.\n\n## Discoverable commands\n\n### get_commands\n\nGet available commands (extension commands, prompt templates, and skills). Run one through the `prompt` command by prefixing its name with `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\n        \"name\": \"fix-tests\",\n        \"description\": \"Fix failing tests\",\n        \"source\": \"prompt\",\n        \"sourceInfo\": {\n          \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\",\n          \"source\": \"local\",\n          \"scope\": \"project\",\n          \"origin\": \"top-level\"\n        }\n      }\n    ]\n  }\n}\n```\n\nEach command has:\n- `name`: Command name (use `/name`)\n- `description`: Human-readable description (optional for extension commands)\n- `source`: What kind of command:\n  - `\"extension\"`: Registered via `pi.registerCommand()` in an extension\n  - `\"prompt\"`: Loaded from a prompt template `.md` file\n  - `\"skill\"`: Loaded from a skill directory (name is prefixed with `skill:`)\n- `sourceInfo`: Metadata for the resource that registered the command:\n  - `path`: Absolute path to the resource\n  - `source`: How Pi discovered it, such as `\"local\"`, `\"auto\"`, or `\"cli\"`\n  - `scope`: `\"user\"`, `\"project\"`, or `\"temporary\"`\n  - `origin`: `\"top-level\"` for a directly loaded resource or `\"package\"` for a package resource\n  - `baseDir`: Package base directory, when applicable\n\n**Note**: Built-in TUI commands (`/settings`, `/hotkeys`, etc.) are not included. They are handled only in interactive mode and would not execute if sent via `prompt`.\n\n## Model object\n\nModel commands return the complete configured model definition. Costs are in US dollars per million tokens.\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\nFor model configuration, see [Configure a compatible endpoint](models.md#configure-a-compatible-endpoint). For TypeScript, use the exported `Model` type from `@earendil-works/pi-ai`.\n","packages/coding-agent/docs/rpc-extension-ui.md":"# RPC Extension UI\n\nExtensions can request user interaction through `ctx.ui`. In RPC mode, supported calls become a request/response subprotocol alongside normal [RPC commands](rpc-commands.md) and [session events](json.md).\n\nThere are two categories of extension UI methods:\n\n- **Dialog methods** (`select`, `confirm`, `input`, `editor`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.\n- **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): emit an `extension_ui_request` on stdout but do not expect a response. The client can display the information or ignore it.\n\nIf a dialog method includes a `timeout` field, the agent-side will auto-resolve with a default value when the timeout expires. The client does not need to track timeouts.\n\n## Limitations\n\nSome `ExtensionUIContext` methods are not supported or degraded in RPC mode because they require direct terminal UI access:\n\n- `custom()` returns `undefined`.\n- `onTerminalInput()` returns a no-op unsubscribe function.\n- `setWorkingMessage()`, `setWorkingVisible()`, `setWorkingIndicator()`, `setHiddenThinkingLabel()`, `setFooter()`, `setHeader()`, `addAutocompleteProvider()`, `setEditorComponent()`, and `setToolsExpanded()` are no-ops.\n- `getEditorText()` returns `\"\"` and `getEditorComponent()` returns `undefined`.\n- `getToolsExpanded()` returns `false`.\n- `pasteToEditor()` delegates to `setEditorText()` without terminal paste handling.\n- `getAllThemes()` returns `[]`, and `getTheme()` returns `undefined`.\n- `setTheme()` returns `{ success: false, error: \"Theme switching not supported in RPC mode\" }`.\n\nNote: `ctx.mode` is `\"rpc\"` and `ctx.hasUI` is `true` in RPC mode because the dialog and fire-and-forget methods are functional via the extension UI sub-protocol. Use `ctx.mode === \"tui\"` to guard TUI-specific features like `custom()` that require a real terminal.\n\n## Requests from Pi\n\nAll requests have `type: \"extension_ui_request\"`, a unique `id`, and a `method` field.\n\n### select\n\nPrompt the user to choose from a list. Dialog methods with a `timeout` field include the timeout in milliseconds; the agent auto-resolves with `undefined` if the client doesn't respond in time.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the selected option string) or `cancelled: true`.\n\n### confirm\n\nPrompt the user for yes/no confirmation.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\nExpected response: `extension_ui_response` with `confirmed: true/false` or `cancelled: true`.\n\n### input\n\nPrompt the user for free-form text.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the entered text) or `cancelled: true`.\n\n### editor\n\nOpen a multi-line text editor with optional prefilled content.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the edited text) or `cancelled: true`.\n\n### notify\n\nDisplay a notification. Fire-and-forget, no response expected.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\nThe `notifyType` field is `\"info\"`, `\"warning\"`, or `\"error\"`. Defaults to `\"info\"` if omitted.\n\n### setStatus\n\nSet or clear a status entry in the footer/status bar. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\nSend `statusText: undefined` (or omit it) to clear the status entry for that key.\n\n### setWidget\n\nSet or clear a widget (block of text lines) displayed above or below the editor. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\nSend `widgetLines: undefined` (or omit it) to clear the widget. The `widgetPlacement` field is `\"aboveEditor\"` (default) or `\"belowEditor\"`. Only string arrays are supported in RPC mode; component factories are ignored.\n\n### setTitle\n\nSet the terminal window/tab title. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n### set_editor_text\n\nSet the text in the input editor. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n## Responses to Pi\n\nResponses are sent for dialog methods only (`select`, `confirm`, `input`, `editor`). The `id` must match the request.\n\n### Value response (select, input, editor)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n### Confirmation response (confirm)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n### Cancellation response (any dialog)\n\nDismiss any dialog method. The extension receives `undefined` (for select/input/editor) or `false` (for confirm).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Example\n\nSee the checked [RPC extension UI client](../examples/rpc-extension-ui.ts) and its [demo extension](../examples/extensions/rpc-demo.ts).\n\nThe exported request and response unions are defined in [`rpc-types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-types.ts). See [Extensions](extensions.md#ui-and-modes) for mode-independent extension guidance.\n","packages/coding-agent/docs/rpc.md":"# RPC Mode\n\nRPC mode runs Pi as a long-lived subprocess controlled through JSON records on stdin and stdout. Use it for language-independent integrations, process isolation, IDEs, and custom user interfaces.\n\nFor an in-process Node.js or Bun integration, prefer the [SDK](sdk.md). For a subprocess-based TypeScript integration, prefer the exported `RpcClient`, which starts Pi, correlates responses, exposes typed command methods, and delivers events to listeners.\n\n| Interface | Process boundary | Control model | Best fit |\n|---|---|---|---|\n| [SDK](sdk.md) | In process | Direct TypeScript methods and events | Node.js or Bun hosts that want complete API access |\n| RPC | Child process | JSONL commands, responses, and events | Other languages, isolated processes, IDEs, or custom clients |\n\n## Start RPC mode\n\n```bash\npi --mode rpc --no-session\n```\n\nNormal CLI options still select the working folder, model, tools, resources, and session behavior. Common choices include `--provider`, `--model`, `--name`, `--no-session`, and `--session-dir`. See [Command Line](cli.md) for the complete, version-specific interface; `pi --help` is authoritative for the installed version.\n\nRPC mode rejects `@file` prompt arguments. Send prompts through the [`prompt`](rpc-commands.md#prompt) command instead.\n\n## Protocol records\n\nThe protocol has four record families:\n\n| Direction | Record | Purpose |\n|---|---|---|\n| stdin | Command | Ask Pi to prompt, inspect state, change configuration, or manage the session |\n| stdout | `response` | Report whether one command succeeded and return any command data |\n| stdout | Session event | Stream run, message, tool, queue, compaction, and retry activity |\n| Both | Extension UI record | Forward supported extension interactions between Pi and the client |\n\nSee [RPC Commands](rpc-commands.md), [JSON Event Stream](json.md), and [RPC Extension UI](rpc-extension-ui.md) for the canonical record definitions.\n\n### Correlate commands and responses\n\nEvery command accepts an optional string `id`. A matching response repeats it:\n\n```json\n{\"id\":\"req-1\",\"type\":\"get_state\"}\n{\"id\":\"req-1\",\"type\":\"response\",\"command\":\"get_state\",\"success\":true,\"data\":{\"...\":\"...\"}}\n```\n\nUse unique IDs whenever more than one command can be outstanding. Command handling is asynchronous, so clients should correlate by ID rather than response order.\n\nSession events generally have no command ID because they describe session activity. `bash_execution_update` is the exception: when the originating [`bash`](rpc-commands.md#bash) command has an ID, its output events repeat that ID.\n\nAn `extension_ui_response` uses the ID supplied by its `extension_ui_request`. It does not produce a normal command response.\n\n## Framing\n\nRPC uses strict JSONL framing. Write one complete JSON object per record and terminate it with LF (`\\n`). Read stdout as a byte or UTF-8 stream and split records only on LF. Strip an optional preceding carriage return to accept CRLF input.\n\nDo not use a generic line reader that treats Unicode line or paragraph separators as record boundaries. In particular, Node.js `readline` also splits on `U+2028` and `U+2029`, which are valid inside JSON strings.\n\nRead stdout continuously. Pi honors stdout backpressure, but a client that stops reading can stall the process. Honor stdin backpressure when writing commands. Stdout is reserved for protocol records; diagnostics and application logging go to stderr.\n\n## Run lifecycle\n\nA successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean model work completed:\n\n```json\n{\"id\":\"req-2\",\"type\":\"prompt\",\"message\":\"Review this repository\"}\n{\"id\":\"req-2\",\"type\":\"response\",\"command\":\"prompt\",\"success\":true,\"data\":{\"disposition\":\"started\"}}\n```\n\n`data.disposition` reports what happened to the prompt. If it is `\"handled\"`, no run started for this prompt, so don't wait for `agent_settled`. See [RPC Commands](rpc-commands.md#prompt) for all values.\n\nContinue consuming [events](json.md) after that response. `agent_end` marks the end of one low-level agent run, but retries, overflow recovery, compaction, steering, or follow-up work can still follow. Wait for `agent_settled` when the client needs to know Pi will not continue automatically.\n\nSubscribe before sending a prompt to avoid missing a fast completion. `RpcClient.promptAndWait()` does this internally. If using separate `RpcClient` calls, install the event listener before `prompt()` and call `waitForIdle()` only while a run is active.\n\n## Errors\n\nA failed command returns one response with `success: false`:\n\n```json\n{\"id\":\"req-3\",\"type\":\"response\",\"command\":\"set_model\",\"success\":false,\"error\":\"Model not found: invalid/model\"}\n```\n\nMalformed JSON produces a parse response without a request ID:\n\n```json\n{\"type\":\"response\",\"command\":\"parse\",\"success\":false,\"error\":\"Failed to parse command: Unexpected token...\"}\n```\n\nA success response only covers command handling. Provider failures and aborts after a prompt is accepted appear in the message and event stream.\n\nClients must also handle child-process startup failures, unexpected exits, stderr diagnostics, cancellation, and their own deadlines. Do not parse stderr as protocol data.\n\n## Shutdown\n\nClose the child's stdin to request an orderly shutdown. Pi disposes the active runtime before exiting. Clients should still handle process signals and unexpected exits.\n\nAn extension can also request shutdown through its extension context. Pi completes shutdown after the current command or after the active run emits `agent_settled`.\n\n## Minimal client\n\nThis Python example uses a binary pipe reader, which splits on LF without treating Unicode separators as protocol boundaries:\n\n```python\nimport json\nimport subprocess\n\nprocess = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n)\n\nassert process.stdin is not None\nassert process.stdout is not None\n\ncommand = {\"id\": \"prompt-1\", \"type\": \"prompt\", \"message\": \"Hello\"}\nprocess.stdin.write(json.dumps(command).encode(\"utf-8\") + b\"\\n\")\nprocess.stdin.flush()\n\nwhile line := process.stdout.readline():\n    record = json.loads(line)\n    if record.get(\"type\") == \"message_update\":\n        update = record[\"assistantMessageEvent\"]\n        if update[\"type\"] == \"text_delta\":\n            print(update[\"delta\"], end=\"\", flush=True)\n    elif record.get(\"type\") == \"agent_settled\":\n        print()\n        break\n\nprocess.stdin.close()\nprocess.wait()\n```\n\nFor maintained TypeScript clients, use the checked [RPC client example](../examples/rpc-client.ts). It requires a built Pi CLI because the repository example points to `dist/cli.js`.\n\n## Reference\n\n- [RPC Commands](rpc-commands.md): every stdin command and response\n- [JSON Event Stream](json.md): shared stdout session events and streaming reconstruction\n- [RPC Extension UI](rpc-extension-ui.md): dialogs, notifications, responses, and limitations\n- [Message Types](message-types.md): messages and content blocks used by responses and events\n- [Session File Format](session-format.md): entries returned by session commands\n- [`rpc-types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-types.ts): exported TypeScript protocol definitions\n- [`RpcClient`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-client.ts): subprocess client implementation\n\n## Moved reference anchors\n\nThe detailed references formerly on this page now have dedicated pages. These anchors preserve existing links.\n\n<a id=\"prompt\"></a>\n<a id=\"steer\"></a>\n<a id=\"follow_up\"></a>\n<a id=\"abort\"></a>\n<a id=\"clear_queue\"></a>\n<a id=\"new_session\"></a>\n<a id=\"get_state\"></a>\n<a id=\"get_messages\"></a>\n<a id=\"set_model\"></a>\n<a id=\"cycle_model\"></a>\n<a id=\"get_available_models\"></a>\n<a id=\"set_thinking_level\"></a>\n<a id=\"cycle_thinking_level\"></a>\n<a id=\"get_available_thinking_levels\"></a>\n<a id=\"set_steering_mode\"></a>\n<a id=\"set_follow_up_mode\"></a>\n<a id=\"compact\"></a>\n<a id=\"set_auto_compaction\"></a>\n<a id=\"set_auto_retry\"></a>\n<a id=\"abort_retry\"></a>\n<a id=\"bash\"></a>\n<a id=\"abort_bash\"></a>\n<a id=\"get_session_stats\"></a>\n<a id=\"export_html\"></a>\n<a id=\"switch_session\"></a>\n<a id=\"fork\"></a>\n<a id=\"clone\"></a>\n<a id=\"get_fork_messages\"></a>\n<a id=\"get_entries\"></a>\n<a id=\"get_tree\"></a>\n<a id=\"get_last_assistant_text\"></a>\n<a id=\"set_session_name\"></a>\n<a id=\"get_commands\"></a>\n\nCommand details moved to [RPC Commands](rpc-commands.md).\n\n<a id=\"message_update-streaming\"></a>\n<a id=\"bash_execution_update\"></a>\n<a id=\"compaction_start--compaction_end\"></a>\n<a id=\"summarization_retry_scheduled--summarization_retry_attempt_start--summarization_retry_finished\"></a>\n\nEvent details moved to [JSON Event Stream](json.md).\n\n<a id=\"extension-ui-protocol\"></a>\n\nExtension interaction details moved to [RPC Extension UI](rpc-extension-ui.md).\n","packages/coding-agent/docs/sdk.md":"# SDK\n\n`@earendil-works/pi-coding-agent` embeds Pi in a Node.js or Bun process. It provides direct TypeScript access to the agent, sessions, tools, models, and resources used by the command-line application.\n\nUse the SDK for in-process TypeScript integration. For a language-independent or isolated subprocess, see [CLI Integration](cli-integration.md).\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\nconst { session } = await createAgentSession();\n\ntry {\n  await session.prompt(\"What files are in the current directory?\");\n  console.log(session.getLastAssistantText());\n} finally {\n  session.dispose();\n}\n```\n\nThis uses the working directory, discovered resources, stored settings, and configured credentials. `prompt()` resolves when the run finishes.\n\nThe [complete minimal example](../examples/sdk/01-minimal.ts) also streams text events. All [SDK examples](../examples/sdk/) are typechecked with the repository.\n\n<a id=\"session-management\"></a>\n\n## Session lifecycle\n\n`createAgentSession()` creates an `AgentSession`. The session owns one conversation, its model and tools, queued messages, compaction state, and extension runtime.\n\nRead current state through `session.messages`, `session.model`, `session.thinkingLevel`, `session.systemPrompt`, and `session.getActiveToolNames()`.\n\n`session.systemPrompt` is read-only and returns the current effective system prompt, including changes that have not yet been sent to the model. Tool changes are declared to the model before the next request.\n\n<a id=\"sessionmanager-api\"></a>\n\n### Session storage\n\nSessions are persistent by default. `SessionManager` owns the persisted or in-memory entry tree and tracks its active leaf. Branching changes that leaf without deleting abandoned branches. When Pi reconstructs model context, the manager selects the active branch and applies compaction.\n\n`SessionManager` is authoritative for finalized model context. Restore external history by constructing the session with a manager containing those entries. Assigning `session.agent.state.messages` does not replace persisted context.\n\nUse an in-memory manager when the host does not want session files:\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\nSee the checked [sessions example](../examples/sdk/11-sessions.ts) for creating, opening, continuing, listing, and forking sessions. [Session File Format](session-format.md) defines the persisted JSONL contract, and [Message Types](message-types.md) defines transcript values. For exact methods and signatures, use the exported TypeScript declarations or [`session-manager.ts`](../src/core/session-manager.ts).\n\n`cwd` selects the workspace used for project resource discovery, context files, session grouping, and built-in tool paths. Pass it explicitly when the target differs from `process.cwd()`.\n\n`session.dispose()` aborts active work, invalidates extension contexts, disconnects from the agent, and removes event listeners. Call it when the session is no longer needed.\n\n`AgentSessionRuntime` adds `newSession()`, `switchSession()`, `fork()`, and `importFromJsonl()`. Each operation replaces the active `AgentSession` and recreates services for the target working directory.\n\nAfter a runtime replacement, subscriptions belong to the old `AgentSession` and must be rebound. See the [session runtime example](../examples/sdk/13-session-runtime.ts).\n\n## Prompting\n\n`prompt()` handles extension commands and expands file-based prompt templates before ordinary user messages enter the agent. For an accepted agent run, it resolves after the run finishes, including automatic retries.\n\nA prompt sent while the session is already streaming must specify whether it should steer the current run or follow it. Calling `prompt()` without that choice rejects rather than guessing.\n\nA steering message enters after the current assistant turn and its tool calls. A follow-up enters after the current run finishes its pending work. `steer()` and `followUp()` expose those behaviors directly and return `\"queued\"` if the input was queued (including after an extension transformed it), or `\"handled\"` if an extension consumed it.\n\n`abort()` stops the active operation and waits for the session to become idle. `waitForIdle()` waits without aborting it.\n\n## Subscribing to events\n\nSubscribe before prompting when the host needs streamed output:\n\n```typescript\nconst unsubscribe = session.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\ntry {\n  await session.prompt(\"Explain this repository\");\n} finally {\n  unsubscribe();\n}\n```\n\nSession events report message updates, tool execution, queues, compaction, retries, and run lifecycle changes.\n\n`message_end` contains the authoritative completed message. `agent_end` marks the end of one low-level agent run, but automatic recovery or queued work can still follow.\n\nUse `agent_settled` when the host needs to know that Pi will not continue automatically.\n\n## Configuring a session\n\nWithout overrides, the factory creates a `ModelRuntime`, file-backed `SettingsManager`, persistent `SessionManager`, `DefaultResourceLoader`, and the configured default tools.\n\nEach boundary can be supplied explicitly:\n\n- `modelRuntime`, `model`, `thinkingLevel`, and `scopedModels` control model access and selection.\n- `settingsManager` supplies merged settings or an in-memory configuration.\n- `sessionManager` supplies persistent or in-memory conversation history.\n- `resourceLoader` supplies extensions, skills, prompt templates, themes, and context files.\n- `tools`, `noTools`, `excludeTools`, and `customTools` control the active tool set.\n\nUse `DefaultResourceLoader` when you want standard discovery with selected overrides. Supply a custom `ResourceLoader` when the host owns resource storage and discovery completely.\n\n<a id=\"inlineextension\"></a>\n\nInline extension factories can be supplied through `DefaultResourceLoader`. Give one an `InlineExtension` name only when it needs a stable name in diagnostics and startup output. A named inline extension with `replaceable: true` is left out when another extension registers a tool, command, or flag with a name it registers during loading, instead of both loading with a conflict. The CLI's built-in codemode, tool search, and MCP extensions are replaceable. A named entry with `builtin: true` is not an inline extension: it supplies the code of the `builtin:<name>` extension, which loads like a configured extension file. It loads by default, is listed in `pi config`, and is disabled by `-builtin:<name>` in the `extensions` setting or by `noExtensions`; `additionalExtensionPaths: [\"builtin:<name>\"]` loads it explicitly. It loads after project trust is resolved, so it cannot handle `project_trust`. The CLI's built-in extensions use it.\n\n<a id=\"codemode-mcp\"></a>\n\nThe CLI loads `codemode`, `tool_search`, and MCP as built-in extensions. SDK sessions do not; add `createCodemodeExtension()`, `createToolSearchExtension()`, and `createMcpExtension()` to the `extensionFactories` of `DefaultResourceLoader`. `codemode` and `tool_search` are registered inactive: enable them through the `defaultTools` setting (`[\"+codemode\", \"+tool_search\"]` keeps the other default tools), or let the MCP extension activate them: `codemode` for servers with `codemode` exposure, `tool_search` for servers with `deferred` exposure. The MCP extension connects its servers on `session_start`, so call `session.bindExtensions()`. See [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts).\n\nSee the focused examples for [models](../examples/sdk/02-custom-model.ts), [tools](../examples/sdk/05-tools.ts), [extensions](../examples/sdk/06-extensions.ts), and [full control](../examples/sdk/12-full-control.ts).\n\n## Examples\n\n| Example | Purpose |\n|---|---|\n| [Minimal](../examples/sdk/01-minimal.ts) | Create, prompt, observe, and dispose a session |\n| [Custom model](../examples/sdk/02-custom-model.ts) | Select a model and thinking level |\n| [System prompt](../examples/sdk/03-custom-prompt.ts) | Replace or append to the system prompt |\n| [Skills](../examples/sdk/04-skills.ts) | Discover, filter, and add skills |\n| [Tools](../examples/sdk/05-tools.ts) | Select built-in tools and their working directory |\n| [Extensions](../examples/sdk/06-extensions.ts) | Load file-based and inline extensions |\n| [Context files](../examples/sdk/07-context-files.ts) | Add or replace project instructions |\n| [Prompt templates](../examples/sdk/08-prompt-templates.ts) | Add file-style prompt templates |\n| [Credentials](../examples/sdk/09-api-keys-and-oauth.ts) | Configure credential and model storage |\n| [Settings](../examples/sdk/10-settings.ts) | Supply file-backed or in-memory settings |\n| [Sessions](../examples/sdk/11-sessions.ts) | Control session persistence and restoration |\n| [Full control](../examples/sdk/12-full-control.ts) | Replace default discovery and state services |\n| [Session runtime](../examples/sdk/13-session-runtime.ts) | Replace the active session safely |\n| [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts) | Add the `codemode`, `tool_search`, and MCP extensions |\n\n<a id=\"exports\"></a>\n\n## Resources\n\n- [Choose a Model](models.md) covers model selection and compatible endpoints; [Providers](providers.md) covers credentials and provider-specific setup.\n- [Configuration](configuration.md) explains normal discovery and settings; [Settings](settings.md) lists every setting.\n- [Sessions and Context](sessions.md) explains session behavior; [Session Format](session-format.md) defines persisted entries; [Message Types](message-types.md) defines shared transcript values.\n- [Extensions](extensions.md), [Skills](skills.md), and [Prompt Templates](prompt-templates.md) document resources supplied through a `ResourceLoader`.\n- [CLI Integration](cli-integration.md) covers print, JSON, and RPC alternatives to an in-process SDK integration.\n","packages/coding-agent/docs/security.md":"# Run Pi safely\n\nTreat model-generated commands and code as untrusted. Pi can read, change, and execute files with the permissions of the account that started it, and it does not ask for approval before every tool call. Extensions, package installers, language servers, and other child processes run with those same permissions unless an operating-system or virtualization boundary restricts them.\n\nFiles, comments, instructions, command output, and model responses can steer the model through prompt injection. Project trust controls which project resources load at startup, but it does not make that content or the resulting actions safe.\n\nSafety comes from limiting the files, credentials, processes, and network services Pi can access and affect if a generated action is wrong or hostile. Watching the transcript, using project trust, and reviewing changes do not create a security boundary.\n\n## Choose how to run Pi\n\nDifferent ways of running Pi place different limits on what generated commands can access:\n\n| How Pi runs | What remains protected |\n|---|---|\n| Directly, with the permissions of its operating-system user | Anything that user cannot access. A dedicated user account can narrow those permissions, but Pi still shares the operating system and network with other users. |\n| Entirely inside a container, virtual machine, or sandbox | Host files and processes that you do not expose to the environment. Credentials and network services remain accessible if you make them available inside it. This is usually the strongest practical option. |\n| Outside the isolated environment, with only its built-in tools running inside | Host resources are protected from actions performed through those tools. Pi itself and other extensions remain outside the boundary, so this is a narrower form of isolation. |\n\nThe working folder controls resource discovery and the default location for tools, but it does not prevent commands from accessing other paths available to the Pi process.\n\nWhichever option you choose, only provide the files and services required for the task. Keep credentials outside the environment where possible, or use narrowly scoped, short-lived credentials. Restrict network access when commands do not need it.\n\nFor setup instructions and the limitations of each isolation method, see [Run Pi in an isolated environment](containerization.md).\n\n<a id=\"project-trust\"></a>\n\n## Understand project trust\n\nProject trust controls whether Pi loads most settings and resources supplied by a working folder. It prevents a folder from silently loading executable extensions before you approve it.\n\nProject trust is not a complete startup boundary. Pi reads the project `sessionDir` setting while selecting or creating a session, before it resolves project trust. Declining trust prevents the remaining project settings and protected resources from loading, but it cannot undo that initial session-directory lookup.\n\nProject trust does not limit what tool calls can access or affect. After Pi starts, enabled tools still use the operating-system permissions of the Pi process. Instructions and other content in the folder can also influence the model.\n\n### Resources protected by project trust\n\nPi requires a project-trust decision when it finds any of these resources from the current working directory:\n\n- `.pi/settings.json`\n- `.pi/mcp.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts`, or `.pi/themes`\n- `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md`\n- project `.agents/skills` in the current directory or an ancestor directory\n\nA bare `.pi` directory does not require project trust.\n\nGranting project trust allows Pi to load:\n\n- project settings\n- project MCP servers from `.pi/mcp.json`\n- extensions, skills, prompt templates, themes, and system-prompt files under `.pi`\n- missing packages configured through project settings\n- project-local and project-package extensions\n\nDeclining project trust skips those protected resources, except for the initial `sessionDir` lookup described above.\n\nContext files such as `AGENTS.override.md`, `AGENTS.md`, and `CLAUDE.md` load regardless of project trust unless you disable context loading. Treat instructions in a folder as untrusted input even when you decline project trust.\n\n### How Pi chooses a trust decision\n\nA command-line `--approve` or `--no-approve` override applies first. When protected resources exist and there is no command-line override:\n\n1. User-level and command-line extensions can handle the `project_trust` event. The first extension that returns yes or no owns the decision.\n2. If no extension decides, Pi looks for a saved decision for the current directory or one of its parents. The closest decision applies.\n3. If no saved decision applies, Pi follows the global `defaultProjectTrust` setting, whose default is `\"ask\"`.\n\nSaved decisions use canonical directory paths and live in:\n\n```text\n~/.pi/agent/trust.json\n```\n\nUse `/trust` to save a decision for future Pi processes.\n\n### Project trust without an interactive prompt\n\nPrint, JSON, and RPC modes cannot show the built-in trust prompt. If no command-line override, extension, or saved decision applies:\n\n- `defaultProjectTrust: \"always\"` loads protected project resources.\n- `defaultProjectTrust: \"ask\"` or `\"never\"` skips them.\n\nUse `--approve` or `--no-approve` when an automated run needs an explicit one-time decision.\n\n## Reduce impact and improve recovery\n\nThese practices do not replace isolation, but they reduce exposure or make recovery easier:\n\n- Give Pi access only to files and services required for the task.\n- Use snapshots, backups, or version control before substantial changes.\n- Review extensions and packages before loading them. Extensions execute inside the Pi process.\n- Prefer narrowly scoped, short-lived credentials.\n- Review diffs and generated output before applying results to another system.\n- Review sessions before exporting or sharing them. They can contain prompts, tool arguments, command output, file contents, and credentials exposed during the conversation.\n\n## Report a security issue\n\nFollow the repository [Security Policy](https://github.com/earendil-works/pi/blob/main/SECURITY.md). Do not open a public issue for a security-sensitive report.\n\nExpected local-agent behavior, prompt injection from untrusted content, lack of a built-in sandbox, and behavior from user-installed extensions or skills are generally outside the security boundary unless the report demonstrates a privilege-boundary bypass or access that the local user did not already have.\n","packages/coding-agent/docs/session-format.md":"# Session File Format\n\nSessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field. Session entries form a tree structure via `id`/`parentId` fields, enabling in-place branching without creating new files.\n\nFor programmatic creation, persistence, and tree navigation, see the [`SessionManager` API](sdk.md#sessionmanager-api).\n\n\n## File Location\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<session-id>.jsonl\n```\n\nBy default, `<session-id>` is a UUID. Callers can supply a custom ID through the SDK or `--session-id`. For `<path>`, Pi removes the leading path separator and replaces `/`, `\\\\`, and `:` with `-`.\n\n## Deleting Sessions\n\nSessions can be removed by deleting their `.jsonl` files under `~/.pi/agent/sessions/`.\n\nPi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.\n\n## Session Version\n\nSessions have a version field in the header:\n\n- **Version 1**: Linear entry sequence (legacy, auto-migrated on load)\n- **Version 2**: Tree structure with `id`/`parentId` linking\n- **Version 3**: Renamed `hookMessage` role to `custom` (extensions unification)\n\nExisting sessions are automatically migrated to the current version (v3) when loaded.\n\n## Source Files\n\nSource on GitHub ([pi](https://github.com/earendil-works/pi)):\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts) - Session entry types and SessionManager\n- [Message Types](message-types.md) - Shared message and content-block reference\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/messages.ts) - Extended message types\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/ai/src/types.ts) - Base message and content-block types\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts) - Extensible `AgentMessage` union\n\nFor TypeScript definitions in your project, inspect `node_modules/@earendil-works/pi-coding-agent/dist/` and `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Messages\n\nA `message` entry stores an [`AgentMessage`](message-types.md). Message content blocks, roles, usage, and message timestamps are defined in [Message Types](message-types.md).\n\nSession entry timestamps are ISO 8601 strings. The nested message timestamp is a Unix timestamp in milliseconds.\n\n## Entry Base\n\nAll entries (except `SessionHeader`) extend `SessionEntryBase`:\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // Usually an 8-char hex ID; may fall back to a full UUID\n  parentId: string | null;  // Parent entry ID (null for a root entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## Entry Types\n\n### SessionHeader\n\nFirst line of the file. Metadata only, not part of the tree (no `id`/`parentId`).\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\nFor sessions with a parent (created via `/fork`, `/clone`, or `newSession({ parentSession })`):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### SessionMessageEntry\n\nA message in the conversation. The `message` field contains an `AgentMessage`. System messages carry the prompt and tool loadout: the first request of a session persists one with every prompt section and tool declaration, and later changes persist as system messages that patch `sections` by name (`null` removes one) and list `toolsAdded`/`toolsRemoved`. Replaying them in order yields the current prompt and tools; there is no separate prompt state entry.\n\n```json\n{\"type\":\"message\",\"id\":\"a0b1c2d3\",\"parentId\":null,\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"message\":{\"role\":\"system\",\"content\":\"\",\"sections\":{\"preamble\":\"You are an expert coding assistant...\",\"tools\":\"<tools>\\n- read: ...\\n</tools>\",\"cwd\":\"/project\"},\"toolsAdded\":[{\"name\":\"read\",\"description\":\"...\",\"parameters\":{}}],\"timestamp\":1733234400000}}\n{\"type\":\"message\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:04:00.000Z\",\"message\":{\"role\":\"system\",\"content\":\"\",\"sections\":{\"skills\":\"<skills>...</skills>\"},\"toolsRemoved\":[{\"name\":\"write\"}],\"timestamp\":1733234640000}}\n```\n\nSessions created before system messages existed have no leading system message; the first request declares the current prompt as a later system message, which replays the same way.\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\",\"timestamp\":1733234401000}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"api\":\"anthropic-messages\",\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\",\"timestamp\":1733234402000}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false,\"timestamp\":1733234403000}}\n```\n\nAssistant messages name the model that produced them. Newer messages also record `thinkingLevel`, the Pi thinking level requested for that response.\n\n### ModelChangeEntry\n\nEmitted when the user switches models mid-session. The latest entry is the selected model, which may be a [virtual model](virtual-models.md); assistant messages then name the physical model that answered.\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### ThinkingLevelChangeEntry\n\nEmitted when the user changes the thinking/reasoning level.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### UsageEntry\n\nRecords model-attributed usage that is not an assistant message and does not participate in LLM context. `kind` is an arbitrary string identifying the operation; for example, cache warming uses `\"cache_warm\"`.\n\n```json\n{\"type\":\"usage\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:08:00.000Z\",\"kind\":\"cache_warm\",\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{\"input\":0,\"output\":0,\"cacheRead\":50000,\"cacheWrite\":0,\"totalTokens\":50000,\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0.015,\"cacheWrite\":0,\"total\":0.015}}}\n```\n\nUsage entries contribute to session token and cost totals. Pi hides them from the conversation tree. Consumers should treat unknown `kind` values as normal usage rather than rejecting them.\n\n### CompactionEntry\n\nCreated when context is compacted. Stores a summary of earlier messages and a complete system prompt/tool checkpoint.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000,\"systemMessage\":{\"role\":\"system\",\"content\":\"You are a coding assistant.\",\"toolsAdded\":[],\"timestamp\":1733235000000}}\n```\n\n`firstKeptEntryId` is required. It identifies the first entry retained from before the compaction entry. When rebuilding context, Pi replaces older summarized entries with the compaction summary and keeps the range beginning at this entry. A retain-none compaction stores its own ID in this field, so no preceding entries are retained.\n\nOptional fields:\n- `systemMessage`: The replayed prompt sections and tool declarations at the compaction boundary; it becomes the leading system message of the compacted context, and system messages among the kept entries are dropped in its favor. It is absent on older session entries.\n- `usage`: LLM usage from generating the summary; included in session token and cost totals\n- `details`: Implementation-specific data (e.g., `{ readFiles: string[], modifiedFiles: string[] }` for default, or custom data for extensions)\n- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)\n\n### ContextEditEntry\n\nAppend-only edit of one earlier context-producing entry. It changes only future model context; the target entry and its metadata remain unchanged in raw history, UI, exports, and session accounting.\n\n```json\n{\"type\":\"context_edit\",\"id\":\"g6h7i8j9\",\"parentId\":\"f6g7h8i9\",\"timestamp\":\"2024-12-03T14:11:00.000Z\",\"targetId\":\"c3d4e5f6\",\"replacement\":null}\n```\n\nTargets may be user, assistant, tool-result, or custom-message entries. `replacement: null` omits the target from model context. A non-null `replacement` replaces only the target message content. String replacements for assistant and tool-result entries are normalized to one text block because those roles require content arrays. If several edits target the same entry, the latest edit on the active branch wins. Edits are branch-relative: navigating to a point before the edit reveals the target's original contribution again.\n\n### BranchSummaryEntry\n\nCreated when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\n`parentId` is the entry from which the new branch continues. `fromId` is the previous leaf whose abandoned path was summarized.\n\nOptional fields:\n- `usage`: LLM usage from generating the summary; included in session token and cost totals\n- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default, or custom data for extensions\n- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)\n\n### CustomEntry\n\nExtension state persistence. Does NOT participate in LLM context.\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\nUse `customType` to identify your extension's entries on reload. Interactive mode can render custom entries via `pi.registerEntryRenderer(customType, renderer)`, but they still do not participate in LLM context.\n\nPi stores [virtual model](virtual-models.md) router state as custom entries with `customType` `pi.virtual-model-state` and `data` `{ provider, modelId, state }`.\n\n### CustomMessageEntry\n\nExtension-injected messages that DO participate in LLM context.\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\nFields:\n- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)\n- `display`: `true` = show in TUI with distinct styling, `false` = hidden\n- `details`: Optional extension-specific metadata (not sent to LLM)\n\n### LabelEntry\n\nUser-defined bookmark/marker on an entry.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\nSet `label` to `undefined` to clear a label.\n\n### SessionInfoEntry\n\nSession metadata (e.g., user-defined display name). Set via `/name`, `--name` / `-n`, or `pi.setSessionName()` in extensions.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nThe session name is displayed in the session selector (`/resume`) instead of the first message when set.\n\n## Tree Structure\n\nEntries normally form one tree, but navigation APIs can create multiple roots:\n- A root entry has `parentId: null`; the first entry is initially the root\n- Each non-root entry points to its parent via `parentId`\n- Branching creates new children from an earlier entry\n- The \"leaf\" is the current position in the tree\n- Calling `resetLeaf()` or `branchWithSummary(null, ...)` allows a later entry to become another root\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Context Building\n\n`buildContextEntries()` walks from the current leaf to the root, producing the active entry list while honoring compaction:\n\n1. Collects all entries on the path\n2. If one or more `CompactionEntry` values are on the path, uses the latest one:\n   - Includes the compaction entry first\n   - Includes non-system entries from `firstKeptEntryId` up to, but not including, the compaction entry\n   - Includes entries after the compaction entry\n3. Preserves non-message entries in the selected range so interactive mode can render them\n\n`buildSessionProjection()` then applies the latest `context_edit` for each selected target. It returns the model-visible messages together with their source entries. Omitted targets produce no message; replacements retain the source entry's role and metadata while changing only content. The raw selected entries are not modified.\n\n`buildSessionContext()` builds on that projection to produce the message list for the LLM:\n\n1. Extracts current model and thinking level settings from the full path\n2. Converts selected entries to messages:\n   - `message` -> stored `AgentMessage`\n   - `compaction` -> complete system checkpoint followed by `compactionSummary`\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `context_edit` -> no context message of its own\n   - `usage` and `custom` -> no context message\n\nThe compaction summary replaces entries before `firstKeptEntryId`. Pre-compaction system messages are folded into the complete checkpoint rather than replayed from the retained range. Retained non-system entries and all entries after the compaction remain available to the LLM.\n\n## Parsing Example\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"usage\":\n      console.log(`[${entry.id}] Usage (${entry.kind}): ${entry.usage.totalTokens} tokens`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n","packages/coding-agent/docs/sessions.md":"# Sessions and Context\n\nPi saves a conversation as a session. The active branch of that session supplies conversation history for the next model request. Use session commands to continue work, explore another branch, or reduce the amount of history sent to the model.\n\n## Continue or switch sessions\n\nPi saves sessions automatically unless you start it with `--no-session`.\n\n```bash\npi --continue\npi --resume\n```\n\n`--continue` opens the most recent session for the current working directory. `--resume` opens the session picker. In interactive mode, `/resume` opens the same picker and `/new` starts a new session.\n\nUse `/name` or `--name` to assign a recognizable session name. Run `/session` to verify the current session file, ID, message count, token usage, and cost.\n\nThe session picker lets you search, rename, and delete sessions. It can also show paths, change sorting, and limit results to named sessions. See [Keybindings](keybindings.md#sessions) for its shortcuts.\n\n## Choose how to branch\n\nPi stores entries as a tree, so returning to an earlier point does not erase the branch you leave.\n\n| Action | Result | Use it when |\n|---|---|---|\n| `/tree` | Moves within the current session file | Related alternatives should stay together |\n| `/fork` | Creates a new session from an earlier user message | The alternative should become separate work |\n| `/clone` | Copies the active branch into a new session | You want a separate copy of the current state |\n\nIn `/tree`, select a user message to put its text back in the editor. Edit and submit it to create another branch. Selecting an assistant response or another entry continues after that entry with an empty editor.\n\nWhen you leave a branch, Pi can summarize it and attach that summary to the branch you enter. This preserves relevant work from the abandoned path without including every message from it.\n\nFor the persisted tree and entry types, see [Session Format](session-format.md).\n\n## Manage conversation context\n\nThe model receives the active branch, not every branch in the session file. Pi combines that history with the system prompt, discovered context files, available tools, and loaded skill descriptions. [How Pi Works](how-pi-works.md#context) describes how those inputs are assembled.\n\nThe footer shows current context usage. When the active context approaches the model's limit, Pi normally compacts older history automatically. Compaction adds a summary and keeps recent messages. It does not delete the original session entries.\n\nRun `/compact` to compact manually. You can add instructions when the summary should preserve a particular topic or decision. Configure automatic compaction and retained history through [Settings](settings.md#compaction).\n\nCompaction can fail if the provider is unavailable or cannot accept the summarization request. Correct the provider problem and run `/compact` again. Disabling automatic compaction does not disable the manual command.\n\nSee [Compaction Reference](compaction.md) for thresholds, retained boundaries, branch-summary behavior, and extension hooks.\n\n## Control session storage\n\nBy default, Pi stores sessions under `~/.pi/agent/sessions/`, grouped by working directory. Use `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, or the `sessionDir` setting to choose another location. The CLI option has highest precedence.\n\nUse `--no-session` for an ephemeral run. An ephemeral session cannot be resumed after Pi exits.\n\nUse `--session` when you already know the session path or ID. Use `--fork` to create a new session from an existing session before interactive mode starts.\n\n## Export or share a session\n\nUse `/export` to write the current session as HTML or JSONL. Use `/share` to upload it and get a viewer link. By default, Pi uses a Radius artifact when Radius authentication is configured; otherwise, it uses a private GitHub gist. Set [`sessionShareDestination`](settings.md#sessions-and-context) to `gist` or `radius` to force one destination.\n\nReview exported or shared sessions first. They can contain prompts, model responses, tool arguments, command output, file contents, and extension messages.\n\n## Report a bug\n\nRun `/bug [description]` to prepare a private report for the Pi developers. You can include the session transcript, omit it, or ask the current model to summarize the problem. Review any transcript or generated summary because it can contain sensitive conversation data.\n\nThe report includes environment and provider configuration without credential values, plus recorded error diagnostics. Upload it through `radius.pi.dev` or export the same report as a zip to inspect and share yourself. Uploads do not require a login; Radius authentication attributes the report to your account so the developers can follow up. If an upload fails, Pi offers to export the zip.\n","packages/coding-agent/docs/settings.md":"# Settings Reference\n\nThis reference lists user-configurable settings, their types, defaults, and purposes. Project settings override agent-directory settings. Resource lists are combined. See [Configuration](configuration.md) for file locations and trust behavior.\n\nAdd `\"$schema\": \"https://pi.dev/schemas/settings.schema.json\"` to `settings.json` for editor validation and completion.\n\n## Model and thinking\n\n<a id=\"model-cycling\"></a>\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `defaultProvider` | string | Automatic | Startup AI provider. |\n| `defaultModel` | string | Automatic | Startup model ID. |\n| `defaultThinkingLevel` | `\"off\" \\| \"minimal\" \\| \"low\" \\| \"medium\" \\| \"high\" \\| \"xhigh\" \\| \"max\"` | `\"medium\"` | Startup thinking level. |\n| `modelThinkingLevels` | object | None | Per-model startup thinking levels keyed by exact `provider/modelId`. |\n| `thinkingBudgets` | object | Built-in budgets | Token budgets for `minimal`, `low`, `medium`, and `high` thinking levels. |\n| `enabledModels` | `string[]` | All available models | Model patterns used for startup selection and model cycling. Uses the same format as `--models`. |\n| `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in the transcript. |\n| `showCacheMissNotices` | boolean | `false` | Show notices for significant cache misses, successful cache warming, compaction usage, and provider recovery. |\n| `cacheWarming` | `\"off\" \\| \"streaming\" \\| \"idle\"` | `\"streaming\"` | Keep eligible provider prompt caches warm during active runs or, with `\"idle\"`, between runs. Global setting only. |\n\nCache warming runs only when the model declares a cache lifetime and Pi estimates at least $0.05 in avoided cache-miss cost. Refresh usage counts toward session totals but does not enter model context. `/session` shows the next decision; extensions can override it with `cache_warming_decision`. See [Prompt Cache Lifetimes](models.md#prompt-cache-lifetimes).\n\nSee [Choose a Model](models.md) for model selection and thinking controls.\n\n## Interaction\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `steeringMode` | `\"all\" \\| \"one-at-a-time\"` | `\"one-at-a-time\"` | How queued steering messages are delivered. |\n| `followUpMode` | `\"all\" \\| \"one-at-a-time\"` | `\"one-at-a-time\"` | How queued follow-up messages are delivered. |\n| `externalEditor` | string | `$VISUAL`, `$EDITOR`, then platform default | Command opened by the external-editor keybinding. |\n| `doubleEscapeAction` | `\"tree\" \\| \"fork\" \\| \"none\"` | `\"tree\"` | Action for double Escape with an empty editor. |\n| `treeFilterMode` | `\"default\" \\| \"no-tools\" \\| \"user-only\" \\| \"labeled-only\" \\| \"all\"` | `\"default\"` | Initial filter used by `/tree`. |\n| `defaultProjectTrust` | `\"ask\" \\| \"always\" \\| \"never\"` | `\"ask\"` | Fallback project-trust behavior. **Can only be set in agent-directory settings.** |\n\n## Tools\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `defaultTools` | `string[]` | `read`, `bash`, `edit`, `write` | Tools enabled at startup. Plain names replace the defaults; `+name` adds a tool and `-name` removes one. An empty array disables all built-in tools but not extension or SDK tools. |\n| `codemode.mode` | `\"on\"` \\| `\"only\"` | `\"on\"` | How the `codemode` tool presents tools while it is active. `on`: declared tools get a note on calling them from scripts appended to their description, and `codemode` lists only tools that are not declared. `only`: `codemode` lists every tool scripts can call, and active built-in and extension tools are hidden from the model, so it reaches them through `codemode`. |\n| `codemode.inlineBudget` | number | `3000` | Estimated tokens (characters / 4) the `codemode` tool's description may spend on tool declarations. Tools that do not fit are left out and found with `searchTools()`. `0` lists only namespaces. |\n\nAvailable built-in tools are `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`. `defaultTools` can also name `codemode` and `tool_search`, which built-in extensions register inactive, and other extension tools registered inactive.\n\nA list of only `+name` and `-name` entries changes the inherited selection instead of replacing it. For example, this enables `codemode` next to the default tools:\n\n```json\n{\n  \"defaultTools\": [\"+codemode\"]\n}\n```\n\nThis replaces `bash` with `powershell` and enables `grep`: `[\"-bash\", \"+powershell\", \"+grep\"]`. Project settings apply on top of user settings: a project list with only `+name` and `-name` entries changes the user's selection, and a project list with a plain name replaces it. In one list, plain names form the selection, and `+name` and `-name` then apply in order.\n\n`/reload` enables tools newly added to `defaultTools`. It does not disable tools removed from it or re-enable unchanged tools you turned off. `--tools` with plain names, `--no-tools`, and `--no-builtin-tools` override `defaultTools`, also on reload.\n\nCLI tool options override this setting for one invocation. `--tools` with only `+name` and `-name` entries changes the resolved `defaultTools` selection instead, for example `pi --tools +codemode`. On `/reload`, these entries apply to the reloaded setting too, so a tool removed with `-name` stays removed. See [Command Line](cli.md#tools).\n\n## Sessions and context\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `sessionDir` | string | Agent session directory | Session storage directory. Relative paths resolve from the working directory. `PI_CODING_AGENT_SESSION_DIR` and `--session-dir` override this setting. |\n| `sessionShareDestination` | `\"auto\" \\| \"gist\" \\| \"radius\"` | `\"auto\"` | Destination used by `/share`. `auto` uses Radius when authenticated and otherwise creates a GitHub gist. `gist` always uses a gist. `radius` requires Radius authentication and never falls back to a gist. |\n\n### Compaction\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `compaction.enabled` | boolean | `true` | Enable automatic compaction. |\n| `compaction.reserveTokens` | number | `16384` | Free context tokens below which automatic compaction starts. |\n| `compaction.keepRecentTokens` | number | `20000` | Recent tokens retained without summarization. |\n| `compaction.modelOverrides` | object | None | Per-model token settings keyed by exact `provider/modelId`. |\n\n<a id=\"per-model-compaction-overrides\"></a>\n\nCompaction token values must be non-negative safe integers. Each value resolves independently from the matching model override, then the ordinary compaction setting, then the built-in default. Project and user objects merge before model lookup.\n\nSee [Compaction Reference](compaction.md) for trigger, summarization, and validation behavior.\n\n### Branch summaries\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `branchSummary.reserveTokens` | number | `16384` | Tokens reserved when summarizing branch history. |\n| `branchSummary.skipPrompt` | boolean | `false` | Skip the branch-summary prompt and default to no summary. |\n\n## Terminal and display\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `theme` | string | `\"system\"` | Built-in or custom theme name. `system` derives colors from the terminal theme. |\n| `quietStartup` | boolean \\| `\"header\"` | `false` | `true` hides the startup header and loaded-resource listing. `\"header\"` keeps the header (version and key hints) but hides the model scope line and loaded-resource listing. |\n| `tuiMode` | `\"regular\" \\| \"fullscreen\"` | `\"fullscreen\"` | Interactive terminal UI mode. |\n| `fullscreenExitOutput` | `\"transcript\" \\| \"resume-hint\"` | `\"transcript\"` | Output printed when fullscreen mode exits. |\n| `fullscreenScrollbar` | `\"auto\" \\| \"always\" \\| \"hidden\"` | `\"auto\"` | Fullscreen transcript scrollbar behavior. |\n| `fullscreenCopyOnSelect` | boolean | `true` | Copy selected text automatically in fullscreen mode. |\n| `fullscreenWheelScrollLines` | `\"auto\"` \\| number | `\"auto\"` | Lines per mouse-wheel event in fullscreen mode, from 1 to 100. `\"auto\"` moves one line per event in local macOS terminals, which already accelerate wheel and trackpad input; elsewhere, and over SSH, it speeds up fast wheel spins to at most 6 lines per event. Alt+wheel moves five times as far. |\n| `editorPaddingX` | number | `0` | Horizontal editor padding from 0 to 3 cells. |\n| `outputPad` | `0 \\| 1` | `1` | Horizontal transcript padding for messages, tool output, `!` command output, and summary blocks. |\n| `autocompleteMaxVisible` | number | `5` | Visible autocomplete entries, from 3 to 20. |\n| `showHardwareCursor` | boolean | `false` | Use the terminal cursor instead of Pi's drawn cursor. Pi still positions it for input methods. |\n| `terminal.showImages` | boolean | `true` | Display inline images when supported. |\n| `terminal.imageWidthCells` | number | `60` | Preferred inline image width in terminal cells. |\n| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when rendered content shrinks. |\n| `terminal.showTerminalProgress` | boolean | `false` | Show OSC 9;4 progress in the terminal tab. |\n| `terminal.hyperlinks` | `boolean \\| \"auto\"` | `\"auto\"` | Override OSC 8 hyperlink detection. |\n| `terminal.images` | `\"kitty\" \\| \"iterm2\" \\| \"auto\" \\| false` | `\"auto\"` | Override inline-image protocol detection. |\n| `terminal.trueColor` | `boolean \\| \"auto\"` | `\"auto\"` | Override true-color detection. |\n| `images.autoResize` | boolean | `true` | Resize images to at most 2000 by 2000 pixels before sending them to a model. |\n| `images.blockImages` | boolean | `false` | Prevent images from being sent to models. |\n| `markdown.codeBlockIndent` | string | `\"  \"` | Prefix used to indent rendered code blocks. |\n| `markdown.mermaid` | `\"off\" \\| \"final\" \\| \"streaming\"` | `\"streaming\"` | Mermaid rendering mode. |\n\nSee [Themes](themes.md) and [Terminal Setup](terminal-setup.md) for format and platform details.\n\n## Network and retries\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `transport` | `\"auto\" \\| \"sse\" \\| \"websocket\" \\| \"websocket-cached\"` | `\"auto\"` | Preferred transport for AI providers that support multiple transports. |\n| `httpProxy` | string | None | Proxy URL applied as `HTTP_PROXY` and `HTTPS_PROXY` for Pi-managed HTTP clients. **Can only be set in agent-directory settings.** |\n| `httpIdleTimeoutMs` | number | `300000` | HTTP header and body idle timeout in milliseconds. Set to `0` to disable. |\n| `websocketConnectTimeoutMs` | number | `15000` | WebSocket connection timeout in milliseconds. Set to `0` to disable. |\n| `retry.enabled` | boolean | `true` | Enable automatic agent-level retry for transient failures. |\n| `retry.maxRetries` | number | `3` | Maximum agent-level retry attempts. |\n| `retry.baseDelayMs` | number | `2000` | Initial exponential-backoff delay in milliseconds. |\n| `retry.maxAgentDelayMs` | number | `60000` | Maximum agent-level retry delay in milliseconds. |\n| `retry.provider.timeoutMs` | number | `httpIdleTimeoutMs` | Provider request timeout in milliseconds. |\n| `retry.provider.maxRetries` | number | `0` | Provider-level retry attempts. |\n| `retry.provider.maxRetryDelayMs` | number | `60000` | Maximum server-requested delay in milliseconds. Set to `0` to disable the limit. |\n\nKeep `retry.provider.maxRetries` at `0` unless provider-level retries are required. Provider retries can delay Pi from handling quota and usage-limit errors itself.\n\n## Shell\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `shellPath` | string | Platform default | Custom shell executable path. Supports a leading `~`. |\n| `shellCommandPrefix` | string | None | Prefix prepended to every shell command. |\n| `npmCommand` | `string[]` | `npm` | Command and arguments used for npm package lookup and installation. |\n\nSee [Shell aliases](shell-aliases.md) for shell setup and [Pi Packages](packages.md) for package-manager behavior.\n\n## Resources\n\nResource paths in user settings resolve from the agent directory. Paths in project settings resolve from the project `.pi` directory. Absolute paths and `~` are supported.\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `packages` | array | `[]` | npm, git, or local Pi package sources. See [Pi Packages](packages.md). |\n| `extensions` | `string[]` | `[]` | Extension files or directories. |\n| `skills` | `string[]` | `[]` | Skill files or directories. |\n| `prompts` | `string[]` | `[]` | Prompt-template files or directories. |\n| `themes` | `string[]` | `[]` | Theme files or directories. |\n| `enableSkillCommands` | boolean | `true` | Register skills as `/skill:name` commands. |\n\nResource arrays support glob exclusions with `!pattern`, exact inclusion with `+path`, and exact exclusion with `-path`. Pi loads resources listed in both user-level and project settings.\n\nThe built-in extensions are named `builtin:mcp`, `builtin:llama.cpp`, `builtin:codemode`, and `builtin:tool-search` in `extensions`. They load by default; `-builtin:mcp` disables one. A `+builtin:<name>` or `-builtin:<name>` entry in project settings overrides the user setting. `pi config` lists them under Built-in. `--no-extensions` disables them too, and `-e builtin:<name>` loads one explicitly.\n\n## Updates, telemetry, and warnings\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `collapseChangelog` | boolean | `false` | Show a condensed changelog after an update. |\n| `enableInstallTelemetry` | boolean | `true` | Enable anonymous install/update reporting and selected provider attribution headers. Does not control update checks. |\n| `enableAnalytics` | boolean | `false` | Opt in to analytics data sharing. Currently used only by the experimental first-run setup. |\n| `warnings.anthropicExtraUsage` | boolean | `true` | Warn when Anthropic subscription authentication may use paid extra usage. |\n","packages/coding-agent/docs/shell-aliases.md":"# Configure shell commands\n\nPi starts a separate non-interactive shell process for each Bash command. Non-interactive Bash does not expand aliases by default and usually does not load the same startup files as an interactive terminal.\n\nUse `shellPath` to choose the Bash executable and `shellCommandPrefix` to run setup before each command.\n\n## Understand which shell Pi uses\n\n| Command source | Shell |\n|---|---|\n| Model calls the built-in `bash` tool | Pi's resolved Bash executable |\n| You enter `!command` or `!!command` | The same resolved Bash executable |\n| Model calls the optional `powershell` tool | PowerShell 7 (`pwsh.exe`) or Windows PowerShell |\n| An extension provides or replaces a shell tool | The operations implemented by that extension |\n\nPi normally invokes Bash with `bash -c`. On Unix systems, it uses `/bin/bash`, then `bash` on `PATH`, and finally `sh` when Bash is unavailable. Native Windows first checks the configured path, then Git Bash, then `bash.exe` on `PATH`.\n\n## Choose a Bash executable\n\nSet `shellPath` in `~/.pi/agent/settings.json` when Pi should use a specific executable:\n\n```json\n{\n  \"shellPath\": \"~/.local/bin/bash\"\n}\n```\n\nOn Windows, use forward slashes or escape backslashes:\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```\n\nRun `/reload` after changing the setting. See [Run Pi on Windows](windows.md) for the native Windows defaults.\n\n## Run setup before every Bash command\n\nSet `shellCommandPrefix` to prepend shell setup to both the built-in `bash` tool and user-entered `!` or `!!` commands:\n\n```json\n{\n  \"shellCommandPrefix\": \"export CI=1\"\n}\n```\n\nPi joins the prefix and requested command with a newline. The prefix runs again for every command, so keep it fast and free of interactive prompts.\n\n## Enable Bash aliases\n\nStore aliases needed by Pi in a Bash-compatible file instead of parsing an entire interactive shell configuration.\n\nCreate `~/.bash_aliases`:\n\n```bash\nalias ll='ls -la'\nalias gs='git status --short'\n```\n\nThen configure Pi to enable alias expansion and load the file:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\nsource ~/.bash_aliases\"\n}\n```\n\nRun `/reload`, then verify the alias through Pi:\n\n```text\n!ll\n```\n\nThe command should produce the same listing as `ls -la`.\n\nAliases must use Bash-compatible syntax. Do not source an arbitrary `.zshrc` into Bash because zsh options, functions, and plugins may not parse or behave correctly there.\n\n## Troubleshooting\n\n### The prefix works for `!` but not for an extension tool\n\n`shellCommandPrefix` configures Pi's built-in Bash execution. An extension that replaces the `bash` tool or provides its own shell operations controls its own setup. Check that extension's documentation.\n\n### `shopt` is not found\n\nPi has fallen back to `sh` or `shellPath` points to a non-Bash shell. Install Bash or set `shellPath` to a Bash executable before using Bash-specific setup such as `shopt`.\n\n### A setup command waits for input\n\nRemove interactive commands from `shellCommandPrefix`. The prefix runs in a non-interactive process before every Bash command.\n\nFor the complete setting definitions, see [Shell settings](settings.md#shell).\n","packages/coding-agent/docs/skills.md":"# Skills\n\nSkills give Pi specialized instructions and supporting files for a particular kind of work. Pi advertises each available skill by name and description, then loads its full instructions only when the task calls for them.\n\nUse a skill when a workflow needs more context than a prompt template but does not need a new executable integration point. Skills can bundle scripts, references, and assets alongside their instructions.\n\nPi implements the [Agent Skills specification](https://agentskills.io/specification). Most invalid fields produce warnings rather than stopping startup.\n\n## Create a skill\n\nA skill is a directory containing `SKILL.md`:\n\n```text\npdf-tools/\n├── SKILL.md\n├── scripts/\n│   └── extract.sh\n├── references/\n│   └── formats.md\n└── assets/\n    └── template.json\n```\n\nStart `SKILL.md` with frontmatter followed by direct instructions:\n\n```markdown\n---\nname: pdf-tools\ndescription: Extract text and tables from PDF files. Use when reading, converting, or inspecting PDFs.\n---\n\n# PDF tools\n\nRead `references/formats.md` before converting a document. Run scripts relative to this skill directory.\n```\n\nThe description determines when the model considers loading the skill. State both what the skill does and when it applies. Avoid descriptions such as “Helps with PDFs,” which do not provide enough routing information.\n\nUse relative paths from the skill directory when referring to bundled files. Pi tells the model where the skill lives so it can resolve those paths.\n\n## Understand how skills load\n\nAt startup, Pi scans configured skill locations and adds each skill’s name, description, and path to the system prompt. It does not add the full instructions.\n\nWhen a task matches, the model reads `SKILL.md` and follows its instructions. This keeps detailed guidance out of context until it is needed. A model might fail to load a relevant skill, so use `/skill:name` when you need to force it.\n\nArguments after `/skill:name` are appended to the loaded instructions as a user request:\n\n```text\n/skill:pdf-tools extract report.pdf\n```\n\nSet `disable-model-invocation: true` in frontmatter when a skill should be available only through its explicit command. The `enableSkillCommands` [setting](settings.md) controls whether skill commands appear in interactive command discovery; manually entered `/skill:name` commands still work.\n\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the skill in your user or project skills directory. Directories containing `SKILL.md` are discovered recursively.\n\nPi also supports the Agent Skills locations `~/.agents/skills/` and `.agents/skills/`. Project `.agents/skills/` directories are discovered from the working directory through its ancestors, stopping at the repository root when one exists.\n\nPi accepts some standalone Markdown skills, but a directory containing `SKILL.md` is the portable form and should be preferred. See [Settings](settings.md#resources) and [Pi Packages](packages.md) for additional locations.\n\nProject skills can instruct the model to run scripts or modify files. Review unfamiliar skills and their supporting files before granting project trust.\n\n## Write portable frontmatter\n\nThe Agent Skills specification defines these fields:\n\n| Field | Purpose |\n|---|---|\n| `name` | Command and display name |\n| `description` | Routing description shown to the model |\n| `license` | License name or bundled license file |\n| `compatibility` | Environment requirements |\n| `metadata` | Additional key-value metadata |\n| `allowed-tools` | Experimental pre-approved tool list |\n| `disable-model-invocation` | Hide the skill from automatic model selection |\n\nNames use lowercase letters, numbers, and hyphens, with no leading, trailing, or consecutive hyphens. They can contain at most 64 characters; descriptions can contain at most 1024.\n\nPi neither requires nor warns when the declared name differs from the parent directory. Other Agent Skills implementations may enforce that requirement, so matching names remain the portable choice.\n\nMalformed `SKILL.md` files and declared skills without descriptions are not loaded. Name collisions keep the first discovered skill and produce a warning.\n\n## Validate and share a skill\n\nRun Pi from a location where the skill is discoverable, then inspect the startup diagnostics and `/skill:name` command. Run `/reload` after editing a skill during an active session.\n\nUse a [Pi package](packages.md) to distribute one or more skills through npm or git. Keep environment setup inside the skill and declare any required runtime dependencies in the package.\n\nFor examples, see the [Anthropic skills collection](https://github.com/anthropics/skills) and [Pi skills collection](https://github.com/badlogic/pi-skills).\n","packages/coding-agent/docs/slash-commands.md":"# Slash commands\n\nType `/` in Pi's terminal editor to search the commands available in the current session. This page lists the built-in commands in the current Pi release.\n\nExtensions, prompt templates, and skills can add commands. The command menu in Pi is therefore the exact reference for the resources loaded in your session.\n\n## Models and settings\n\n| Command | Description |\n|---|---|\n| `/settings` | Open settings |\n| `/model [provider/model]` | Select a model |\n| `/thinking [level]` | Set the thinking level |\n| `/scoped-models` | Configure the models used by interactive cycling |\n| `/login [provider]` | Add provider authentication |\n| `/logout` | Remove provider authentication |\n| `/llama` | Manage models on the configured llama.cpp router |\n\n## Sessions and context\n\n| Command | Description |\n|---|---|\n| `/new` | Start a new session |\n| `/resume` | Switch to another saved session |\n| `/name [name]` | Set the session display name, or show the current name when omitted |\n| `/session` | Show current session information and statistics |\n| `/tree` | Navigate the session tree |\n| `/fork` | Create a new session from an earlier user message |\n| `/clone` | Duplicate the current session at its current position |\n| `/compact [instructions]` | Compact the current context, optionally with custom instructions |\n| `/import <path>` | Import and resume a JSONL session |\n\n## Export and share\n\n| Command | Description |\n|---|---|\n| `/copy` | Copy the last assistant message |\n| `/export [path]` | Export the session as HTML or JSONL |\n| `/share` | Upload the session and return a viewer link |\n| `/bug [description]` | Prepare a private bug report for the Pi developers |\n\nReview a session before exporting or sharing it. Sessions can contain prompts, tool arguments, command output, file contents, and credentials exposed during the conversation.\n\n## Runtime and project\n\n| Command | Description |\n|---|---|\n| `/trust` | Save a project trust decision for future Pi processes |\n| `/reload` | Reload keybindings, extensions, skills, templates, themes, and context files |\n| `/hotkeys` | Show active keyboard shortcuts |\n| `/changelog` | Show changelog entries |\n| `/quit` | Quit Pi |\n\n## Commands added by resources\n\n- Extensions can register commands with their own arguments and completion behavior.\n- Each prompt template is available under its template name.\n- Skills are available as `/skill:name` when skill commands are enabled.\n\nUse `/reload` after adding or changing a discovered command resource. See [Extensions](extensions.md), [Prompt Templates](prompt-templates.md), and [Skills](skills.md) for their loading and naming rules.\n","packages/coding-agent/docs/terminal-setup.md":"# Configure your terminal\n\nMost modern terminals work with Pi without additional setup. Use this page when modified keys, scrolling, links, images, colors, or input-method editor (IME) positioning do not behave as expected.\n\nPi uses extended-key protocols so terminals can distinguish combinations such as `Shift+Enter` and `Alt+Enter` from plain `Enter`. Terminal proxies, multiplexers, and built-in IDE terminals can change or discard that information.\n\n## Troubleshooting\n\n| Symptom | Start here |\n|---|---|\n| `Shift+Enter` submits instead of inserting a line | Your terminal's section below; for tmux, see [Run Pi in tmux](tmux.md) |\n| `Alt+Enter` does not queue a follow-up | [WezTerm](#wezterm), [Alacritty](#alacritty), or [Windows Terminal](#windows-terminal) |\n| Fullscreen scrolling is unusually slow | [iTerm2](#iterm2) |\n| Links work but show no hover preview | [Ghostty](#ghostty) |\n| Inline images or colors are not detected | [Override detected capabilities](#override-detected-capabilities) |\n| An IME candidate window appears in the wrong place | [WezTerm](#wezterm) or [IntelliJ IDEA](#intellij-idea-integrated-terminal) |\n| Modified keys fail only inside tmux | [Run Pi in tmux](tmux.md) |\n\nUse `/hotkeys` to inspect Pi's active shortcuts. See [Keybindings](keybindings.md) to change them.\n\n## Kitty\n\nKitty supports the required keyboard protocol without additional configuration.\n\n## iTerm2\n\nRegular terminal mode works without additional configuration.\n\n### Fix slow fullscreen scrolling\n\nIn fullscreen mode, Pi owns the viewport, so iTerm2 sends mouse-wheel reports instead of scrolling native terminal history. Fast trackpad gestures can then move only about one line at a time.\n\nTo change this behavior:\n\n1. Open **iTerm2 > Settings > Advanced**.\n2. Search for **Trackpad scrolls fast?**.\n3. Set it to **No**.\n\nThis is an iTerm2-wide setting and can also change native trackpad scrolling. The underlying behavior is tracked in [iTerm2 issue 9619](https://gitlab.com/gnachman/iterm2/-/work_items/9619).\n\n## Apple Terminal\n\nPi enables enhanced key reporting when available. If Terminal.app still sends plain Return for `Shift+Enter`, Pi uses a local macOS modifier fallback and treats it as `Shift+Enter`.\n\nThe fallback works only when Pi runs on the same Mac as Terminal.app. It cannot inspect the local modifier state when Pi runs on another machine over SSH.\n\n## Ghostty\n\nAdd this mapping to Ghostty's configuration if `Alt+Backspace` does not work:\n\n```text\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nThe configuration file is `~/Library/Application Support/com.mitchellh.ghostty/config` on macOS and `~/.config/ghostty/config` on Linux.\n\nOlder Claude Code configurations may contain:\n\n```text\nkeybind = shift+enter=text:\\n\n```\n\nThis sends a raw linefeed, which Pi cannot distinguish from `Ctrl+J`. Remove the mapping if an older Claude Code installation is the only reason you added it. Pi already binds `Ctrl+J` as a newline alternative, so the mapping may appear to work while still preventing Pi and tmux from receiving a real `Shift+Enter` event.\n\n### Open links in fullscreen mode\n\nLinks remain clickable in fullscreen mode, but Ghostty does not show its normal hover underline or URL preview while Pi captures mouse input. Hold `Shift+Command` on macOS or `Shift+Ctrl` on Linux to use Ghostty's native link handling.\n\n## WezTerm\n\nWezTerm normally reports `Shift+Enter` through xterm extended keys. To enable the Kitty keyboard protocol explicitly, create `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\n### Forward Alt+Enter on macOS\n\nWezTerm binds `Option+Enter` to fullscreen by default on macOS. To use it for Pi's follow-up queue, add this entry to your `config.keys` table:\n\n```lua\n{\n  key = 'Enter',\n  mods = 'ALT',\n  action = wezterm.action.SendString('\\x1b[13;3u'),\n}\n```\n\nA complete minimal configuration is:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\n### Position an IME candidate window in WSL\n\nIf CJK IME candidates do not follow Pi's text cursor in WSL, show the hardware cursor:\n\n```bash\nexport PI_HARDWARE_CURSOR=1\npi\n```\n\nYou can instead set `showHardwareCursor` to `true` in Pi settings.\n\n## Alacritty\n\nAlacritty normally reports `Shift+Enter`. On macOS, `Option+Enter` can arrive as plain `Enter`. Add this to `~/.config/alacritty/alacritty.toml` to forward it to Pi:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nRestart Alacritty after changing the file.\n\n## VS Code integrated terminal\n\nVS Code 1.109.5 and newer enable the Kitty keyboard protocol in the integrated terminal by default.\n\nFor an older version, add a `Shift+Enter` terminal binding to `keybindings.json`:\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\nThe user `keybindings.json` file is normally located at:\n\n- macOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Linux: `~/.config/Code/User/keybindings.json`\n- Windows: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\n## Zed integrated terminal\n\nAdd these bindings to Zed's `keymap.json`:\n\n```json\n{\n  \"context\": \"Terminal\",\n  \"bindings\": {\n    \"shift-enter\": [\"terminal::SendText\", \"\\u001b[13;2u\"],\n    \"ctrl--\": [\"terminal::SendText\", \"\\u001b[45;5u\"],\n    \"ctrl-alt-]\": [\"terminal::SendText\", \"\\u001b[93;7u\"]\n  }\n}\n```\n\n## Windows Terminal\n\nWindows Terminal uses Pi's Windows and WSL shortcut defaults. See [Keybindings](keybindings.md) for the complete list.\n\n### Forward Shift+Enter\n\nOpen Windows Terminal's `settings.json` with `Ctrl+Shift+,` or **Settings > Open JSON file**. Add this object to its `actions` array:\n\n```json\n{\n  \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n  \"keys\": \"shift+enter\"\n}\n```\n\nFully close and reopen Windows Terminal, then verify that `Shift+Enter` inserts a new line in Pi.\n\n### Use Alt+Enter for follow-ups\n\nWindows Terminal binds `Alt+Enter` to fullscreen by default. Pi therefore uses `Ctrl+Q` for follow-ups on Windows and WSL.\n\nTo use `Alt+Enter` instead, configure Windows Terminal to forward the key and bind `app.message.followUp` to `alt+enter` in Pi's `keybindings.json`. See [Keybindings](keybindings.md#assign-keybindings).\n\n## xfce4-terminal and Terminator\n\nThese terminals cannot reliably distinguish modified Enter keys from plain `Enter`. Custom bindings such as `Ctrl+Enter` or `Shift+Enter` therefore may not work.\n\nUse a terminal with modern extended-key support when you need those shortcuts, such as Kitty, Ghostty, WezTerm, iTerm2, Windows Terminal, or a compatible Alacritty build.\n\n## IntelliJ IDEA integrated terminal\n\nIntelliJ IDEA's built-in terminal cannot reliably distinguish `Shift+Enter` from plain `Enter`. Use `Ctrl+J` for a newline or run Pi in a terminal with modern extended-key support.\n\nIf an IME candidate window does not follow the text cursor, show the hardware cursor:\n\n```bash\nexport PI_HARDWARE_CURSOR=1\npi\n```\n\n## Override detected capabilities\n\nPi automatically detects OSC 8 hyperlinks, inline image protocols, and truecolor support. A terminal proxy or multiplexer can make that detection inaccurate.\n\n| Capability | Environment variable | Setting |\n|---|---|---|\n| Hyperlinks | `PI_HYPERLINKS=1\\|0\\|auto` | `terminal.hyperlinks: true\\|false\\|\"auto\"` |\n| Inline images | `PI_IMAGE_PROTOCOL=kitty\\|iterm2\\|none\\|auto` | `terminal.images: \"kitty\"\\|\"iterm2\"\\|false\\|\"auto\"` |\n| Truecolor | `PI_TRUE_COLOR=1\\|0\\|auto` | `terminal.trueColor: true\\|false\\|\"auto\"` |\n\nSettings take precedence over environment variables. An unset value or `auto` preserves automatic detection.\n\nOnly force a capability supported by the complete terminal path. Unsupported escape sequences can corrupt rendering. See [Environment Variables](environment-variables.md#pi-process-configuration) and [Settings](settings.md) for the canonical value definitions.\n\n## Program status\n\nPi reports its state with the [Program Status Protocol (OSC 7501)](https://www.superlogical.com/rex/docs/build/program-status), so terminals and agent dashboards can show whether it is working, waiting for you, done, or failed:\n\n| State | When |\n|---|---|\n| `working` | An agent run or compaction is in progress. The message is the session name. |\n| `blocked` | An extension dialog or login waits for you. The message is the dialog title. |\n| `done` | A run finished. The message is the session name. |\n| `error` | A run ended with an error that is not retried. The message is the first line of the error. |\n| `idle` | Pi started, or you cancelled the run. |\n\nReports never contain prompts or model output. Pi sends them only after the terminal answers the protocol's support query; tmux and screen do not forward them. Set `PI_PROGRAM_STATUS=1` to send reports without asking, or `PI_PROGRAM_STATUS=0` to turn them off.\n","packages/coding-agent/docs/termux.md":"# Run Pi on Android with Termux\n\nPi runs on Android through [Termux](https://termux.dev/), a terminal emulator and Linux environment. Text input, file tools, and shell commands are supported. Pi can copy and paste text through the Android clipboard with Termux:API. Clipboard image paste is not supported.\n\n## Before you begin\n\nInstall Termux from [GitHub or F-Droid](https://github.com/termux/termux-app#installation). Do not use the deprecated Google Play build.\n\n[Termux:API](https://github.com/termux/termux-api#installation) is optional. Install it only when you want Pi to copy or paste Android clipboard text, or when shell commands need Android device APIs.\n\n## Install Pi\n\n1. Update Termux packages:\n\n   ```bash\n   pkg update && pkg upgrade\n   ```\n\n2. Install Node.js and Git:\n\n   ```bash\n   pkg install nodejs git\n   ```\n\n3. Install Pi:\n\n   ```bash\n   npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n   ```\n\n4. Verify the installation:\n\n   ```bash\n   pi --version\n   ```\n\n5. Open the folder you want to work in and start Pi:\n\n   ```bash\n   cd /path/to/working-folder\n   pi\n   ```\n\nContinue with the main [Quickstart](quickstart.md#3-choose-a-model) to connect a model and run your first task.\n\n## Access Android shared storage\n\nTermux cannot access shared Android storage until you grant permission. Run this once:\n\n```bash\ntermux-setup-storage\n```\n\nAfter approval, Android shared storage is available under `/storage/emulated/0` and through the links Termux creates under `~/storage/`.\n\nOnly grant this permission when Pi should be able to access those files. Commands and tools running in Termux use the same storage permissions as the Termux process.\n\n## Use clipboard commands\n\nPi uses `termux-clipboard-set` to copy text and `termux-clipboard-get` for its clipboard-paste shortcut. Shell commands can use both commands directly. Install the Termux:API app and its command-line package:\n\n```bash\npkg install termux-api\n```\n\nVerify the integration:\n\n```bash\nprintf 'Pi clipboard test' | termux-clipboard-set\ntermux-clipboard-get\n```\n\nThe second command should print `Pi clipboard test`.\n\nThe Termux clipboard API supports text only. Pi's clipboard-paste shortcut inserts that text into the editor but cannot attach clipboard images.\n\n## Add Termux-specific instructions\n\nPi detects that it is running in Termux, but it cannot infer how you want it to interact with Android. Add only the environment details relevant to your work to `~/.pi/agent/AGENTS.md`:\n\n````markdown\n# Termux environment\n\n- Pi runs in Termux on Android.\n- Shared Android storage is under `/storage/emulated/0`.\n- Open URLs with `termux-open-url \"https://example.com\"`.\n- Open files with `termux-open <path>`.\n- Do not access shared storage unless the task requires it.\n````\n\nRun `/reload` after changing the file during an active session.\n\n## Troubleshooting\n\n### Clipboard integration fails\n\nConfirm that you installed both components:\n\n1. The Termux:API Android app from the same source as Termux\n2. The `termux-api` command-line package\n\nThen run the clipboard verification commands above outside Pi. If they fail there, fix the Termux:API installation before retrying Pi's copy command.\n\n### Shared storage reports permission denied\n\nRun `termux-setup-storage`, approve the Android permission request, and retry the path under `~/storage/` or `/storage/emulated/0`.\n\n### Pi is not found after installation\n\nOpen a new Termux shell and run:\n\n```bash\nnpm prefix -g\ncommand -v pi\n```\n\nConfirm that the global npm binary directory is on `PATH`, then reinstall Pi if the package is missing.\n","packages/coding-agent/docs/themes.md":"# Customize Pi with themes\n\nThemes control the colors Pi uses in interactive mode and HTML exports. Pi includes the `system`, `dark`, and `light` themes. You can select one theme, follow your terminal's light or dark appearance, or create your own palette.\n\n## Use your terminal's colors\n\nThe `system` theme is the default. It builds Pi's colors from your terminal's theme, so Pi matches the terminal instead of bringing its own palette:\n\n- Pi queries the terminal's default foreground and background colors and its 16 ANSI colors.\n- Each Pi color takes its hue from one ANSI color, for example errors from red and links from blue.\n- Pi sets each color's lightness so that it stands out from the background by a minimum contrast. Body text keeps at least a 4.5:1 WCAG contrast ratio on the background and every panel.\n- When the terminal switches between light and dark, Pi queries the colors again and rebuilds the theme.\n\nThe theme adapts to what the terminal reports:\n\n| Terminal reports | Result |\n|---|---|\n| Background and ANSI colors | Colors from the terminal palette, placed for the actual background. |\n| Background only | Pi's own hues, placed for the actual background. |\n| Nothing | ANSI color indices and the terminal's default colors, which the terminal renders itself. Secondary text is faint, and panels have no background color. |\n\nPi asks the terminal for its colors when it starts. Terminals usually answer within a few milliseconds, and Pi waits at most 100 ms before showing the startup header. If the terminal does not answer in time, Pi uses the ANSI color fallback, and it still applies the colors if they arrive later, for example over a slow SSH connection. `system` is a reserved name: a custom theme with that name is ignored.\n\n<a id=\"selecting-a-theme\"></a>\n\n## Choose a theme\n\nOpen `/settings` and select **Theme**. You can use one theme for every terminal appearance or choose separate themes for light and dark terminals.\n\nThe selection is saved as the `theme` [setting](settings.md#terminal-and-display):\n\n```json\n{\n  \"theme\": \"dark\"\n}\n```\n\nWithout a `theme` setting, Pi uses `system`.\n\nAutomatic mode stores the light theme first and the dark theme second:\n\n```json\n{\n  \"theme\": \"light/dark\"\n}\n```\n\nPi decides whether the terminal is light or dark from its reported background and foreground colors. If the terminal does not report its background, Pi uses the terminal's light/dark notification, then the `COLORFGBG` environment variable, then dark. The same decision picks the theme of a light/dark pair and the appearance of `system`. When automatic mode is active, Pi changes themes when the terminal reports an appearance change. Theme names cannot contain `/` because Pi reserves it for this setting format.\n\nUse `--use-theme` to choose the initial theme for one invocation without changing the saved setting:\n\n```bash\npi --use-theme light\npi --use-theme light/dark\n```\n\nSee [CLI resources](cli.md#resources) for the command-line option.\n\n## Create a custom theme\n\nCopy one of the [built-in themes](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/src/modes/interactive/theme) or create a new JSON file conforming to the [schema](https://pi.dev/schemas/theme.schema.json). The built-in themes use OKHSL colors, with variables for colors that several roles share, so you can adjust a hue, saturation, or lightness directly.\n\n1. Save the file as `<agent-dir>/themes/my-theme.json`. The agent directory defaults to `~/.pi/agent`.\n2. Set its `name` to `my-theme`.\n3. Change values in `vars` and `colors`.\n4. Select `my-theme` through `/settings`.\n\nUse the theme name as the filename. Pi hot-reloads the active user theme only from `<agent-dir>/themes/<name>.json`. Run `/reload` after adding or changing a theme from any other source.\n\n## Understand the theme file\n\n| Property | Required | Responsibility |\n|---|---|---|\n| `$schema` | No | Enables editor validation and completion against Pi's published schema. |\n| `name` | Yes | Identifies the theme in selectors and settings. It must be unique, cannot contain `/`, and cannot be `system`. |\n| `appearance` | No | `\"dark\"` or `\"light\"`: the background the theme is designed for. Pi detects it from the theme colors when omitted. |\n| `vars` | No | Defines reusable color values. Variables can reference other variables. |\n| `colors` | Yes | Assigns colors to terminal UI roles. The schema identifies required and optional roles. |\n| `export` | No | Overrides page and panel backgrounds in HTML exports. |\n\nTheme objects are strict: only documented top-level fields and color tokens are accepted. Define reusable custom colors under `vars`; custom keys under `colors` or `export` and additional top-level metadata are rejected.\n\nA color can be written in six forms:\n\n| Form | Example | Meaning |\n|---|---|---|\n| RGB hexadecimal | `\"#0af\"` or `\"#00aaff\"` | A three- or six-digit sRGB color. |\n| OKLCH | `\"oklch(62% 0.1 200)\"` | Perceptual lightness, chroma, and hue. |\n| OKHSL | `\"okhsl(250 60% 55%)\"` | Hue, saturation, and lightness. Saturation is relative to the most the sRGB gamut allows at that hue and lightness, so every value is in gamut and equal saturation looks equally colorful. |\n| 256-color index | `39` | An ANSI palette index from `0` through `255`. |\n| Variable reference | `\"primary\"` | The value of an entry in `vars`. |\n| Terminal default | `\"\"` | The terminal's default foreground or background color. |\n\nTerminal default colors render as the terminal's own colors. Where Pi needs a concrete value, such as HTML export or extension color math, it uses the default colors the terminal reports, or a black or white guess based on the theme's appearance.\n\nPi resolves chained variable references. A missing variable or circular reference makes the theme invalid. Pi uses truecolor when available, gamut-maps OKLCH to sRGB, and approximates colors for 256-color terminals. HTML exports convert OKHSL values to hexadecimal because CSS does not support them. If colors differ from their source values, check your terminal's truecolor detection and contrast settings. See [Configure Your Terminal](terminal-setup.md#override-detected-capabilities).\n\nUse the [theme JSON schema](https://pi.dev/schemas/theme.schema.json) for the exact properties, required colors, and accepted value types.\n\nPi reports invalid theme files during startup and `/reload`.\n\n## Find the color to change\n\nTheme colors describe interface roles rather than individual components. Use these groups to find the relevant part of the schema:\n\n| Area | Color names |\n|---|---|\n| General interface | `accent`, `border*`, `text`, `muted`, `dim`, `success`, `error`, `warning` |\n| Selection and fullscreen | `selectedBg`, `searchMatch*`, `scrollbar*` |\n| Messages | `userMessage*`, `customMessage*`, `thinkingText` |\n| Tool execution | `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`, `toolTitle`, `toolOutput` |\n| Markdown | `md*` |\n| Tool diffs | `toolDiff*` |\n| Syntax highlighting | `syntax*` |\n| Editor modes | `thinking*`, `bashMode` |\n| HTML export | `export.pageBg`, `export.cardBg`, `export.infoBg` |\n\nThe schema is the format reference. The built-in themes provide complete values that you can copy and adjust.\n\nFive colors are optional and inherit another color when omitted:\n\n| Optional color | Fallback |\n|---|---|\n| `scrollbarTrack` | `muted` |\n| `scrollbarThumb` | `text` |\n| `searchMatchBg` | `selectedBg` |\n| `searchMatchText` | `text` |\n| `thinkingMax` | `thinkingXhigh` |\n\nIf `export` colors are omitted, Pi derives HTML page and panel backgrounds from `userMessageBg`.\n\n## Load a theme from a project or package\n\nPlace a project theme in `.pi/themes/`. Project themes load only after [project trust](security.md#understand-project-trust) is granted.\n\nYou can also load theme files and directories through the `themes` setting or distribute them in a Pi package. See [Configuration](configuration.md), [Settings](settings.md#resources), and [Pi Packages](packages.md).\n\nEach loaded theme must have a unique name. Pi reports duplicate names as resource collisions.\n","packages/coding-agent/docs/tmux.md":"# Run Pi in tmux\n\nPi works inside tmux, but tmux can report `Shift+Enter`, `Ctrl+Enter`, and plain `Enter` as the same key. Enable extended keys so Pi can distinguish them.\n\n## Check your tmux version\n\n```bash\ntmux -V\n```\n\nFor tmux 3.5 or newer, use the recommended CSI-u configuration below. For tmux 3.2 through 3.4, use the older-version configuration.\n\n## Enable extended keys in tmux 3.5 or newer\n\nAdd these lines to `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nPi requests extended-key reporting when the terminal does not provide the Kitty keyboard protocol directly. CSI-u is the most reliable format for forwarding modified keys through tmux.\n\n## Restart tmux\n\nThe configuration applies to the tmux server. To guarantee that it is active, close your tmux sessions and start a new server.\n\nIf you choose to stop the server from the command line, save your work first. This command terminates every session managed by that server:\n\n```bash\ntmux kill-server\ntmux\n```\n\n## Verify modified keys\n\nStart Pi inside the new tmux session and check that:\n\n1. `Shift+Enter` inserts a new line in the editor.\n2. `Enter` submits the prompt.\n3. `Alt+Enter` queues a follow-up on macOS and Linux. Windows and WSL use `Ctrl+Q` by default.\n\nIf these keys still behave like plain `Enter`, verify that the terminal outside tmux can report modified keys. See [Configure your terminal](terminal-setup.md).\n\n## Use tmux 3.2 through 3.4\n\nThese versions support extended keys but not `extended-keys-format csi-u`. Add only:\n\n```tmux\nset -g extended-keys on\n```\n\nPi supports the xterm `modifyOtherKeys` format used by these versions. Restart tmux and repeat the verification steps.\n\nFor older versions, upgrade tmux or use Pi outside tmux rather than relying on modified Enter shortcuts.\n","packages/coding-agent/docs/tui.md":"# Terminal UI\n\n`@earendil-works/pi-tui` provides the terminal component system used by Pi. Extensions use it when built-in dialogs, notifications, status text, and widgets are not enough for the interaction they need.\n\nStart with `ctx.ui` methods from an [extension](extensions.md#interact-with-the-user). Build a custom component only when the UI needs its own rendering, keyboard or mouse input, focus, layout, or lifecycle.\n\n## Choose an integration point\n\n| Need | Use |\n|---|---|\n| Select, confirm, input, or multi-line editor | `ctx.ui.select()`, `confirm()`, `input()`, or `editor()` |\n| Non-blocking feedback | `ctx.ui.notify()` or `setStatus()` |\n| Persistent content near the editor | `ctx.ui.setWidget()` |\n| Replace the header, footer, or editor | The corresponding `ctx.ui` component factory |\n| Temporary interactive screen or overlay | `ctx.ui.custom()` |\n| Custom rendering for a tool or session entry | An extension renderer |\n\nThese APIs receive Pi’s active theme and keybindings where needed. Do not create a second terminal renderer inside an extension.\n\n## Understand the component model\n\nA component renders an array of terminal lines for an available width. It can optionally handle keyboard and mouse input, and it must invalidate cached output when its state or theme-dependent content changes.\n\nEvery rendered line must fit within the supplied width. Measure visible terminal columns rather than string length because ANSI escapes, wide characters, emoji, and combining characters change display width.\n\nUse `visibleWidth()`, `truncateToWidth()`, `sliceByColumn()`, and `wrapTextWithAnsi()` instead of implementing terminal-width handling yourself. Pi resets styling and hyperlinks after every line, so reapply styles on each rendered line.\n\nAfter changing component state, invalidate the affected component and call the injected `tui.requestRender()`. The TUI coalesces render requests and updates the terminal.\n\n## Compose built-in components\n\nThe package includes components for common layouts and controls:\n\n- `Text`, `Markdown`, `Image`, and `TruncatedText` render content.\n- `Container`, `VStack`, `HStack`, `Box`, and `Spacer` compose layouts.\n- `Input` and `Editor` accept text.\n- `SelectList` and `SettingsList` implement searchable selection and settings flows.\n- `ScrollView` provides a bounded scrollable viewport.\n- `Loader` and `CancellableLoader` report ongoing work.\n- `MouseRegion` adds pointer behavior around another component.\n\nPrefer these components over rebuilding selection, scrolling, text editing, or width handling. The extension examples show how to combine them with Pi’s borders and themes.\n\n## Handle keyboard input and focus\n\nUse `matchesKey()` and `Key` for terminal keyboard input. The parser accounts for supported terminal protocols and key modifiers. Extension components should use the injected `KeybindingsManager` for configurable application actions.\n\nA component that displays a text cursor should implement `Focusable` and place `CURSOR_MARKER` immediately before its visual cursor. Wrap the cursor cell in `renderFakeCursor()` so the TUI draws it in reverse video, or hides it after `CURSOR_MARKER` when the hardware cursor is shown. The TUI uses the marker to position the hardware cursor for input method editors.\n\nContainers that wrap an `Input` or `Editor` must propagate their `focused` state to that child. Without propagation, Chinese, Japanese, Korean, and other IME candidate windows can appear at the wrong screen position.\n\nExtend Pi’s `CustomEditor` when replacing the main editor. It preserves application shortcuts and agent controls.\n\nForward keys your editor does not own to the base implementation, and restore the default by clearing the custom editor factory.\n\n## Handle mouse input\n\nFullscreen mode routes normalized mouse events to components. A handler can mark an event handled, capture a drag sequence, request focus, or request a render.\n\nUnhandled wheel events scroll the nearest `ScrollView`. Unhandled primary-button drags remain available for transcript selection. OSC 8 links take precedence over enclosing click regions.\n\nRegular mode leaves mouse input to the terminal because the terminal owns scrollback. Design every interaction with a keyboard path even when fullscreen mouse input is available.\n\n## Use custom screens and overlays\n\n`ctx.ui.custom()` temporarily gives one component control of the interactive area and resolves when that component calls the supplied completion callback.\n\n### Build a minimal custom screen\n\nThis complete extension shows the custom-component lifecycle: render within the supplied width, use injected keybindings, invalidate cached output after state changes, request a render, and finish through `done()`.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth } from \"@earendil-works/pi-tui\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"pick-target\", {\n    description: \"Pick a deployment target\",\n    handler: async (_args, ctx) => {\n      if (ctx.mode !== \"tui\") {\n        ctx.ui.notify(\"pick-target requires interactive mode\", \"error\");\n        return;\n      }\n\n      const choices = [\"development\", \"staging\", \"production\"];\n      const selected = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {\n        let selectedIndex = 0;\n        let cachedWidth: number | undefined;\n        let cachedLines: string[] | undefined;\n\n        const invalidate = () => {\n          cachedWidth = undefined;\n          cachedLines = undefined;\n        };\n\n        return {\n          render(width: number): string[] {\n            if (cachedLines && cachedWidth === width) return cachedLines;\n\n            cachedWidth = width;\n            cachedLines = [\n              theme.fg(\"accent\", \"Choose a deployment target\"),\n              ...choices.map((choice, index) => `${index === selectedIndex ? \"> \" : \"  \"}${choice}`),\n              theme.fg(\"dim\", \"up/down move, enter selects, escape cancels\"),\n            ].map((line) => truncateToWidth(line, width));\n            return cachedLines;\n          },\n          handleInput(data: string): void {\n            if (keybindings.matches(data, \"tui.select.up\")) {\n              selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;\n            } else if (keybindings.matches(data, \"tui.select.down\")) {\n              selectedIndex = (selectedIndex + 1) % choices.length;\n            } else if (keybindings.matches(data, \"tui.select.confirm\")) {\n              done(choices[selectedIndex]);\n              return;\n            } else if (keybindings.matches(data, \"tui.select.cancel\")) {\n              done(undefined);\n              return;\n            } else {\n              return;\n            }\n\n            invalidate();\n            tui.requestRender();\n          },\n          invalidate,\n        };\n      });\n\n      if (selected) ctx.ui.notify(`Selected ${selected}`, \"info\");\n    },\n  });\n}\n```\n\nThis component does not display a text cursor, so it does not need `Focusable`. A component containing `Input` or `Editor` must implement `Focusable` and propagate its `focused` value to that child.\n\nPass `overlay: true` to draw above existing content. Overlay options control size, anchors, offsets, margins, and responsive visibility. An overlay handle can change focus or temporarily hide and show the overlay with `setHidden()` while the interaction remains active.\n\nFocused overlays retain input ownership across ordinary renders. If another component should receive input while an overlay remains visible, explicitly release or redirect focus through the handle.\n\nTreat each custom component instance as belonging to one interaction. Create a new instance when starting that interaction again.\n\nFinish the interaction with the completion callback supplied to the component factory. It resolves the `ctx.ui.custom()` promise and disposes the component. Do not call `OverlayHandle.hide()` on an overlay created by `ctx.ui.custom()`.\n\nSee [`overlay-qa-tests.ts`](../examples/extensions/overlay-qa-tests.ts) for positioning, stacking, focus, responsive visibility, and animation behavior.\n\n## Apply themes correctly\n\nUse the theme passed to the extension or component callback. Theme helpers produce ANSI-styled strings for semantic colors such as accent, muted text, success, warnings, errors, tool output, and Markdown.\n\nUse `theme.style()` to combine foreground and background colors with text attributes:\n\n```typescript\nreturn new Text(\n  theme.style(\"Done!\", {\n    fg: \"success\",\n    bg: \"toolSuccessBg\",\n    bold: true,\n  }),\n  0,\n  0,\n);\n```\n\nA style color can be a semantic theme token or a concrete `Color`. Foreground tokens are accepted as `fg` and background tokens as `bg`; to use a token's color in the other position, pass its concrete color, for example `{ fg: theme.colors.userMessageBg }`. Access concrete colors through `theme.colors` and use utilities such as `mixColors()` from `@earendil-works/pi-tui` when color math is needed. Tokens that a theme sets to the terminal default render with the terminal's own color; `theme.colors` reports the color the terminal announced for them, or a guess when it did not. Use `theme.appearance` (`\"dark\"` or `\"light\"`) to decide, for example, whether to lighten or darken a color. Pi converts the result to truecolor or 256-color output based on terminal capabilities. Theme tokens are converted once per theme; compute concrete colors outside the render path when possible.\n\nThe existing `theme.fg()` and `theme.bg()` helpers remain available for applying one semantic color.\n\nDo not permanently store strings with theme colors unless `invalidate()` rebuilds them. A theme change clears render caches, but it cannot remove old ANSI colors embedded in application state.\n\nTheme callbacks evaluated during rendering do not need special rebuilding. Stateless components can also calculate themed output on every render.\n\nUse [Themes](themes.md) to create terminal palettes. Use Pi’s `getMarkdownTheme()` when rendering Markdown that should match the active application theme.\n\n## Keep rendering responsive\n\nRendering runs on the interactive path. Cache expensive layout and highlighting work by width and content, then clear that cache from `invalidate()`.\n\nKeep the default view compact and reveal detail through expansion or a dedicated screen. For custom tool rendering, handle partial results and reuse the previous component when it can be updated safely.\n\nUse `PI_TUI_WRITE_LOG` to capture the raw ANSI stream when diagnosing rendering problems. Test narrow widths, wide characters, resize events, theme changes, focus transitions, and both regular and fullscreen modes.\n\n## Examples and source\n\nThe checked extension examples cover the main patterns:\n\n- [`preset.ts`](../examples/extensions/preset.ts) and [`tools.ts`](../examples/extensions/tools.ts) use selection and settings lists.\n- [`qna.ts`](../examples/extensions/qna.ts) uses cancellable asynchronous UI.\n- [`modal-editor.ts`](../examples/extensions/modal-editor.ts) replaces the editor.\n- [`custom-footer.ts`](../examples/extensions/custom-footer.ts) replaces the footer.\n- [`widget-placement.ts`](../examples/extensions/widget-placement.ts) places persistent content around the editor.\n- [`doom-overlay/`](../examples/extensions/doom-overlay/) demonstrates a continuously rendered overlay.\n\nThe public exports are defined in [`packages/tui/src/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/tui/src/index.ts). See [Extensions](extensions.md) for extension lifecycle, state, tools, events, and mode behavior.\n","packages/coding-agent/docs/usage.md":"# Use Pi in the terminal\n\nRun `pi` from the folder you want to work in. Pi uses that folder to discover files, instructions, and configuration, and to group saved sessions. If you have not installed Pi or chosen a model yet, follow the [Quickstart](quickstart.md).\n\nPi may ask whether you trust the working folder before loading its project resources. See [Project trust](security.md#understand-project-trust).\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Pi interactive mode showing a conversation, editor, and status information\" width=\"750\"></p>\n\nThe transcript shows your prompts, Pi's responses, tool calls, results, and errors. You write prompts and commands in the editor. The footer shows the current folder, session, model, context usage, and accumulated usage and cost.\n\n## Enter a prompt\n\nType a request and press `Enter` to send it. Use `Shift+Enter` to add a line, or press `Ctrl+G` to work on a longer prompt in your configured external editor.\n\nTo include files or images:\n\n- Type `@` to search for a file and add it to your prompt.\n- Press `Tab` to complete a path.\n- Paste an image or drag it into a compatible terminal.\n\n## Follow Pi's work\n\nPi shows each tool call and result while it works. Press `Ctrl+O` to expand or collapse tool output. Press `Ctrl+T` to show or hide thinking blocks.\n\nThe startup header lists the instructions and resources Pi loaded. The editor border indicates the current thinking level. The footer updates as the model uses context and reports usage.\n\nPi does not ask before every tool call. Review commands and changed files, and use a sandbox for untrusted or unattended work. See [Security](security.md).\n\n## Change direction\n\nYou can send more input while Pi is working:\n\n| What you want | Action |\n|---|---|\n| Adjust the current task | Type a message and press `Enter` |\n| Add work after the current task | Type a message and press `Alt+Enter` |\n| Return queued messages to the editor | Press `Alt+Up` |\n| Stop the current task | Press `Escape` |\n\nA message sent with `Enter` waits until the current response and its tool calls finish, then guides the next response. A follow-up sent with `Alt+Enter` waits until Pi finishes the current task. Aborting returns queued messages to the editor.\n\nWindows Terminal reserves some Alt shortcuts. See [Terminal Setup](terminal-setup.md) for the Windows alternatives.\n\n## Change the model or settings\n\nType `/` to search the available commands. The commands you will use most often are:\n\n- `/model` selects a model. Press `Ctrl+L` to open the same selector.\n- `/thinking` selects how much reasoning the current model uses. Press `Shift+Tab` to cycle through supported levels.\n- `/login` and `/logout` manage provider access.\n- `/settings` changes common preferences.\n\nPrompt templates, skills, and extensions can add more commands to the same menu. See [Choose a Model](models.md), [Configuration](configuration.md), or the complete [Slash Commands reference](slash-commands.md).\n\n## Continue or start over\n\nPi saves sessions automatically unless session persistence is disabled.\n\n- `/new` starts a new session.\n- `/resume` opens another saved session.\n- `/name` gives the current session a recognizable name.\n- `/session` shows its file, ID, message count, token usage, and cost.\n\nUse `/tree`, `/fork`, or `/clone` when you want to explore another approach without losing existing work. Use `/compact` to reduce the conversation history sent to the model. See [Sessions and Context](sessions.md) for these workflows.\n\nAfter leaving Pi, run `pi --continue` from the same folder to resume its most recent session.\n\n## Run a terminal command\n\nPrefix a command with `!` to run it and include its output in the conversation:\n\n```text\n!git status\n```\n\nUse `!!` when you want to run a command without sending its output to the model.\n\n## Copy, export, or share results\n\nPress `Ctrl+X` or run `/copy` to copy the last assistant response. Use `/export` to save the session as HTML or JSONL.\n\nUse `/share` to upload the session and get a viewer link. By default, Radius authentication makes the artifact visible to your Radius organization; otherwise, Pi creates a private GitHub gist through the GitHub CLI. The [`sessionShareDestination`](settings.md#sessions-and-context) setting can force either destination. Review the session first because it can contain prompts, tool output, file contents, and credentials exposed during the conversation.\n\n## Adjust the terminal\n\nFullscreen mode, the default, keeps the editor and status area fixed while the transcript scrolls within the terminal window. Regular mode uses the terminal's normal scrollback. Choose a mode through `/settings` or `--tui-mode`.\n\nTerminal support for mouse input, keyboard shortcuts, and inline images varies. See [Terminal Setup](terminal-setup.md) for platform-specific configuration and [Keybindings](keybindings.md) for every configurable shortcut. Run `/hotkeys` to inspect the shortcuts active in your current session.\n\n## Collect diagnostic information\n\nWhen troubleshooting terminal rendering or conversation state, run `/debug`. Pi writes the rendered terminal lines and current session messages to `pi-debug.log` in your [agent directory](configuration.md#agent-directory).\n\nReview this file before sharing it. It can contain prompts, model responses, tool output, file contents, and terminal data.\n","packages/coding-agent/docs/virtual-models.md":"# Virtual Models\n\nA virtual model is a selectable model that picks a physical model for each request. Use one to route by task, cost, or conversation state. For example, a router can send quick questions to a small model and hard problems to a large one, while the user selects a single model.\n\nRegister virtual models from an [extension](extensions.md). They appear in `/model`, `--model`, scoped models, and settings like any other model. A virtual model can be listed under any provider, including one with physical models, such as `openai-codex/auto`.\n\n## Selection and dispatch\n\nA virtual model selects a model and a thinking level. A router maps that pair to a physical pair for each request:\n\n```\nselected (virtual model, virtual level)  ->  dispatched (physical model, physical level)\njev/auto:low                             ->  anthropic/claude-sonnet-4-5:high\n```\n\nThe virtual thinking level is an input to the router. Its meaning is up to the router; it need not correspond to a reasoning budget.\n\nPi keeps the two pairs apart:\n\n| | Selection | Dispatch |\n|---|---|---|\n| Recorded in | `model_change` and `thinking_level_change` entries | Each assistant message: `provider`, `api`, `model`, `thinkingLevel` |\n| Visible as | `ctx.model`, `ctx.thinkingLevel`, `PI_MODEL`, `PI_REASONING_LEVEL`, `/model` | The assistant message of each response |\n\nProviders only receive physical models. Assistant messages name the physical model, so replaying a conversation across different physical models works the same as after a manual model switch. Resuming a session restores the virtual selection from its latest `model_change` entry. If the virtual model is no longer registered, Pi falls back to the physical model that answered last.\n\nIn interactive mode, the footer shows the routed model next to the selection, for example `auto • high → gpt-5.6-luna • medium`. `/session` lists the cost for each physical model.\n\nContext usage uses the limits of the physical model that produced the latest response, even if that response came before switching to the virtual model. Without such a response, it uses the limits declared on the virtual model, if any. Compaction checks the same limits, and again the limits of the model each request is routed to. If that model's context window is too small for the conversation, Pi compacts before sending the request; the route stays as the router chose it.\n\n## Register a virtual model\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerVirtualModel({\n    provider: \"router\",\n    id: \"auto\",\n    name: \"Auto\",\n    thinkingLevels: [\"low\", \"high\"],\n    route(request, ctx) {\n      // Tool follow-ups and retries stay on the model that handled the turn.\n      const sticky = request.failed ?? request.previous;\n      if (request.reason !== \"user\" && sticky) {\n        return { model: sticky.model, thinkingLevel: sticky.thinkingLevel ?? \"medium\" };\n      }\n      const id = request.thinkingLevel === \"high\" ? \"claude-sonnet-4-5\" : \"claude-haiku-4-5\";\n      return { model: ctx.modelRegistry.find(\"anthropic\", id)!, thinkingLevel: \"medium\" };\n    },\n  });\n}\n```\n\n- `provider` is the provider the model is listed under. It can be any provider ID. A provider can list several virtual models next to its physical ones. On a physical provider, the virtual model is available when that provider has credentials. Under an ID that no provider uses, it is always available.\n- `id` must not be the ID of a physical model of that provider. If a catalog refresh later adds a physical model with the same ID, the virtual model hides it.\n- `thinkingLevels` lists the levels offered for selection. It defaults to `[\"off\"]`.\n- `contextWindow` and `maxTokens` are shown before the first response. Unset limits are unknown.\n- `input` lists the input types offered for selection. It defaults to text and images; physical models without image support receive placeholders.\n\nRegistration follows the same queuing and reload rules as `pi.registerProvider()`. Registering the same provider and ID again replaces the virtual model. `pi.unregisterVirtualModel(provider, id)` removes it; `pi.unregisterProvider()` does not. SDK code can register one without an extension: `modelRuntime.registerVirtualModel(definition)`.\n\n## Route requests\n\n`route(request, ctx)` runs before every request made with the virtual model and returns `{ model, thinkingLevel }`. The model can be any physical model in the catalog whose provider has credentials; look it up with `ctx.modelRegistry`. A virtual model cannot route to another virtual model. Pi clamps the thinking level to the returned model.\n\n| Field | Meaning |\n|---|---|\n| `model`, `thinkingLevel` | The selected virtual model and level |\n| `reason` | Why the request is made, see below |\n| `previous` | Physical model and thinking level of the latest successful response in `messages` |\n| `failed` | For `retry`: physical model, thinking level, and assistant `message` of the failed request, which `messages` no longer contains. The message carries `stopReason` and `errorMessage`. Absent when routing itself failed |\n| `state` | Router state last returned on this session branch, see below |\n| `messages` | The conversation for this request, including system messages |\n| `signal` | Abort signal of the request |\n\n| `reason` | Request |\n|---|---|\n| `user` | First request after a message the user wrote, including steering and follow-up messages |\n| `continuation` | Any other request in the agent loop, such as after tool results or extension messages |\n| `retry` | Automatic retry after a failed request, including after compaction for a context overflow |\n| `direct` | Request made outside the agent loop, such as a compaction summary or an extension calling `ctx.modelRegistry.streamSimple()` |\n\nReturning `previous` for `continuation` and `failed` for `retry` keeps prompt caches and thinking signatures valid. Switching models between turns is allowed but loses the prompt cache. A retry can also switch to another model, for example when `failed.message.errorMessage` reports that a provider is overloaded or the context overflowed.\n\nIf `route()` throws, or returns a virtual model or a model without credentials, the request ends with an error response.\n\n## Keep routing state\n\n`route()` can return `state` next to the model. Pi stores it on the session branch and passes it back as `request.state` on later requests. Use it for decisions the transcript does not record, such as classifier results or a routing phase:\n\n```typescript\npi.registerVirtualModel<{ phase: \"plan\" | \"build\" }>({\n  provider: \"router\",\n  id: \"phased\",\n  name: \"Phased\",\n  route(request, ctx) {\n    const state = request.state ?? { phase: \"plan\" };\n    const id = state.phase === \"plan\" ? \"claude-opus-4-5\" : \"claude-haiku-4-5\";\n    return { model: ctx.modelRegistry.find(\"anthropic\", id)!, thinkingLevel: \"medium\", state };\n  },\n});\n```\n\n- State must be JSON-serializable. Returning `undefined` or `request.state` itself keeps the current state.\n- Pi stores any other returned object as new state, before the request is sent, even when it equals the current state. Return a new object only when the state changes. The state stays stored if the request later fails.\n- State follows the session tree, so forks and `/tree` navigation see the state of their branch. It survives compaction.\n- `direct` requests have no state, and Pi ignores state they return.\n\nThe transcript already records the selection and every dispatched model, and `ctx.sessionManager.getBranch()` exposes both.\n\nRouters can call other models through `ctx.modelRegistry`, for example `ctx.modelRegistry.classify()` with a classifier model from `ctx.modelRegistry.findOfType(\"classifier\", provider, id)`. The call adds latency before the first token of the turn.\n\nSee [`jev-router.ts`](../examples/extensions/jev-router.ts) for a complete router. It plans on a strong OpenAI Codex model chosen by the Jev classifier, lets that model make the first edit, and then switches once to a cheaper model, accepting a single prompt-cache miss. It keeps the phase as router state.\n","packages/coding-agent/docs/windows.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fdocs%2Fwindows.md -->","packages/coding-agent/examples/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2FREADME.md -->","packages/coding-agent/examples/extensions/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2FREADME.md -->","packages/coding-agent/examples/extensions/doom-overlay/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fdoom-overlay%2FREADME.md -->","packages/coding-agent/examples/extensions/dynamic-resources/SKILL.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fdynamic-resources%2FSKILL.md -->","packages/coding-agent/examples/extensions/plan-mode/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fplan-mode%2FREADME.md -->","packages/coding-agent/examples/extensions/subagent/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fsubagent%2FREADME.md -->","packages/coding-agent/examples/sdk/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fsdk%2FREADME.md -->","packages/coding-agent/test/mcp-conformance/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Ftest%2Fmcp-conformance%2FREADME.md -->","packages/coding-agent/test/suite/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Ftest%2Fsuite%2FREADME.md -->","packages/durable/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fdurable%2FREADME.md -->","packages/durable/docs/spec.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fdurable%2Fdocs%2Fspec.md -->","packages/env/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2FREADME.md -->","packages/env/docs/protocol.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2Fdocs%2Fprotocol.md -->","packages/env/docs/semantics.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2Fdocs%2Fsemantics.md -->","packages/evals/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fevals%2FREADME.md -->","packages/mcp/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fmcp%2FREADME.md -->","packages/telemetry/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftelemetry%2FREADME.md -->","packages/tui/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2FREADME.md -->","packages/tui/native/darwin/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Fdarwin%2FREADME.md -->","packages/tui/native/linux/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Flinux%2FREADME.md -->","packages/tui/native/win32/README.md":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Fwin32%2FREADME.md -->"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Development Rules\n\n## Conversational Style\n\n- Keep answers short and concise\n- No emojis in commits, issues, PR comments, or code\n- No fluff or cheerful filler text (e.g., \"Thanks @user\" not \"Thanks so much @user!\")\n- Technical prose only, be direct\n- Use concise, clear, simple language. Define unavoidable jargon before using it.\n- Explain non-trivial designs and problems as: problem, concrete example or short trace, then solution. State why the solution is necessary and distinguish it from optional complexity.\n- Prefer concrete behavior and small illustrations over abstract summaries, dense terminology, or unexplained lists of changes.\n- When the user asks a question, answer it first before making edits or running implementation commands.\n- When responding to user feedback or an analysis, explicitly say whether you agree or disagree before saying what you changed.\n\n## Code Quality\n\n- Read files in full before wide-ranging changes, before editing files you have not fully inspected, and when asked to investigate or audit. Do not rely on search snippets for broad changes.\n- No `any` unless absolutely necessary.\n- Inline single-line helpers that have only one call site.\n- Check node_modules for external API types; don't guess.\n- **No inline imports** (`await import()`, `import(\"pkg\").Type`, dynamic type imports). Top-level imports only.\n- In `packages/coding-agent`, resolve package assets through helpers in `src/config.ts`. Do not use `__dirname` directly; the helpers account for source checkouts, npm installations, and standalone binaries.\n- Never remove or downgrade code to fix type errors from outdated deps; upgrade the dep instead.\n- Use only erasable TypeScript syntax (Node strip-only mode) in code checked by the root config (`packages/*/src`, `packages/*/test`, `packages/coding-agent/examples`): no parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other constructs needing JS emit. Use explicit fields with constructor assignments.\n- Always ask before removing functionality or code that appears intentional.\n- Do not preserve backward compatibility unless the user asks for it.\n- Never hardcode key checks (e.g. `matchesKey(keyData, \"ctrl+x\")`). Add defaults to `DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS` so they stay configurable.\n- Never modify `packages/ai/src/models.generated.ts` directly; update `packages/ai/scripts/generate-models.ts` instead, then regenerate. Including the resulting `models.generated.ts` diff is always OK, even if regeneration includes unrelated upstream model metadata changes. The generated model data in `packages/ai/src/providers/data/` is gitignored: never stage it, and don't look for it in git history.\n\n## Commands\n\n- After code changes (not docs): `npm run check` (full output, no tail). Fix all errors, warnings, and infos before committing. Does not run tests.\n- Never run `npm run build` or `npm test` unless requested by the user.\n- Never run the full vitest suite directly: it includes e2e tests that activate when endpoint/auth env vars are present. For all non-e2e tests, run `./test.sh` from the repo root. Otherwise run specific tests from the package root:\n  - Vitest: `node \"$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js\" --run test/specific.test.ts`\n  - `packages/tui` (`node:test`): `node --test test/specific.test.ts`\n- If you create or modify a test file, run it and iterate on test or implementation until it passes.\n- For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` + the faux provider. No real provider APIs, keys, or paid tokens.\n- When regressions tests for fixing a github issue, add a comment with the github issue number next to the test.\n- For ad-hoc scripts, `write` them to a temp file (e.g. `/tmp`), run, edit if needed, remove when done. Don't embed multi-line scripts in `bash` commands.\n- Never commit unless the user asks.\n\n## Dependency and Install Security\n\n- Treat npm dep and lockfile changes as reviewed code. Direct registry deps, including internal workspace deps, stay pinned to exact versions.\n- When updating `undici`, you MUST read its changelog/release notes for the target version and evaluate whether any changes may affect functionality before applying the update.\n- Hydrate/update locally with `npm install --ignore-scripts`; clean/CI-style with `npm ci --ignore-scripts`. Don't run lifecycle scripts unless the user asks.\n- If dep metadata changes, refresh `package-lock.json` with `npm install --package-lock-only --ignore-scripts`.\n- If `packages/coding-agent/install-lock/` needs regen, run `node scripts/generate-coding-agent-install-lock.mjs` (verify with `--check` or `npm run check`). New deps with lifecycle scripts require review and an explicit allowlist entry in that script; never add one silently.\n- Pre-commit blocks lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1`. Don't bypass unless the user wants the lockfile change committed.\n\n## Git\n\nMultiple pi sessions may be running in this cwd at the same time, each modifying different files. Git operations that touch unstaged, staged, or untracked files outside your own changes will stomp on other sessions' work. Follow these rules:\n\nCommitting:\n\n- Only commit files YOU changed in THIS session.\n- Stage explicit paths (`git add <path1> <path2>`); never `git add -A` / `git add .`.\n- Before committing, run `git status` and verify you are only staging your files.\n- `packages/ai/src/models.generated.ts` may always be included alongside your files.\n- Message format: `{feat,fix,docs}[(ai,tui,agent,coding-agent)]: <commit message> (optionally multiple lines)`. Message is informative and concise.\n\nNever run (destroys other agents' work or bypasses checks):\n\n- `git reset --hard`, `git checkout .`, `git clean -fd`, `git stash`, `git add -A`, `git add .`, `git commit --no-verify`.\n\nIf rebase conflicts occur:\n\n- Resolve conflicts only in files you modified.\n- If a conflict is in a file you did not modify, abort and ask the user.\n- Never force push.\n\n## Issues and PRs\n\nSee `CONTRIBUTING.md` for the contributor gate (auto-close workflows, `lgtm`/`lgtmi`, quality bar).\n\nWhen reviewing PRs:\n\n- Do not run `gh pr checkout`, `git switch`, or otherwise move the worktree to the PR branch unless the user explicitly asks.\n- Use `gh pr view`, `gh pr diff`, `gh api`, and local `git show`/`git diff` against fetched refs to inspect PR metadata, commits, and patches without changing branches.\n- If you need PR file contents, fetch/read them into temporary files or use `git show <ref>:<path>` without switching branches.\n\nWhen creating issues:\n\n- Add `pkg:*` labels for affected packages (`pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui`); use all that apply.\n\nWhen posting issue/PR comments:\n\n- Write the comment to a temp file and post with `gh issue/pr comment --body-file` (never multi-line markdown via `--body`).\n- Keep comments concise, technical, in the user's tone.\n- End every AI-posted comment with the AI-generated disclaimer line specified by the originating prompt (e.g. `This comment is AI-generated by `/wr``).\n\nWhen closing issues via commit:\n\n- Include `fixes #<number>` or `closes #<number>` in the message so merging auto-closes the issue. For multiple issues, repeat the keyword per issue (`closes #1, closes #2`); a shared keyword (`closes #1, #2`) only closes the first.\n\n## Testing pi Interactive Mode with tmux\n\nFor testing pi's interactive mode, load and follow [.pi/skills/interactive-testing.md](.pi/skills/interactive-testing.md).\n\n## Changelog\n\nLocation: `packages/*/CHANGELOG.md` (one per package).\n\nSections under `## [Unreleased]`: `### Breaking Changes` (API changes requiring migration), `### Added`, `### Changed`, `### Fixed`, `### Removed`.\n\nRules:\n\n- All new entries go under `## [Unreleased]`. Read the full section first and append to existing subsections; never duplicate them.\n- Released version sections (e.g. `## [0.12.2]`) are immutable; never modify them.\n- Do not create changelog entries when working on a branch other than `main` or pull request\n\nAttribution:\n\n- Internal (from issues): `Fixed foo bar ([#123](https://github.com/earendil-works/pi/issues/123))`\n- External contributions: `Added feature X ([#456](https://github.com/earendil-works/pi/pull/456) by [@username](https://github.com/username))`\n\n## Releasing\n\nFor release preparation, publishing, verification, or recovery, load and follow [.pi/skills/release.md](.pi/skills/release.md).\n\n## User Override\n\nIf the user's instructions conflict with any rule in this document, ask for explicit confirmation before overriding. Only then execute their instructions.\n","isInternal":false,"tokens":1995,"sizeBytes":8648},{"name":"add-llm-provider.md","path":".pi/skills/add-llm-provider.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/.pi/skills/add-llm-provider.md","title":"add-llm-provider","category":"anthropic-skill","format":"markdown","content":"---\nname: add-llm-provider\ndescription: Checklist for adding a new LLM provider to packages/ai. Covers core types, provider implementation, lazy registration, model generation, the full test matrix, coding-agent wiring, and docs.\n---\n\n# Adding a New LLM Provider (packages/ai)\n\nA new provider touches multiple files. Work through these steps in order.\n\n## 1. Core Types (`packages/ai/src/types.ts`)\n\n- Add API identifier to `Api` type union (e.g. `\"bedrock-converse-stream\"`).\n- Create options interface extending `StreamOptions`.\n- Add mapping to `ApiOptionsMap`.\n- Add provider name to `KnownProvider` type union.\n\n## 2. Provider Implementation (`packages/ai/src/providers/`)\n\nCreate a provider file exporting:\n\n- `stream<Provider>()` returning `AssistantMessageEventStream`.\n- `streamSimple<Provider>()` for `SimpleStreamOptions` mapping.\n- Provider-specific options interface.\n- Message/tool conversion functions.\n- Response parsing that emits standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`).\n\n## 3. Provider Exports and Lazy Registration\n\n- Add a package subpath export in `packages/ai/package.json` pointing at `./dist/providers/<provider>.js`.\n- Add `export type` re-exports in `packages/ai/src/index.ts` for provider option types that should remain available from the root entry.\n- Register the provider in `packages/ai/src/providers/register-builtins.ts` via lazy loader wrappers; do not statically import provider implementation modules there.\n- Add credential detection in `packages/ai/src/env-api-keys.ts`.\n\n## 4. Model Generation (`packages/ai/scripts/generate-models.ts`)\n\n- Add logic to fetch/parse models from the provider source.\n- Map to the standardized `Model` interface.\n\n## 5. Tests (`packages/ai/test/`)\n\n- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API impl such as `openai-completions`.\n- Add the provider to the broader matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.\n- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (e.g. GPT and Claude), add at least one pair per family.\n- For non-standard auth, create a utility (e.g. `bedrock-utils.ts`) with credential detection.\n\n## 6. Coding Agent (`packages/coding-agent/`)\n\n- `src/core/model-resolver.ts`: add default model ID to `defaultModelPerProvider`.\n- `src/core/provider-display-names.ts`: add API-key login display name so `/login` and related UI show the provider for built-in API-key auth.\n- `src/cli/args.ts`: add env var documentation.\n- `README.md`: add provider setup instructions.\n- `docs/providers.md`: add setup instructions, env var, and `auth.json` key.\n\n## 7. Documentation\n\n- `packages/ai/README.md`: add to providers table, document options/auth, add env vars.\n- `packages/ai/CHANGELOG.md`: add entry under `## [Unreleased]`.\n","frontmatter":{"name":"add-llm-provider","description":"Checklist for adding a new LLM provider to packages/ai. Covers core types, provider implementation, lazy registration, model generation, the full test matrix, coding-agent wiring, and docs."},"isInternal":false,"tokens":726,"sizeBytes":3097},{"name":"github-work.md","path":".pi/skills/github-work.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/.pi/skills/github-work.md","title":"github-work","category":"anthropic-skill","format":"markdown","content":"---\nname: github-work\ndescription: Work on GitHub issues and pull requests of this repo. Use when asked to investigate, analyze, fix, or review an issue or PR, and when asked to wrap up such work (changelog, comment, commit, push, close).\n---\n\n# Working on GitHub issues and PRs\n\nThe repo's prompt templates in `.pi/prompts/` (two directories above this skill) define the workflow. Read the matching template in full and follow it. Where a template says `$ARGUMENTS` or `$@`, substitute the issue or PR number or URL from the request, and any extra instructions the user gave.\n\n| Request | Template |\n|---|---|\n| Investigate, analyze, or fix an issue | `.pi/prompts/is.md` |\n| Review a pull request | `.pi/prompts/pr.md` |\n| Wrap up issue or PR work: changelog, final comment, commit, push, close | `.pi/prompts/wr.md` |\n\nRules:\n\n- `is.md` analyzes and proposes only. Implement only when the user asks for it, then keep following AGENTS.md (checks, tests, changelog rules).\n- `pr.md` never checks out or switches to the PR branch.\n- Run `wr.md` only when the user asks to wrap up, commit, or push. Post at most one final GitHub comment per session, ending with the exact disclaimer line it specifies.\n- When a request spans several steps, for example \"fix issue X and wrap it up\", read every template involved before starting.\n","frontmatter":{"name":"github-work","description":"Work on GitHub issues and pull requests of this repo. Use when asked to investigate, analyze, fix, or review an issue or PR, and when asked to wrap up such work (changelog, comment, commit, push, close)."},"isInternal":false,"tokens":320,"sizeBytes":1327},{"name":"interactive-testing.md","path":".pi/skills/interactive-testing.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/.pi/skills/interactive-testing.md","title":"interactive-testing","category":"anthropic-skill","format":"markdown","content":"---\nname: interactive-testing\ndescription: Test and debug pi's interactive mode in a controlled tmux terminal. Use for TUI behavior checks and interactive release smoke tests.\n---\n\n# Testing pi Interactive Mode with tmux\n\nRun the TUI in a controlled terminal (from the repo root, two directories above this skill):\n\n```bash\ntmux new-session -d -s pi-test -x 80 -y 24\ntmux send-keys -t pi-test \"./pi-test.sh\" Enter\nsleep 3 && tmux capture-pane -t pi-test -p     # capture after startup\ntmux send-keys -t pi-test \"your prompt here\" Enter\ntmux send-keys -t pi-test Escape               # special keys (also C-o for ctrl+o, etc.)\ntmux kill-session -t pi-test\n```\n\nFor release smoke tests, start the tmux session with `-c /tmp` and replace `./pi-test.sh` with the absolute path to the release binary. Test both Node and Bun binaries separately, submit a prompt, and wait for the model reply; startup alone is not a passing smoke test.\n","frontmatter":{"name":"interactive-testing","description":"Test and debug pi's interactive mode in a controlled tmux terminal. Use for TUI behavior checks and interactive release smoke tests."},"isInternal":false,"tokens":232,"sizeBytes":930},{"name":"release.md","path":".pi/skills/release.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/.pi/skills/release.md","title":"release","category":"anthropic-skill","format":"markdown","content":"---\nname: release\ndescription: Prepare, publish, verify, and recover pi releases. Use for release preparation, local release smoke tests, publishing, and failed release CI or announcements.\n---\n\n# Releasing pi\n\nRun repository commands from the repo root (two directories above this skill), unless instructed otherwise.\n\n**Lockstep versioning**: all packages share one version; every release updates all together. `patch` = fixes + additions, `minor` = breaking changes. No major releases.\n\n1. **Update CHANGELOGs**: ask the user whether they ran the `/cl` prompt on the latest commit on `main`. If not, they must run `/cl` first to audit and update each package's `[Unreleased]` section before releasing.\n\n2. **Local smoke test**: build an unpublished release and smoke test from outside the repo (so it can't resolve workspace files):\n   ```bash\n   npm run release:local -- --out /tmp/pi-local-release --force\n   cd /tmp\n\n   # Node package install smoke tests\n   /tmp/pi-local-release/node/pi --help\n   /tmp/pi-local-release/node/pi --version\n   /tmp/pi-local-release/node/pi --list-models\n   /tmp/pi-local-release/node/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/node/pi\n\n   # Bun binary smoke tests\n   /tmp/pi-local-release/bun/pi --help\n   /tmp/pi-local-release/bun/pi --version\n   /tmp/pi-local-release/bun/pi --list-models\n   /tmp/pi-local-release/bun/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/bun/pi\n   ```\n   Verify both Node and Bun startup, model/account listing, interactive startup, and at least one real prompt with the intended default provider. The bare commands `/tmp/pi-local-release/node/pi` and `/tmp/pi-local-release/bun/pi` start interactive mode; run each in tmux, submit a prompt, and wait for the model reply before considering the interactive smoke test passed. Failures are release blockers unless the user explicitly accepts the risk.\n\n   Load and follow [interactive-testing.md](interactive-testing.md) for the tmux workflow. Start each release binary from `/tmp`, not the repo root.\n\n3. **Run the release script**:\n   ```bash\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:patch    # fixes + additions\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:minor    # breaking changes\n   ```\n   If the test suite already passed in `npm run release:local` and only fails from machine load, `PI_RELEASE_SKIP_TESTS=1` skips `./test.sh` in the release script; checks and the packed install check still run. Use it only with the user's approval.\n\n   Use `npm_config_min_release_age=0` only for the release command. The repo's normal npm age gate can otherwise block the release lockfile refresh when the current workspace package version was published recently. Review any lockfile or install lock diffs the release creates before push.\n\n   The release script pins the newest published model catalog for Nix (`nix/model-catalog.json`) and builds the checkout with it as the Nix build does (it fails if that catalog predates main's model changes; publish one first), bumps all package versions, updates changelogs, regenerates release artifacts, runs `npm run check`, commits `Release vX.Y.Z`, tags `vX.Y.Z`, adds fresh `## [Unreleased]` changelog sections, commits `Add [Unreleased] section for next cycle`, then pushes `main` and the tag. Do not rerun the release script after a tag was pushed.\n\n4. **CI verifies and announces the npm release**: pushing the `vX.Y.Z` tag triggers `.github/workflows/build-binaries.yml`. The `publish-npm` job uses npm trusted publishing through GitHub Actions OIDC with environment `npm-publish`; no local `npm publish`, `npm whoami`, OTP, or WebAuthn flow is required. After publishing, `announce-pi-dev-release` verifies every public workspace package resolves at the exact release version and that its npm tarball is available, then writes the verified release marker to R2. `pi.dev/api/latest-version` reads that marker; it must never announce a release from npm before this job succeeds.\n\n5. **CI builds the Nix release**: the tag push also triggers `.github/workflows/nix.yml`, which builds the tagged flake on Linux and macOS and then fast-forwards the `stable` branch to the release commit. `nix run github:earendil-works/pi/stable` runs the latest release. If the Nix build fails, `stable` stays on the previous release; the tag cannot be fixed, so fix `main` and ship the fix in the next release.\n\n6. **If CI publish or announcement fails**: inspect the failed job. The publish helper is idempotent and skips package versions already present on npm; the announcement job rechecks availability before updating the R2 marker. Rerun the failed job or workflow after fixing CI or transient npm issues. Do not rerun `npm run release:patch` or `npm run release:minor` for the same version.\n","frontmatter":{"name":"release","description":"Prepare, publish, verify, and recover pi releases. Use for release preparation, local release smoke tests, publishing, and failed release CI or announcements."},"isInternal":false,"tokens":1105,"sizeBytes":4812},{"name":"guide.md","path":"packages/chord/docs/guide.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/chord/docs/guide.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Chord usage guide\n\n> Describes the runtime in `src`. The [specification](spec.md) defines its semantics and lists known limitations.\n\nChord builds applications out of **objects** that live in several processes (a server, workers, browser tabs) and can be **hot reloaded** while they run.\n\n- An **object type** is declared by a contract: a name, an input (the object's address) and members. A plugin's\n  **facet** (its code for one host: server, worker, browser) serves object types.\n- An **object** is one instance of an object type: state plus code at an address. It is created on demand and lives\n  while someone references it.\n- Everyone gets a **reference**, never the object. References work the same locally and across processes, and survive reloads.\n- Every object knows its **principal**: an owner and a guest get different objects, with different methods.\n- **State** is observable JSON. Remote viewers get a snapshot followed by deltas.\n- **Hot reload** swaps code while it runs. Work in flight finishes on the code it started with, state is kept, and incompatible changes migrate at a safe point.\n\n## Contents\n\n- [Quick start](#quick-start)\n- [Contracts](#contracts)\n- [Serving objects](#serving-objects)\n- [Declarations](#declarations)\n- [Activation](#activation)\n- [Principals and variants](#principals-and-variants)\n- [References](#references)\n- [State](#state)\n- [Calls, ctx and roots](#calls-ctx-and-roots)\n- [Lifetime](#lifetime)\n- [Registries](#registries)\n- [Plugins](#plugins)\n- [Hot reload](#hot-reload)\n- [Hosts and links](#hosts-and-links)\n- [Durable](#durable)\n- [Errors](#errors)\n- [Rules of thumb](#rules-of-thumb)\n\n## Quick start\n\n```ts\nimport { Type } from \"typebox\";\nimport { createHost, method, remote } from \"@earendil-works/chord\";\n\n// contract.ts: shared by provider and consumers\nexport const Counter = remote(\"demo.counter\", {\n\tversion: 1,\n\tinput: Type.Object({ name: Type.String() }),\n\tvariants: {\n\t\tdefault: {\n\t\t\tstate: { count: Type.Number() },\n\t\t\tmethods: { increment: method(Type.Object({ by: Type.Number() })) },\n\t\t},\n\t},\n});\n\nconst host = createHost({ name: \"server\", self: { kind: \"server\" } });\n\nhost.serve(Counter, ({ name }, _principal, self) => {\n\tconst count = self.state(\"count\", Type.Number(), 0);\n\treturn {\n\t\tvariant: \"default\",\n\t\tcount,\n\t\tincrement: async ({ by }, ctx) => {\n\t\t\tcount.change(ctx, (n) => n + by);\n\t\t},\n\t};\n});\n\nawait host.root(async (ctx) => {\n\tconst clicks = await ctx.get(Counter, { name: \"clicks\" });  // ready, with its state; released when this root ends\n\tclicks.count.subscribe((n) => console.log(\"count\", n));\n\tawait clicks.increment({ by: 2 }, ctx);  // logs 0, then 2\n});\n```\n\n## Contracts\n\nA contract is a runtime value: a name, a version number, and a description of the members. Shipped bundles have no TypeScript, so everything the runtime checks must be in the contract. TypeScript types are inferred from it.\n\n### Remote types\n\nRemote types can be reached from other hosts. Everything that crosses the wire has a schema, and the provider validates it.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),  // lives on `session:S1` for `{ session: \"S1\", … }`\n\tvariants: {\n\t\tviewer: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t},\n\t\tparticipant: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t\tmethods: {\n\t\t\t\tsend: method(SendInputSchema, Type.Object({ submissionId: Type.Number() })),\n\t\t\t\tstop: method(),  // no arguments, no result: `chat.stop(ctx)`\n\t\t\t},\n\t\t},\n\t},\n});\n```\n\n**Where a remote type lives** is part of its contract, so no caller names a host:\n\n- `host: hostNamed(\"server\")`: every object lives on the host named `server`.\n- `host: hostFamily(\"session:\", \"session\")`: an object lives on `session:` + its input's `session` field, a required\n  string. TypeScript checks the field name against the input.\n\nA `get` normalizes the input (defaults count), resolves the host, and acquires the object here if that is this host,\notherwise over the link to it. `serve` refuses a type whose placement does not include the serving host, so a misplaced\ntype fails at startup or at the reload that adds it. The host that gets a hold checks that its own contract places the\ninput on itself, and a relay checks the hold's target against its contract token: a client never chooses where an\nobject lives. The placement is part of the description and the fingerprint: moving a type is a breaking change, and\nreferences to it get `upgrade_required`.\n\n### Local types\n\nLocal types are reachable only inside their host. Their members may be anything: functions, callbacks, native objects. Non-JSON members get opaque or function descriptors.\n\n```ts\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(HitSchema)),\n\t\tonChange: method(Type.Function([ChangeSchema], Type.Void()), UnsubscribeSchema),\n\t\tharness: opaque(\"pi.durable-harness@1\"),\n\t},\n});\n```\n\n### Inputs identify objects\n\n`{ session: \"S1\", conversation: 1 }` always means the same object. Inputs are normalized (defaults applied, unknown fields removed), validated, and serialized canonically. Fields equal to their default are left out, so adding a new optional field with a default doesn't change existing addresses. Changing a default, or how inputs are normalized, needs a new type name.\n\n### Versions and compatibility\n\nEvery bundle carries its own copy of each contract it was built against. When a consumer binds to a provider, the two copies are compared:\n\n> A consumer built against version M binds to a provider at version N if **N = M and the provider's description covers the consumer's**. Otherwise the provider must offer an adapter in `accepts[M]`, or binding fails with `contract_mismatch`.\n\n\"Covers\" means the provider still offers everything the consumer was built against:\n- every member exists with the same kind;\n- methods accept everything the consumer can send (new arguments must be optional) and return at least what it expects (extra result fields are fine);\n- state values fit the consumer's schema;\n- opaque members have the same name and version.\n\n| Change | Version | Result |\n| --- | --- | --- |\n| Add a method, an optional argument, a result field, a state field | keep | binds |\n| Remove or rename a member; make an argument required; change a type | **bump** | old consumers need `accepts`, or must be reloaded too |\n| Same shape, different meaning (`query` is now a regular expression) | **bump** | as above; undetectable, so it's on you |\n| Forgot to bump on a breaking shape change | keep | refused: \"consumer needs member `search`\" |\n\n```ts\n// version 2 changed what `search` means; old consumers keep working through an adapter\nhost.serve(Index, indexFactory, {\n\taccepts: {\n\t\t1: (current) => ({ search: ({ query }, ctx) => current.search({ query: escapeRegExp(query) }, ctx) }),\n\t},\n});\n```\n\n## Serving objects\n\n```ts\nhost.serve(Type, (input, principal, self) => implementation, options);\n```\n\n- The **factory** runs synchronously. It declares what the object needs and returns its implementation: methods and state members, plus `variant` for remote types. Returning a promise throws.\n- There is one **object** per `(type, input, principal)`. It holds the object's state, resources and references, and survives reloads. Only the implementation is replaced.\n- **Options:**\n  - `accepts`: adapters for consumers built against older contract versions;\n  - `lifetime`: `\"referenced\"` (default) creates the object at its first `get` and disposes it after the last reference\n    is released; `\"host\"` creates it when the plugin loads and keeps it until the host closes (see [Lifetime](#lifetime));\n  - `linger`: how long to keep a referenced object after the last reference is released.\n\n## Declarations\n\nA reload runs the factory again over the same object. Declarations that keep something across a reload have a **name**: the new code's `status` cell is the old code's `status` cell, with its value. Derivations and tasks hold nothing across a reload and have no name.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 });\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async ({ signal }) => openSqlite(statusPath(session), { signal }),\n\t\tclose: async (db) => db.close(),\n\t});\n\tself.on(\"catalogChanges\", catalog.rows, (rows, ctx) => errors.replace(ctx, checkRows(rows)));\n\tself.task(async (ctx) => {\n\t\twhile (!ctx.signal.aborted) {\n\t\t\tawait sleep(10_000, ctx.signal);\n\t\t\tawait db.value.run(\"INSERT INTO history VALUES (?)\", [JSON.stringify(status.value)]);\n\t\t}\n\t});\n\tconst summary = self.derive([status, errors], (s, e) => ({ ...s, failing: e.length > 0 }));\n\treturn { summary, report: reportMethod(status) };  // exposed state is what the implementation returns\n});\n```\n\n| Declaration | What it is | On reload |\n| --- | --- | --- |\n| `self.state(name, schema, initial \\| { init }, { version?, migrate? })` | an observable JSON cell; the schema is required | same cell and value (`init` does not run); migrated when its version rises |\n| `self.get(Type, input, { at? })` | a reference to another object; its identity is the address | kept if declared again, otherwise released |\n| `self.resource(name, { version, deps?, exclusive?, open, close? })` | a connection, watch, socket, port | kept; reopened only if `version`, `deps` or `exclusive` change |\n| `self.on(name, source, handler)` or `self.on(name, source, select, handler)` | reacts to state or to an event source | the same subscription, with the new handler, as long as the source and `select` are unchanged |\n| `self.task(async (ctx) => ...)` | long-running background work | the old run is aborted through `ctx.signal`; the new code starts once it finished (at most `taskStopDeadline`, default 5 s) |\n| `self.derive(sources, fn)` | live projection of state; `fn` is synchronous | a new derivation, computed when the reload commits |\n| `self.register(Registry, { key: value })` | an entry in a registry | replaced at commit |\n| `self.expose(resource)` | a member backed by a resource (local types) | follows the resource |\n\nNames are strings: declaring one twice in a run fails (`invalid/duplicate`). Each kind has its own names, so a cell and a resource may share one.\n\nA listener keeps its subscription across reloads when it is declared on the same resource (or source) with the same `select`: the same function, or one with the same source text. `select` is not called again, so a source that accepts a single subscriber (a Durable watch) survives reloads, and `select` may wrap the value in a new adapter object. A `select` whose result depends on captured values that change between reloads is not noticed; give such a listener a new name.\n\n**A listener on state reconciles.** A listener on state (a cell, a derivation, a reference's state member) gets the current value when it starts, then every new value. When a reload replaces its source (a derivation is new at every reload), it subscribes again and gets the current value again. Write its handler to bring things in line with the value it gets (set, compare, start or stop what the value says), not to count or append per call. An event source (a resource's events) delivers no current value.\n\nA name you no longer declare is released, closed or stopped, after the old code has finished what it was doing.\n\n**State that must survive a reload lives in cells,** never in closure variables. A closure variable lasts as long as one version of the code.\n\n```ts\n// wrong: reset on every reload\nhost.serve(Visits, (_, _p, self) => {\n\tlet visits = 0;\n\treturn { hit: async (ctx) => ++visits };\n});\n\n// right\nhost.serve(Visits, (_, _p, self) => {\n\tconst visits = self.state(\"visits\", Type.Number(), 0);\n\treturn {\n\t\thit: async (ctx) => {\n\t\t\tvisits.change(ctx, (n) => n + 1);\n\t\t\treturn visits.value;\n\t\t},\n\t};\n});\n```\n\n**Methods** are declared with `method(args?, result?)` in the contract. Without `args` a method takes none: callers write `ref.hit(ctx)` (or `ref.hit()`), and its implementation is `(ctx) => …`. Without `result` it returns nothing: the implementation returns nothing, and callers get `undefined`. `method(undefined, Type.Number())` takes no arguments and returns a number.\n\n## Activation\n\nThe factory is synchronous, but activation isn't. `self.get` and `self.resources` only *declare*; the slow work happens afterwards:\n\n```text\nget(Index, { session: \"S1\" })\n 1. run the factory               → declarations\n 2. open declared resources        → e.g. connect to SQLite\n 3. wait for declared refs        → dependencies activate first\n 4. run the cells' init           → e.g. read the rows into a cell\n 5. live                           → refs become \"live\"; listeners and tasks start\n```\n\n```ts\nhost.serve(Index, ({ session }, _host, self) => {\n\tconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(indexPath(session)), close: (db) => db.close() });\n\treturn {\n\t\tsearch: async ({ query }, ctx) => db.value.all(\"SELECT entryId, text FROM fts WHERE fts MATCH ?\", [query]),\n\t};\n});\n\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tconst index = self.get(Index, { session });  // a placeholder until Index is live\n\treturn {\n\t\tvariant: \"participant\",\n\t\tfind: async ({ query }, ctx) => index.search({ query }, ctx),  // only ever runs once Chat (and Index) are live\n\t\t// ...\n\t};\n});\n```\n\n**Rules:**\n- Inside the factory, never use what you declared: `db.value` and `index.someState.value` throw `not_ready`, and calling a reference throws. Use them in methods, listeners, tasks and `init`.\n- **The object goes live only with its references ready**: live, and (for a remote reference) every state member of its variant hydrated. The same holds for state it forwards or derives from a remote reference it holds some other way: it goes live once that state has its value. A reference that is not ready after the host's `readyTimeout` (default 60 s) fails the activation with `unavailable/ready_timeout`; a reference cycle across hosts ends that way instead of hanging.\n- **Concurrent `get`s** of one address share one activation, so the database opens once.\n- **Failure propagates:** if `openSqlite` throws, `Index` fails, `Chat` fails with that cause, and every waiting `get` rejects. The next `get` tries again.\n- **Abandoned activations:** if everyone releases before activation finishes, the result is disposed.\n\n### Startup readiness and parent admission\n\n`await host.load(facets)` and `await host.install(facet)` wait for the `lifetime: \"host\"` objects they install to\nbecome live: resources are open, declared references are live, and registrations are published. All facets in a load\nare installed before activation is awaited, so a host object can use a provider in a later facet. Referenced objects\nare created only when something references them. Factory, dependency and resource errors reject startup with the\nreference's activation error; host object failures also reach `onError`. Installation is not rolled back on failure: close the host\nwhen abandoning startup. `install` registers synchronously, so setup/registration errors still throw synchronously.\n\n`await host.ready()` waits for the host objects present when called, including direct\n`host.serve(..., { lifetime: \"host\" })` and previously deferred loads. It is an activation barrier, not a continuous health check, a wait for tasks to\nfinish. Later installations require another readiness wait.\n\nA worker may need to open its local Harness and announce control readiness **before** the parent accepts its link.\nA host object reporting to the server's SessionStatus cannot become live before that acceptance. Awaiting the\nwhole load before announcing control readiness would deadlock. Defer the whole barrier explicitly:\n\n```ts\n// Connect the upstream link first; its outgoing references can queue until the parent accepts it.\nawait worker.load(workerFacets, { awaitHostObjects: false });  // also supported by install\nconst harness = await worker.get(Harness, {});        // local bootstrap only; no upstream dependency\nannounceControlReady();                               // parent can now accept the worker link\nawait worker.ready();                                 // every host object live, including upstream references\nharness.release();\n```\n\n`awaitHostObjects: false` still starts every host object; it skips waiting, not activation. There is no\nautomatic classification of upstream dependencies. Choose a local bootstrap reference with no upstream dependency,\nconnect before loading upstream consumers, and let the parent admit the link before the full barrier. Control\nreadiness means admission is possible, not that every host object is live. Keep worker bootstrap counted as busy\nuntil the full barrier settles, and report a later failure through the worker's fatal/startup-error path. Never await a\nreference whose object needs the parent (a remote one, or a local one that holds one) before the parent admits the link.\n\nTo hand out a native object a resource opens (a local type's `opaque` member), expose the resource. References read its value once the object is live, and `undefined` before:\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", { version: 1, open: () => DurableHarness.open(storage), close: (h) => h.close() });\n\treturn { harness: self.expose(durable) };  // contract member: harness: opaque<DurableHarness>(\"pi.durable-harness@1\")\n});\n\nconst harness = await ctx.get(Harness, {});\nharness.harness.resume();  // the DurableHarness itself\n```\n\n## Principals and variants\n\nThe application defines its principals and how they're keyed. Chord invents none: the app gives every principal a\nrequest can act as.\n\n```ts\ntype Principal =\n\t| { kind: \"owner\"; access: \"local\" | \"relay\" }\n\t| { kind: \"guest\"; id: string; name: string; session: string; mode: \"view\" | \"full\"; revision: number }\n\t| { kind: \"worker\"; session: string; generation: number }\n\t| { kind: \"server\" }   // the parent's own requests, as a worker sees them\n\t| { kind: \"host\" };    // a host's own code\n\nconst host = createHost<Principal>({\n\tname: \"server\",\n\tself: { kind: \"host\" },\n\tprincipalKey: (p) =>\n\t\tp.kind === \"owner\" ? `owner:${p.access}` :\n\t\tp.kind === \"guest\" ? `guest:${p.id}:r${p.revision}` :\n\t\tp.kind === \"worker\" ? `worker:${p.session}:${p.generation}` :\n\t\tp.kind,\n});\n```\n\n- `createHost<Principal>` types every factory's `principal`, `ctx.principal`, routes, `accept` and\n  `invalidatePrincipal` patterns, so code narrows on `kind` without casts. Facet modules say it too:\n  `defineFacet<Principal>((api) => …)` (an app usually exports its own `defineFacet` and `Ctx` aliases). Without a type\n  argument, principals are `unknown`.\n- There is one object per principal key: two tabs of the owner share an object, and a guest gets its own.\n- Code inside a host acts as `self`, so internal objects are shared. A principal with the same key as `self` is the\n  host's own, whichever object carries it.\n- A link stamps the principal of whatever connects to it. Callers can never supply their own.\n- A child host names what its parent's own requests act as: `connect(link, { principal })` (or\n  `upstream: { link, principal }`). Requests the parent relays act as the caller it forwards.\n- A route names what the hosts it resolves act as when they call back: `route(prefix, { …, principal: (name) => … })`.\n\n**A remote factory picks a variant:**\n\n```ts\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tif (!canView(principal, session)) throw forbidden();\n\tconst c = self.get(Conversation, { session, conversation });  // as host: one shared instance\n\tif (!canSend(principal)) return { variant: \"viewer\", view: c.view };\n\tconst author = authorOf(principal);\n\treturn {\n\t\tvariant: \"participant\",\n\t\tview: c.view,\n\t\tsend: async (input, ctx) => c.submit(input, author, ctx),\n\t\tstop: async (ctx) => {\n\t\t\tawait c.stop(author, ctx);\n\t\t},\n\t};\n});\n```\n\n```ts\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // on session:S1\nif (chat.variant === \"participant\") showComposer(chat.send);  // narrows: `send` exists only there\nelse showReadOnly();\n```\n\n**What a variant doesn't do:**\n- **It isn't complete authorization.** Methods still check their arguments: that a submission belongs to this conversation, or a session ID to this principal.\n- **It doesn't cover flags that change while you hold the object.** Those are state, for example `you: { canDraw: boolean }`.\n- **Revocation** is `host.invalidatePrincipal(match)`. It force-disposes matching objects, here and over links, and their references become `gone`. A new `get` runs the factory with the new grants.\n\nLocal types have no variants. Their only callers are inside the host, acting as its `self`. If a local object needs to know who's acting, pass that explicitly, like `author` above.\n\n## References\n\n| Where | API | Released |\n| --- | --- | --- |\n| a factory | `self.get(Type, input)`: synchronous; the object goes live after it is ready | with the object (or its replaced declaration) |\n| a call | `await ctx.get(Type, input, { state? })` | when the call settles |\n| outside Chord | `await host.get(Type, input, { signal?, state? })` | `ref.release()` |\n| a UI runtime | `host.hold(Type, input)`: synchronous, possibly `loading` | `ref.release()` |\n\n```ts\nconst chat = await ctx.get(Chat, input);  // ready: live, every state member of its variant hydrated\nchat.status;  // \"loading\" | \"live\" | \"released\" | \"gone\"\nchat.variant;  // the current variant; a multi-variant reference narrows on it\nchat.shape;  // { variant, members, contract }\nconst send = chat.send;  // a stable function: always dispatches to the current code\nchat.view.value;  // the current value, typed T\nchat.view.subscribe((v) => render(v));\nchat.onStatus((status, error) => { if (status === \"gone\") reconnect(error); });\nchat.release();\n```\n\n- **A reference you hold is usable:** an awaited `get` resolves once the object is live and every state member of its variant has its first snapshot, local or remote. It rejects with the activation error (`forbidden`, unreachable, …), with `aborted` (a `ctx.get` whose ctx is aborted or whose call settles first; `host.get`'s `signal`), or with `unavailable/ready_timeout` after the host's `readyTimeout`. Only that reference is released then.\n- **A held reference stays subscribed** to the state of its variant, with or without listeners, until it is released. After a link loss the last values stay readable while it re-holds (status `loading`).\n- **Reading state before the first hydration throws `not_ready`** (a `hold` that is still loading, a member read inside a factory). A member outside the current variant throws `not_in_variant`; an ended reference throws its end (`released`, or the `gone` error). A listener added before the hydration gets its snapshot then.\n- **`state: false`** gives a method-only reference: it receives no state (unless another reference to the same object wants it) and is ready once its object is live.\n- **Calls on a `loading` reference wait** until it's live, respecting `ctx` cancellation and a deadline.\n- A member missing from the current variant rejects with `not_in_variant`.\n- After `release()`, every member rejects with `released`.\n- **`gone`** means the object was forced away while referenced: revocation, archive, its host dying. Call `get` again, or show \"unavailable\". `onStatus` reports every status change (`live`, `loading` while a link reconnects, `released`, `gone` with the error), so a UI needn't poll.\n- **References to one remote object share one binding** per host and key (type, normalized input, principal key, contract fingerprint): one hold over the link, one stream per state member, calls over the same hold. Each reference keeps its own status, observers and `release`. With `createHost({ linger: { types: { [typeName]: ms }, cap } })`, a binding whose last reference is released stays held (and hydrated) for `ms`, at most `cap` (default 4) at a time, so taking the reference again costs nothing on the link.\n- **Return values are values.** Anything long-lived you want to share is an object you reach with `get`.\n\n## State\n\n```ts\nconst todos = self.state(\"todos\", Type.Array(TodoSchema), []);\n\ntodos.change(ctx, (draft) => {\n\tdraft.push({ id: \"t1\", text: \"Write docs\", done: false });\n});\ntodos.change(ctx, (draft) => {\n\tdraft[0].done = true;\n});\ntodos.replace(ctx, []);\n\ntodos.value;  // immutable\ntodos.subscribe((value) => render(value));\n\n// mirroring an external immutable store: subscribers get a diff, not the whole value\nview.replace(ctx, conversation.view);                    // diffs when the new value reuses old containers\nview.replace(ctx, rebuilt, { diff: true });              // always diff\n```\n\n- A `change` is atomic: one revision, or nothing if the callback throws.\n- `change` only touches the paths you mutate, so fields your code doesn't know about survive. Use `replace` only where your code owns the whole value.\n- Values are immutable. Never mutate what you read or receive.\n- Remote references get a snapshot of each state member of their variant, then deltas. After a reconnect or a reload they get a `reset` snapshot, and nothing is replayed.\n- `replace` sends a structural diff when the new value shares containers with the old one (a new revision of the same state), and a single replacement otherwise. `{ diff: true | false }` forces either. The value is the same either way.\n- An implementation may expose another object's state member as its own: `return { variant: \"viewer\", view: conversation.view }`. The object goes live only once that member has its value, local or remote.\n\n**A cell whose first value comes from a source** declares `{ init }` instead of an initial value:\n\n```ts\nconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(path), close: (db) => db.close() });\nconst notes = self.state(\"notes\", Type.Array(NoteSchema), { init: () => db.value.all(\"SELECT * FROM notes\") });\n```\n\n`init(ctx)` runs once the resources are open and the references live, before the object goes live, so no method,\nlistener, task or subscriber sees a value that did not come from the source. A task that loads the value instead\nstarts only after the object is live: a method called in between answers from the placeholder (a note that exists\nreads as unknown), and an async load can overwrite a write made during its `await`. `init` may be async and gets a\n`ctx` to call other objects. The `init`s of one object run in parallel, so one must not read another's cell. A throw or\nan invalid value fails the activation. A reload keeps the cell and its value and does not run `init` again; a cell the\nreload adds runs its `init` while the reload prepares, and a failure refuses the reload.\n\n`init` reads a source the object has anyway. It saves nothing: what must outlive the process is written to that source\n(a database, a file, a Durable document) by the code that changes it.\n\n```ts\nconst visible = self.derive([catalog.rows], (rows) => rows.filter((r) => visibleTo(principal, r)));\nconst openCount = self.derive([todos], (t) => t.filter((x) => !x.done).length);\n```\n\nDerivation functions are synchronous. A derivation has no name: every reload makes a new one, computed when the\nreload commits.\n\n## Calls, ctx and roots\n\nEvery method gets `(args, ctx)`, or `(ctx)` when it takes no arguments. Pass `ctx` on to everything you call:\n\n```ts\nsend: async (input, ctx) => {\n\tctx.signal.throwIfAborted();\n\tconst uploads = await ctx.get(Uploads, {});  // a reference owned by this call; released when it settles\n\tconst files = await uploads.resolve({ ids: input.uploads }, ctx);\n\treturn c.submit({ ...input, files }, author, ctx);\n},\n```\n\nA **root** is a short tree of calls, started at ingress:\n- by a link, for a request that arrives without one;\n- by `host.root(fn)`, for timers and scripts;\n- by each event delivered to a listener (`self.on`);\n- by each Chord call made from a `task` or a Durable phase.\n\nA root runs on the code that was current when it started (its **code version**), across hosts, until it ends. Keep roots short. Long-running work is a `task`, and long waits are state.\n\n```ts\n// wrong: holds a root (and its version of the code) open for minutes\napprove: async (ctx) => waitForHuman(ctx),\n\n// right: record the request as state; the answer is a separate call\nrequest: async (req, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\td.push(req);\n\t});\n},\nanswer: async ({ id, value }, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\tsettle(d, id, value);\n\t});\n},\n```\n\n## Lifetime\n\nAn object lives while it's **referenced**, and every reference has an owner:\n\n| Owner | Acquired with | Released when |\n| --- | --- | --- |\n| another object | `self.get` | that object is disposed, or no longer declares it |\n| a call | `ctx.get` | the call settles |\n| a connection | a remote host's `get` | release, disconnect, revocation |\n| the host | `lifetime: \"host\"` | host shutdown (or a reload removing its facet) |\n| UI | `useObject` | unmount |\n\n- **Disposal** runs after the last reference is released, plus `linger`. Tasks are aborted, listeners removed, resources closed in reverse order (every close runs, and errors are collected), then the object's own references are released, which can dispose further objects.\n- **A resource's `close` may still use the objects its object references.** That holds for host shutdown and revocation too: `host.close()` and `invalidatePrincipal` dispose referrers before what they reference, and close links last. Bound remote calls in `close` with a signal: a call over a link that is already down waits for a reconnect.\n- **Forced disposal** (revocation, archive) makes references `gone` at once and aborts in-flight calls.\n- **References must not form cycles.** Inside one host this is checked at `get`.\n\n```ts\nhost.serve(Conversation, conversationFactory, { linger: 30_000 });  // switching back quickly doesn't reopen its watch\nhost.serve(Index, indexFactory, { lifetime: \"host\" });              // one object from load to host shutdown\n```\n\n**`lifetime: \"host\"`** is for local types with an empty input (or only defaults): the type has exactly one object,\ncreated for the host principal when its facet loads. Every `get` reaches that object, whoever asks; a `get` with\nanother input is invalid. References others hold do not change its lifetime, and it cannot `linger`. Only such\nobjects may register into registries. TypeScript refuses `lifetime: \"host\"` for a remote type, for an input with a\nrequired field and together with `linger`; `serve` checks the same at runtime, for callers without types.\n\n## Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\n\n// contribute (only from `lifetime: \"host\"` objects)\nhost.serve(Downloads, (_, _host, self) => {\n\tconst blobs = self.get(Blobs, {});\n\tself.register(Routes, {\n\t\t\"GET /download\": async (request, who, ctx) => {\n\t\t\tconst file = await blobs.find({ id: request.query.id }, ctx);\n\t\t\tif (!canView(who, file.session)) return forbidden();\n\t\t\treturn stream(file, ctx);\n\t\t},\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n\n// consume: a request sees the table of its own code version\nserver.on(\"request\", (req, res) =>\n\thost.root(async (ctx) => {\n\t\tconst route = host.registry(Routes).get(`${req.method} ${pathOf(req)}`, ctx);\n\t\treturn route ? route(req, principalOf(req), ctx) : notFound(res);\n\t}),\n);\n```\n\n- Each code version has one complete table. A reload never leaves a moment where an entry is missing.\n- A request that started before a reload keeps using the old table.\n- Order is first-registration order and survives replacement.\n- `conflict: \"error\"` makes a duplicate key fail the registration (or the reload).\n\n## Plugins\n\nA plugin is a feature with one **facet** per host kind:\n\n```text\nplugins/chat/\n  contract.ts    Chat, Conversation, Drafts\n  server.ts      facet for the server\n  worker.ts      facet for session workers\n  web.tsx        facet for browsers\n```\n\n```ts\n// plugins/chat/worker.ts\nimport { defineFacet } from \"@earendil-works/chord\";\nimport { Chat, Conversation } from \"./contract.ts\";\n\nexport default defineFacet((host) => {\n\thost.serve(Conversation, conversationFactory, { linger: 30_000 });\n\thost.serve(Chat, chatFactory);\n});\n```\n\n```ts\n// a host loads the plugins' facets for its kind\nawait host.load([\n\tnew URL(\"../plugins/chat/worker.ts\", import.meta.url),\n\tnew URL(\"../plugins/status/worker.ts\", import.meta.url),\n]);\n```\n\nBuilt-in plugins are trusted code from this repository and load directly from source.\n\nThe facet's `setup` argument is its host, scoped to what a plugin needs: `serve`, `name`, `root(fn)`, `registry(R)` and `signal`, which aborts once this run of the facet is no longer installed (a reload replaced or removed it, or the reload that loaded it aborted). Code that is not a Chord method, listener or task (Durable tools and hooks, callbacks of native libraries) keeps it to start roots and read registries, instead of reaching for a process-global host:\n\n```ts\nexport default defineFacet((host) => {\n\thost.serve(Sessions, sessionsFactory);\n\ttools.register(\"rename_session\", (args) =>\n\t\thost.root(async (ctx) => (await ctx.get(Sessions, {})).rename(args, ctx)),\n\t);\n});\n```\n\n## Hot reload\n\n```ts\nawait host.reload([new URL(\"../plugins/chat/worker.ts\", import.meta.url)]);  // or reload every changed plugin\n```\n\n### What happens\n\n1. **Load** the new modules.\n2. **Prepare:** run the new factories over the existing objects, without side effects. Cell writes throw, listeners and tasks don't start, registrations are collected, and changed resources are opened next to the old ones. Contracts are checked against every live consumer. Any failure leaves everything as it was.\n3. **Gate:** only if a cell's version was raised or an exclusive resource changes.\n4. **Commit:** in one synchronous step, install the new code, swap registry tables, restart tasks, rebind listeners, and send `reset` and shape updates to subscribers.\n5. **Retire:** old code keeps running the roots it started. Once they end, it's dropped, along with whatever it alone declared. If they are still running when the next reload starts, that reload waits up to `retireDeadline`, then aborts them (their `ctx.signal`) before it closes what only the old code used.\n\n### Compatible changes: no waiting\n\nBug fixes, new methods, new cells, new resources and registration changes all switch immediately. A root that started on the old code finishes on it, and new roots get the new code.\n\n### Changing what state looks like or means: bump the cell's version\n\n```ts\n// v1: { running, tasks } where tasks counted phases\n// v2: tasks counts tasks, and usage is new\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusV2, { running: false, tasks: 0, usage: emptyUsage() }, {\n\t\tversion: 2,\n\t\tmigrate: (old, from) => (from < 2 ? { running: old.running, tasks: 0, usage: emptyUsage() } : old),\n\t});\n\treturn {\n\t\tstatus,\n\t\treport: async (s, ctx) => {\n\t\t\tstatus.replace(ctx, s);\n\t\t},\n\t};\n});\n```\n\nAt the gate, for each live `StatusStore`:\n1. **Roots already inside finish.** Their re-entrant calls are let in.\n2. **Old roots that haven't touched it yet get `upgrade_conflict`** instead of old code over migrated state. They retry as a new root.\n3. **`migrate` runs** on a private copy, and the result is validated against the new schema.\n4. **Commit.** New roots use the new code and the migrated state.\n\nIf the roots inside don't finish within the deadline (5 s), the reload is **aborted** and nothing changes. The error names the blocking roots.\n\nThe version belongs to the cell (default 1); other cells of the object are not migrated. The runtime compares each\ncell's schema and version between old and new code:\n- unchanged: compatible;\n- version raised: migrate (without `migrate`, the value is kept and must fit the new schema);\n- schema changed **without** a raised version, or a lowered version: the reload is refused.\n\nA change of meaning with an identical schema needs a bump too. Nothing can detect that one for you.\n\n### Resources\n\n```ts\nconst db = self.resource(\"db\", { version: 2, open: () => openSqlite(path, { wal: true }), close: (db) => db.close() });  // 1 → 2: reopen\nconst port = self.resource(\"port\", { version: 1, exclusive: true, open: () => listen(8787), close: (s) => s.close() });\n```\n\n- A reload keeps a resource's open value unless its `version`, `deps` or `exclusive` change: `open` doesn't run\n  again. The version is required: a native value has no schema to detect a change. The kept resource still takes the\n  new code's declaration at the commit, so a later `close` is the new one.\n- A changed resource opens next to the old one during prepare. The old one is closed with its own `close` once the\n  old code retires.\n- An `exclusive` resource can't exist twice, so it's swapped at the gate: close the old one, open the new one, and reopen the old one if that fails.\n\n### Contract changes\n\n| Change | What happens |\n| --- | --- |\n| Additive, same version | reloads; consumers keep working |\n| Breaking, version bumped, with `accepts[old]` | reloads; old consumers go through the adapter |\n| Breaking, without an adapter | refused, unless the consumers reload in the same batch |\n| Breaking, version not bumped | refused: \"consumer needs member …\" |\n\nThe host's own **reference to a `lifetime: \"host\"` object** is a lifecycle pin, not an old-version consumer. When a reload replaces\nits provider, that reference adopts the new contract at commit; it does not require an `accepts` adapter for itself.\nThe object, cells and unchanged resources stay alive without reactivation. A refused reload keeps the original\nreference and contract. Other references (`self.get`, `ctx.get`, `host.get`, peers) still obey the compatibility rules above;\nstate-version bumps and migrations are still required when stored state changes.\n\n### Several plugins at once\n\nReload them together: `host.reload([a, b])`. Prepare sees all new code at once, so a changed contract and its updated consumer check against each other.\n\nOn the server, `host.reload(serverFacets, { remote: { \"session:\": workerFacets } })` reloads every connected worker too. Its report covers the server; `report.children[\"session:S1\"]` says what each worker changed, prepared and gated.\n\n## Hosts and links\n\n```ts\n// server\nconst server = createHost<Principal>({ name: \"server\", self: { kind: \"host\" }, principalKey });\nserver.accept(webSocketLink(wss, { principal: (req) => authenticate(req) }));  // stamps owner or guest from cookies\nserver.route(\"session:\", {\n\tauthorize: (principal, host) => canReach(principal, host),  // before resolving or spawning\n\tresolve: (host) => workers.ensure(host),  // a link to the worker, spawned if needed\n\tprincipal: (host) => workers.principal(host),  // what the worker acts as when it calls the server\n});\n\n// worker\nconst worker = createHost<Principal>({\n\tname: `session:${id}`,\n\tself: { kind: \"host\" },\n\tprincipalKey,\n\tupstream: { link: ipcLink(process), principal: { kind: \"server\" } },  // the server's own requests act as this\n});\n\n// anywhere\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // browser → server → worker session:S1\nconst sessions = self.get(Sessions, {});  // `hostNamed(\"server\")`: worker → server, acting as worker{S1}\n```\n\n- **The server relays** between its children: worker to worker, and browser to worker.\n- **Disconnects:** when a browser disconnects, exactly the references it had in workers are released.\n- **Principals across links:**\n  - a forwarded principal is accepted only from the parent link;\n  - calls an object makes itself act as its host;\n  - links ping, and a dead link releases everything held through it.\n- **Client origins:** a hold that comes from a browser carries an opaque id of its connection (`origin`), forwarded by the server when it relays the hold, so a worker can tell its clients apart. A client cannot set it. A disconnect releases that client's holds at once.\n- **Slow peers:** a peer acknowledges what it processed. While a window (`window`, default 256 KiB) of frames is unacknowledged, state waits and is coalesced per member's stream; calls, results and the first snapshots of a new reference are never held back, so a `get` does not wait behind other references' state. Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams are replaced by their current value. So under load a result can arrive before the state change it caused: wait for the state you need instead of assuming it is there when the call returns.\n- **Reloads across hosts:** the server coordinates prepare and commit for every host.\n- **Workers that start behind** (spawned after a reload) are caught up to the server's code version before anything is routed to them. Calls their roots make to the server meanwhile run in the server's current code version instead of failing `superseded`.\n- **Protocol version:** both ends announce the link protocol version in their hello. A peer of another version is not\n  talked to: the link closes, and `onProtocolMismatch` (a `LinkOptions` field of `connect` and `accept`) tells the\n  side that has to load new code. A browser reloads its page instead of reconnecting:\n\n  ```ts\n  host.connect(webSocketLink(socket), { onProtocolMismatch: () => location.reload() });\n  ```\n- **Browsers** don't take part in commits. They learn about new code through shape and `reset` messages, and an incompatible contract makes their calls fail with `upgrade_required`.\n\n## Durable\n\nA worker's host provides the Durable Harness as a local object of lifetime `\"host\"`. A Durable phase isn't a root: it's pinned to its registry snapshot by Durable itself. Each Chord call a hook or tool makes starts a fresh short root.\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", {\n\t\tversion: 1,\n\t\topen: () => DurableHarness.open(storage, { models, registry: durableRegistry(host, Extensions) }),  // a reader over the code version tables\n\t\tclose: (h) => h.close(),\n\t});\n\treturn { conversation: (id: number) => durable.value.conversation(id) };\n}, { lifetime: \"host\" });\n\n// a plugin contributes a Durable extension like any registration\nhost.serve(Guard, (_, _host, self) => {\n\tself.register(Extensions, {\n\t\tguard: defineExtension({\n\t\t\tname: \"guard\",\n\t\t\thooks: [hook(ToolTask, { beforeTool: (call) => (isDangerous(call) ? { block: \"Needs approval\" } : undefined) })],\n\t\t}),\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n```\n\nA reload changing the guard: Durable phases already running keep the old snapshot, the next phase uses the new one, and there's never a phase without a guard.\n\n## Errors\n\n| Code | Meaning |\n| --- | --- |\n| `forbidden` | The factory or a method refused this principal |\n| `invalid` | Input, arguments or a result failed validation |\n| `not_ready` | A declared resource or reference was used inside a factory, or state read before its reference's first hydration |\n| `not_in_variant` | The member doesn't exist in the variant you have |\n| `released` | The reference was released |\n| `gone` | The object was forced away while referenced. Call `get` again. |\n| `contract_mismatch` | The provider's contract doesn't cover the consumer's, and no adapter exists |\n| `upgrade_conflict` | An old root reached an object migrated meanwhile. Retry as a new root. |\n| `superseded` | The root's code version is no longer available on this host. Retry. |\n| `upgrade_required` | A browser's contract is outdated. Reload its web facets. |\n| `unavailable` | The target host can't be reached or started; reason `ready_timeout`: a reference was not ready within `readyTimeout` |\n| `aborted` | The call's `ctx` was cancelled |\n\n## Rules of thumb\n\n| Do | Not |\n| --- | --- |\n| Keep reloadable state in cells | Closure variables (lost on reload) |\n| Bump the cell's version, with `migrate`, when a cell's shape or meaning changes | Changing a schema without a bump (refused) or a meaning without a bump (silent) |\n| Bump the contract version on breaking changes; add `accepts` for old consumers | Renaming or removing members at the same version (refused) |\n| Do slow setup in a resource's `open` | Awaiting inside the factory (it's synchronous) |\n| Do side effects in resources, listeners, tasks and registrations | Effects in the factory body (they run during prepare) |\n| Use `ctx.get` inside methods | `self.get` inside methods (a new reference per call) |\n| Keep roots short; long waits are state | Awaiting a human inside a method |\n| Check arguments in methods | Treating the variant as all of authorization |\n| Pass `ctx` everywhere | Starting background work with the caller's `ctx` (use a task) |\n| Treat state values as immutable | Mutating what you read |\n","isInternal":false,"tokens":10885,"sizeBytes":45188},{"name":"spec.md","path":"packages/chord/docs/spec.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/chord/docs/spec.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Chord specification\n\nThis document specifies the runtime in `src`. Where it and the code disagree, the disagreement is a bug. [Known limitations and decisions](#12-known-limitations-and-decisions) lists what the runtime does not do yet and the choices the sections below leave open.\n\nHard constraints:\n- Chord's runtime depends only on its own `delta` module (tracker, applyImmutable, encoder/decoder, diff) and Node/browser built-ins plus `typebox` for schemas. No other Pi packages.\n- Built-in plugins are trusted, loaded from source in-process. The plugin loader uses dynamic `import()` with a cache-busting query (owner-approved exception, `TODO(loader)`: replace with an isolating, evicting loader; old module copies leak; TUI plugins need isolation).\n- Hot reload is required. Host restart only for Chord/runtime code changes or host-level crash recovery.\n\n## 1. Concepts\n\n| Term | Meaning |\n| --- | --- |\n| facet | A plugin's code for one host (surface): it serves object types (`defineFacet`). |\n| object type | Name + contract. `remote` (addressable from other hosts; schemas for input, variant state members, method args/results) or `local` (host-only; members may be anything, including functions and native objects, but must be listed in the contract: `method`, `opaque` or a state schema). |\n| factory | `(input, principal, self) => implementation`. Synchronous. Registered with `host.serve(type, factory, options)` from a plugin facet. |\n| object | One instance of an object type. Stable identity per `(type, canonicalInput, principalKey)`. Owns cells, resources, references, listeners, tasks, registrations. Survives reloads. |\n| implementation | The object's code: what a factory run returns (methods + state members; `variant` for remote types). Immutable, per code version. |\n| reference | What `get` returns: a late-bound claim on an object, never the object or its implementation. An object lives while referenced. |\n| principal | App-defined. Stamped by links. In-host code acts as the `host` principal. |\n| root | One short Chord call tree, admitted at trusted ingress (link request without root, UI event, timer, `host.root`). Carries `{ rootId, codeVersion }`. |\n| code version | One committed set of implementations/registrations per host (server-issued id). |\n| code holder | Long-lived execution that is NOT a root: Durable phases/tool runs, browsers, `self.task` loops. Every Chord call they make starts a fresh short root. |\n\n## 2. Contracts\n\nContracts are runtime values for every type (shipped bundles have no TypeScript; third-party plugins are built separately). TS types are inferred from them.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,                                   // contract version: a plain number, bumped only on breaking changes\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),       // where its objects live: `session:` + input.session\n\tvariants: {\n\t\tviewer: { state: { view: ViewSchema } },\n\t\tparticipant: {\n\t\t\tstate: { view: ViewSchema },\n\t\t\tmethods: { send: method(SendSchema, Type.Object({ submissionId: Type.Number() })) },\n\t\t},\n\t},\n});\n\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(Hit)),\n\t\tonChange: method(Type.Function([Change], Type.Void()), Unsubscribe),   // non-JSON allowed for locals\n\t\tharness: opaque(\"pi.durable-harness@1\"),                               // native object, by name+version\n\t},\n});\n```\n\n- Input identity: normalize (apply defaults, strip unknown), validate, then canonical JSON with fields equal to their schema default omitted. Changing defaults or normalization requires a new type name.\n- Remote calls validate args/results and state values at the provider boundary. Local calls are not validated per call.\n- Local contracts list every member (`members: { search: method(...), harness: opaque(...), status: Schema }`). A reference that is still `loading` must know whether `h.x` is a method (a caller that waits until live), a state member (a readable) or an opaque value, before any implementation exists. Members missing from the contract are found only once the reference is live.\n- Every bundle embeds the contract tokens it was built against (its own copy). Fingerprint = hash of the canonical description.\n- Placement (remote types, required): `host: hostNamed(name)` (one host) or `hostFamily(prefix, key)` (the host `prefix` + the input's required string field `key`). Data only, in the description (and so the fingerprint and the `hold` token); there is no `at` option. A `get` normalizes the input with the caller's contract, resolves the host and acquires locally if that is this host, else remotely. `serve` (and a reload that loads a facet) refuses a type whose placement excludes the host's name. A host refuses a hold whose input its own contract places elsewhere (`invalid/placement`), and a relay refuses a hold whose target disagrees with its token's placement and input. Coverage requires equal placements, and an `accepts` adapter never bridges two placements: a moved type is a breaking change. A remote reference's address is the type, the normalized input and the principal: no host name.\n\n### Compatibility rule\n\nA consumer built against version M binds to a provider at version N iff N = M and the provider's description covers the consumer's; otherwise only through `accepts[M]` adapters declared by the provider (`serve(type, factory, { accepts: { 1: (current) => v1View } })`); else `contract_mismatch`.\n\nCovers: every consumer member exists with the same kind; methods: both take arguments or neither does, the provider accepts every argument the consumer can send (new fields optional, nothing newly required or retyped), and the provider's result fits the consumer's expected result (extra fields fine; a void consumer accepts any); state: provider value fits consumer schema (extra fields fine); opaque: same name@version.\n\nAdditive changes need no bump. Breaking changes (remove/rename/retype, newly required, or a change of meaning with the same shape) bump the number. Same version + non-covering description = forgotten bump, refused. Meaning changes without a bump are undetectable (author's responsibility, as with Durable document versions).\n\nChecked at: `get` binding, reload prepare (new provider token vs every surviving consumer holding the type and every loaded plugin importing it), remote references (consumer token in the `hold` frame).\n\nThe exact host-owned reference to a `lifetime: \"host\"` object is a lifecycle pin, not a surviving old-contract consumer. When its\nprovider is replaced, commit replaces that reference with one bound to the new provider contract, acquiring it before\nreleasing the old one so the same object, state and unchanged resources survive. It needs no `accepts` adapter\nfor its own previous contract. Prepare/abort do not change that reference. All other consumers retain their own tokens\nand the normal coverage/adapter checks; state-version and migration checks are unchanged.\n\n## 3. Factory and declarations\n\nFactories are synchronous; returning a promise throws. A declaration has a name only if something of it survives a reload and must be found again: state (its value) and resources (the open value), plus listeners (a kept subscription). Names are strings, unique per object per declaration kind (a repeated name is `invalid/duplicate`); kinds have separate names. Derivations and tasks have no names: a reload makes new ones. References are identified by their address.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 },  // schema required, initial value\n\t\t{ version: 2, migrate: (old, from) => upgrade(old, from) });                     // per cell; default version 1\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});                         // reference; identity = canonical address (+ at)\n\tconst watch = self.resource(\"watch\", {\n\t\tversion: 1, deps: [catalog], exclusive: false,\n\t\topen: async ({ signal }) => openWatch(session, signal),\n\t\tclose: async (w) => w.close(),\n\t});\n\tself.on(\"changes\", watch, (w) => w.events, (event, ctx) => status.change(ctx, (d) => { d.tasks = event.tasks; }));\n\tself.task(async (ctx) => { /* loop until ctx.signal aborts */ });\n\tconst visible = self.derive([status], (s) => project(s));      // synchronous\n\tself.register(Routes, { \"GET /status\": handler });              // registry keys are the registry's own keys; `lifetime: \"host\"` objects only\n\treturn { status: visible, errors, report: (args, ctx) => { status.replace(ctx, args); } };  // exposed state = what is returned\n}, {\n\tlinger: 30_000,                                                  // with lifetime \"referenced\" (the default) only\n});\n```\n\n| Declaration | Survives reload | Notes |\n| --- | --- | --- |\n| `state(name, schema, initial \\| { init }, { version?, migrate? })` | same cell and value (`initial` ignored, `init` not run); migrated when its version rises; a cell the reload adds runs `init` at prepare (gated objects: at the gate) | Schema required (no TypeScript at runtime). `change(ctx, draft => void)`, `replace(ctx, value)`, `value`, `subscribe`. Writes through `change` touch only mutated paths. Private state is a cell the implementation does not return. `init(ctx)` (may be async) gives the first value, see activation |\n| `get(type, input, { at? })` | kept if declared again (same address) | returns a reference |\n| `resource(name, { version, deps?, exclusive?, open, close? })` | kept unless `version`, `deps` or `exclusive` change; a kept resource adopts the new declaration (its `close`) at commit | `open` runs at prepare (non-exclusive) or at the gate (exclusive); returns a resource whose `.value` is the open value; a replaced resource closes with its own declaration's `close` |\n| `on(name, source, [select,] handler)` | same subscription while source and `select` (identity or source text) are unchanged; `handler` swapped | each event starts a fresh root in the current code version; `select` is not re-run for a kept subscription. State sources deliver their current value at start and after every reload that replaced them |\n| `task(fn)` | restarted with new code at commit: the old run is aborted via ctx.signal, and the new one starts once it finished (at most `taskStopDeadline`, default 5 s, then anyway, reported) | a code holder; calls inside start fresh roots |\n| `derive(sources, fn)` | a new derivation, computed at commit | synchronous fn |\n| `register(registry, { key: value })` | value swapped at commit | `lifetime: \"host\"` objects only |\n| `expose(resource)` | follows the resource | a local member (usually `opaque`) whose reference value is the resource's value once live |\n\nState members of an implementation may be cells, derivations, or state members of references the object holds (they are defined once the object is live). `cell.replace(ctx, value, { diff? })` publishes a structural diff when the new value shares containers with the old one (or `diff: true`), else `r`. Facet `setup` receives `{ serve, name, root, registry, signal }` of its host; `signal` aborts once this run of the facet is no longer installed (a committed reload replaced or removed it, the reload that loaded it aborted, or the host closed).\n\n`migrate` per cell is `(old, fromVersion) => new`. A cell may be several versions behind (author bumped more than once between reloads). Without `migrate`, a raised version keeps the value, which must fit the new schema.\n\n**Listeners on state reconcile.** A listener whose source is state (a cell, a derivation, a reference's state member) is subscribed when it starts and gets the current value, then every new value. A reload that replaces its source (every derivation is new; a reference to another address is another source) subscribes it again, so it gets the current value again: its handler must reconcile (be idempotent), not count events. An event source (a resource's events, any other `subscribe`) delivers no current value; a kept subscription to it is not touched by a reload.\n\n**Methods.** `method(args?, result?)`: without `args` the method takes none (callers `ref.name(ctx?)`, implementation `(ctx) => …`; `method(undefined, result)` for one that returns something); without `result` it is void (callers get `undefined`, whatever the implementation returned). The description records both (absent fields), and the `call` frame carries no `args` and the `result` frame no `value` for them. A consumer and provider must agree on whether a method takes arguments; a consumer without a result accepts a provider's.\n\nRemoved vs guide: `self.own`, `holdWhile`, async factories, inferred dependencies.\n\n### Synchronous factory, asynchronous activation\n\n`self.get` and `self.resource` only declare and return placeholders. Activation of an object:\n\n1. run the factory synchronously (declarations);\n2. await declared resources' `open()` (e.g. connect a DB);\n3. await declared references becoming live (dependencies activate first, coalesced);\n4. run the `init` of every cell declared with one, in parallel, each in a root of its own acting as the host principal; validate and install the values;\n5. attach derivations, start listeners and tasks;\n6. object is live: references `live`, `ready` resolves.\n\nInside the factory, placeholders must not be used (`resource.value` and an `init` cell's `value` throw `not_ready`; reference calls throw). An `init` that throws, or returns a value its schema refuses, fails the activation like a failed `open`. An `init` must not wait for its own object or one that holds it (it is not live yet), and must not read another `init` cell of its object (they run in parallel). `init` gives a first value from a source the object reads anyway; it does not persist anything, and a cell is not saved anywhere by having one. Methods/listeners/tasks run only on live objects, so their dependencies are live. An object goes live only once its references are ready (live and, for remote ones, hydrated) and the state sources it exposes or derives from have their values, also where they forward a remote reference held some other way; waiting for references is bounded by the host's `readyTimeout` (default 60 s, `unavailable/ready_timeout`, also for a reference cycle across hosts). External consumers holding a `loading` reference (`host.hold`): calls wait until live (ctx cancellation, deadline); state members throw `not_ready` until the first hydration. Activation failures propagate to dependents and all waiting gets; the next get retries.\n\n### Lifetime\n\n`serve(type, factory, { lifetime })`:\n\n| `lifetime` | Created | Ends | `linger` |\n| --- | --- | --- | --- |\n| `\"referenced\"` (default) | at the first `get` of its address | after the last reference is released, plus `linger` | allowed (default 0) |\n| `\"host\"` | when its facet loads (or at `serve`); loading waits until it is live | when the host closes, or a reload removes its facet | refused |\n\n- `\"host\"` is refused at `serve` for remote types, for an input that does not accept `{}`, and together with `linger`;\n  the types (`ServeOptionsFor`) refuse the same at compile time, where such an input is one with a required field.\n  The type has exactly one object: the empty input (defaults applied), created for the host principal. Every `get`\n  reaches it, whatever its principal (the factory sees the host principal); a `get` with another input is\n  `invalid/input`. Other references do not change its lifetime: the host keeps its own until it closes.\n- Only `\"host\"` objects may register (`invalid/not_host_lifetime` otherwise).\n- A `\"host\"` object whose activation fails is started again by the next reload that changes its type. A reload that\n  makes a type `\"host\"` creates its object; one that makes it `\"referenced\"` releases the host's reference, so the\n  object lives on only while others reference it.\n\n### Host objects readiness\n\n- `host.install(facet, { awaitHostObjects? })` installs declarations synchronously and returns a promise for this\n  facet's host object activations. `host.load(facets, { awaitHostObjects? })` installs the entire batch before awaiting\n  its host object activations. `awaitHostObjects` defaults to true. Unreferenced objects are not activated.\n- Factory, resource-open, dependency and activation-finalization failures reject the returned promise with the\n  activation error (ChordError codes preserved; ordinary errors wrapped as `unavailable`). Host object failures also\n  reach `onError`. Installation is not a rollback transaction; setup/registration errors from `install` throw\n  synchronously. Abandoned startup should close the host.\n- `{ awaitHostObjects: false }` starts the same host objects but resolves after installation, without waiting for\n  their activation. `host.ready()` awaits a snapshot of the host's references to its host objects at call time\n  (including direct `serve` and deferred loads), rejects their activation failures, and does not acquire additional\n  references. It is not an ongoing health check and does not await task completion or remote state hydration.\n- Local bootstrap readiness and upstream admission are separate. A worker connects its upstream link, loads with\n  `{ awaitHostObjects: false }`, awaits its local bootstrap reference (e.g. Harness, with no upstream dependency), then\n  announces control readiness so its parent can accept the link. Only after admission does it await `host.ready()`.\n  Otherwise a host object reporting to the server's SessionStatus deadlocks with the parent waiting for control\n  readiness. Deferred loads skip the whole barrier, not selected dependencies; callers explicitly choose local\n  bootstrap references, keep bootstrap busy until full readiness, and propagate later startup errors.\n\n## 4. Principals, variants, references\n\n- Principals are the app's type `P` (`createHost<P>`, `Host<P>`, `defineFacet<P>`, `Factory<C, P>`, `Ctx<P>`; default `unknown`). Chord invents none: `createHost({ self })` is the principal of the host's own code (roots, code inside objects, `lifetime: \"host\"` objects), `connect(link, { principal })` what the parent's own requests act as, `accept(link, { principal })` what a link acts as, `Route.principal(hostName)` what a routed host acts as when it calls back. All are required.\n- `createHost({ principalKey })` maps principal to a key that includes connection provenance and grant revision. Objects are per principal key. The host's own principal is recognized by key (`principalKey(p) === principalKey(self)`), not by object identity, so an equal principal never creates a second object.\n- Remote factories return `{ variant, ...members }`; `variant` must be a declared variant; members must match it. Local factories return any object. `variant` is reserved as a member name.\n- Reference: `status` (`loading | live | released | gone`), `ready` promise, `variant`, `shape` (`{ variant, members, contract }`), `onStatus(listener)` (every status change, with the error for `gone`), late-bound members, `release()`. `self.get` is synchronous (the object goes live after its references are ready); `ctx.get` and `host.get` resolve once the reference is ready: live, and every state member of its variant hydrated (`state: false`: live only). They reject with the activation error, `aborted` (ctx aborted or settled, `signal`), or `unavailable/ready_timeout`, releasing only that reference. `host.hold` is the synchronous form for UI runtimes. A held remote reference stays subscribed to its variant's state until released; after a link loss its last values stay readable while it re-holds. State members are typed `T`; reading before the first hydration throws `not_ready`, outside the variant `not_in_variant`, after the end the end's error. A remote reference type is a union discriminated by `variant`. Shape changes (reload, variant) are pushed; members missing from the current variant reject `not_in_variant`; released references reject `released`; forced disposal makes references `gone`.\n- Shared bindings: remote references on a host with the same key (type, normalized input identity, principal key, consumer contract fingerprint) are views onto one binding: one hold, one stream per state member, calls over that hold. Views have their own owner (object, call, external), status, observers and `release`; the binding ends (gone) or re-holds for all of them. Without views a binding closes, or lingers for `linger.types[typeName]` ms (`createHost({ linger: { types, cap } })`, at most `cap`, default 4, least recently released first) while staying held and hydrated.\n- Methods are `(args, ctx) => Promise<result>`. Factories still check arguments; variants are not complete authorization.\n- Revocation: `host.invalidatePrincipal(match)` force-disposes matching objects on this host and propagates over links; links re-stamp or close.\n\n## 5. Lifetime\n\n- Reference owners: object (declared `get`), call (`ctx.get`, released at settle), connection (remote references; relay maps per origin connection), host (`lifetime: \"host\"`), UI.\n- Activation coalesced per address; if all requesters leave before it completes, the result is disposed. Factory throw: unwind declarations, reject gets, next get retries.\n- Disposal after the last reference is released + `linger`: tasks aborted, listeners removed, resources closed (reverse order; all run; errors aggregated), the object's references released (cascade).\n- Bulk forced disposal (`host.close`, `invalidatePrincipal`) goes in waves, referrers before what they reference, so a resource's `close` can still use its object's references; `host.close` closes links after the objects.\n- Forced disposal: references `gone` immediately, new calls rejected, in-flight call contexts aborted, then cooperative cleanup.\n- Reference graph acyclic in-host (checked at get); across hosts by convention.\n- Links have ping/timeout; a dead link releases everything held through it.\n\n## 6. Roots and code versions\n\n- `Ctx = { signal, rootId, codeVersion, get(...): Promise<Ref>, principal? }`. Explicit; no AsyncLocalStorage dependency (may be used in dev to warn).\n- A link creates a root for a request that carries none; nested remote calls carry `{rootId, codeVersion}`.\n- Dispatch: a call with code version E runs the object's implementation for E. A host retains at most `current` and `previous` code versions (two-version rule). A call for a code version the host doesn't have (older than previous, or host started later) is rejected `superseded` (retryable by starting a new root). Never substitute another code version.\n- Browsers never carry code versions into the server: browser calls start roots at the server. Browsers learn of changes via shape/reset pushes and a server `Manifest` object; incompatible contract fingerprint -> `upgrade_required`.\n\n## 7. Reload\n\nExplicit trigger (`reload_plugins` tool, button, API). Serialized; no nesting.\n\n1. Build/type-check gate (application tooling, outside Chord): dependency-closed set; refuse breaking remote contract changes without a contract version bump.\n2. Load: on every affected host, import new facet modules (cache-busted).\n3. Prepare: for each live object of each affected type, run the new factory in recording mode: existing cells readable, writes throw; listeners/tasks/registrations recorded, not started; references recorded; non-exclusive resources whose name is new or whose `version`/`deps` changed are opened as candidate-owned (closed on abort). Validate keys, variants, registry collisions. New objects activated during prepare use the current code version (old code). Any failure: abort, discard candidates. Vote.\n4. Gate (only objects with a cell whose version was raised or which replace an exclusive resource, plus objects depending on those resources):\n   - Admission cut: from now, an old-code-version root that has not touched the object gets `upgrade_conflict` (retryable), not queued. Roots that already hold an activation lease on it are grandfathered; their re-entry is admitted.\n   - Wait for grandfathered activations to finish, deadline (default 5 s). Missed deadline: abort the reload, reopen, nothing changed (report blocking roots).\n   - Exclusive resources: close old, open new; on failure reopen old and abort.\n   - Run `migrate` into private copies; validate.\n5. Commit (server commits itself, then sends commit to children; the decision is not persisted, see section 12; FIFO links guarantee no host receives a new-code-version root before committing): each host in one synchronous step installs migrated cell values and cell versions, installs implementations as code version E', swaps registry tables, restarts tasks with new code, rebinds listeners, pushes shape/reset to subscribers, opens gates. E' current, E previous.\n   The coordinator's result carries `children`: each participating child's prepared report (its vote).\n6. Retire: when no roots of `previous` remain (and, on a child, the parent retired it), drop its implementations and release declarations only it declared. The next reload first waits for that, at most `retireDeadline`; then it aborts the roots still running on `previous` (their ctx signals), gives them up to 1 s to stop, and retires anyway. A coordinator retires before it sends `prepare`, so its `retire` frames reach the children first.\n\nCompatible changes (no state bump, no exclusive resource change) skip the gate: no waiting.\n\nState compatibility rule: reload prepare compares each cell's old and new schema and version. Unchanged: compatible (no gate). Version raised: gate + `migrate` (or the kept value), validated against the new schema. Schema changed without a raised version: reload refused. A lowered version: refused. A change of meaning with an identical schema needs a bump too (undetectable; author's responsibility). Initial values and migrate outputs are validated; per-write validation in dev mode.\n\n## 8. Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\nself.register(Routes, { \"GET /download\": handler });       // in a `lifetime: \"host\"` object; keys are the registry's keys\nhost.registry(Routes).get(\"GET /download\", ctx);            // table of ctx.codeVersion (or current without ctx)\nhost.registry(Routes).subscribe((table, codeVersion) => ...);     // bridges for imperative consumers (Durable)\n```\n\nPer-code-version immutable tables; order = first registration order, kept on replace. An application can implement Durable's `RegistryReader` from the table (snapshot per code version) so phases pin a complete snapshot; Chord does not ship that bridge.\n\n## 9. Hosts and links\n\n- `createHost({ name, self, principalKey, upstream?: { link, principal } })`; `host.accept(link, { principal })` for children; `host.connect(link, { principal })` for the parent; `host.route(prefix, { authorize(principal, hostName), resolve(hostName) => Promise<Link>, principal(hostName) })`.\n- Links: `memoryLinkPair()` (tests), Node IPC (child_process), WebSocket (server `ws`, browser `WebSocket`).\n- Principal stamping: the accepting side stamps the principal of what connects (from its authenticator); forwarded principals accepted only from the parent link; the parent's own requests (no forwarded principal) act as the principal given to `connect`; payload principal fields ignored.\n- A child that connects behind its parent's code version is fenced (nothing routed to it) and reconciled by a reload of its modules to the parent's code version. Calls its roots of the old code version make upstream are admitted in the parent's current code version (the parent never ran that code version), not rejected `superseded`.\n- Relay: server forwards a hold to the child link of its target host (`at`, resolved by the consumer from the contract's placement and the normalized input, and checked against both), mapping references per origin connection; authorizes before resolving/spawning and re-checks after.\n- Wire frames (JSON): `hello`/`bye`, `hold`/`release` (a reference over a link: `hold` returns `shape` or `gone`; there is no separate `get`), `shape`, `call`/`result`/`cancel`, `sub`/`unsub`/`resync` and `state`, `gone`, `root_end`, `retire`, `invalidate`, `ping`/`pong`, `ack`, reload control (`prepare`/`vote`/`commit`/`abort`).\n- State: a `hold` implies the state of its variant (`state: false`: none until `sub`; `unsub` stops it). The provider answers with units: a `shape` whose `state` lists the members the consumer lacks, followed by one `state` frame `as: \"snap\"` per listed member; the consumer applies the shape once those snapshots are here. Later `state` frames of a member (addressed by hold id and member `m`) are `ops` (per-stream delta encoder), `reset`, or `error`; a `snap`/`reset` without `value` carries `undefined`. A failed snapshot before the first hydration ends the reference with its error. A reload or variant change sends a unit with the members that are new; members that left stop. `again` marks a re-hold after a link loss (the consumer has values). `origin` names the client connection a hold comes from (stamped by the host that accepted the client, forwarded by relays, honored only from the parent link).\n- Protocol version: `hello` carries `protocol` (`PROTOCOL_VERSION`, now 4; absent means 1). A side that sees another version processes nothing more, calls `LinkOptions.onProtocolMismatch({ local, peer })` and closes the link with reason `protocol-version <its version>`; the other side's connection reads that reason and reports the mismatch too. Peers do not negotiate: the outdated side must load the current code (a browser reloads its page). Frame fields follow the version: the `shape` frame names the variant `variant`, code versions travel as `codeVersion` (2); remote contract tokens carry their placement (`host`), and a `hold` carries the input normalized by the consumer (3); a hold implies its state, units, `origin`, and `shape` without a code version (4).\n- Flow control: both ends announce `acks` in their hello and acknowledge what they processed (`ack`, a cumulative count of every frame but `ack`). While the frames not yet acknowledged exceed the window (`window`, default 256 KiB of JSON text), state waits at the sender in slots flushed in creation order: one per member stream (ops are appended, a snapshot or reset replaces it, a release or `unsub` drops it) and one per hold for a unit (lazy: built from the current values when sent; a newer unit merges into it). Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams become a reset of their current value built at flush (a relay sends `resync` downstream and drops that member's ops until the reset arrives). The first unit of a hold that is not `again` and every frame but state are sent at once. Links that report no frame sizes (IPC) are never windowed; a peer without `acks` is neither windowed nor acknowledged. An acceptor can limit the calls a peer has in progress (`maxCalls`); calls beyond it are answered at once with `unavailable/too_many_calls`. Nothing outside the connection may add frames to a link: acknowledgements count them.\n\n## 10. Host manager (library)\n\n`createHostManager({ driver, keepAlive })`: fork driver (entry, init, lock, env), serialized start/stop/replace, exit-before-replace, process-tree kill after grace, control channel (ready/fatal/shutdown), liveness callback (booting, activeCalls, viewers with heartbeats, durable tasks, idleFor).\n\n## 11. Test coverage\n\nThe tests cover at least: lifetime/references/linger/forced disposal/gone; activation coalescing and abandonment; principals and variants; remote calls over memory links incl. relay and principal forgery attempts; state snap/ops/reset over a link with reconnect; registries per code version; reload: compatible (no wait, pinned roots), prepare isolation (writes throw, failing factory leaves everything unchanged), state bump with migrate and gate, `upgrade_conflict` for late old roots, deadline abort, exclusive resource replacement, cross-host commit ordering, two-version retirement and `superseded`.\n\n## 12. Known limitations and decisions\n\n### Still open\n\n- **Retirement across hosts.** A child retires a code version only after its parent retired it (`retire` frame) and its own roots of that code version ended; the server decides from its own roots. A child that comes in step (hello or reconciliation) is told which code versions the parent retired before. A root started on a worker that calls up to a server which has already retired the code version both had still gets `superseded` (retryable).\n- **The commit decision is not persisted** (section 7, step 5). A server crash between its own commit and a child's commit leaves that child on the old code version until it reconnects and is reconciled.\n- **One level of reload participants.** Only the coordinator's direct child hosts take part; a child does not forward `prepare` to its own children.\n- **Reconciliation needs the child's module list.** It is remembered from cluster reloads (`host.remoteModules`), so a server restart forgets it, and a child behind a restarted server then reloads no modules: it gets the code version id with its own code. The reconciliation vote deadline is fixed at 30 s; a failed reconciliation closes the link, so the host manager restarts the worker.\n- **Links do not resume.** The provider releases everything held through a closed link at once, so re-held objects are new ones. Keeping references for a reconnect window would need session ids on links. Calls in flight when a link drops reject `unavailable`; whether they ran is unknown.\n- **Refused clients cannot tell.** The WebSocket adapter closes unauthenticated sockets; the client sees a dropped upstream and its calls wait for a reconnect until their ctx is aborted.\n- **The publication queue is process-wide and synchronous.** A slow observer delays every later notification in the process, including those of other hosts in the same process.\n- **No automatic restart.** A terminal host failure is picked up by the host manager's `keepAlive` or `fatal` path, not inside Chord.\n- **The Node loader leaks.** Every reload imports fresh module copies that are never evicted, and it does not isolate plugins (`TODO(loader)`).\n- **Stale socket locks.** On Unix systems other than Linux, two processes that probe the same stale socket file at the same moment could both take the lock. Managers serialize starts per lock name.\n- **Listener `select` identity.** A listener's subscription is kept across factory runs when its origin and `select` are unchanged; `select` counts as unchanged when identical or with the same source text, so a `select` whose implementation depends on captured values that change between runs is not detected.\n- **Shutdown and dead links.** `host.close()` disposes objects in waves (referrers before what they reference) and closes links last. A `close` that awaits a remote call over a link that is already down waits for a reconnect; bound such calls.\n\n### Decisions\n\n- **Errors.** The guide's codes; a `reason` field carries the detail, for example `invalid/args`, `invalid/cycle`, `invalid/duplicate`, `invalid/read_only`, `invalid/not_host_lifetime`, `invalid/registry_conflict`, or `unavailable/unknown_type`. A `ChordError` thrown by a factory or a dependency keeps its code through every dependent; other errors become `unavailable`, with the original as `cause`.\n- **Reload errors.** There is no `reload_aborted` code. A failed reload throws the cause's code and reason with the message `reload aborted: …` (the cause is attached). Reasons of its own are `aborted/gate_deadline` (with the blocking roots in `details`), `aborted/deadline`, `aborted/vote_deadline` and `unavailable/reload_running`. Across hosts, the message names the host that failed. A child cannot reload on its own (`unavailable/not_coordinator`).\n- **New objects during prepare.** A reload pre-allocates the next code version (\"pending\"). Types the same reload adds are served from it; their objects install implementation only for the pending code version and are force-disposed if the reload aborts.\n- **Gate admission.** From the gate until commit or abort, creating a new object of a changed type fails with `upgrade_conflict` (retryable, reason `gate`); nothing is paused, so nothing can deadlock. Gated objects get `minCodeVersion = E'`: calls and writes carrying an older code version are rejected `upgrade_conflict` permanently, and a gated object drops its previous-code-version implementation at commit. Only objects whose cell versions or exclusive resources change are gated.\n- **Tasks and listeners during the gate.** Tasks of gated objects are aborted at the admission cut and restart with new code at commit, or with the old code on abort. Listener events for gated objects are queued and replayed through the new listener at commit, or the old one at abort.\n- **Grandfathered roots** are the roots that called into the object and are still running at the cut. Every host remembers on which links a root's calls went out and sends `root_end` on them when the root ends, so leases outlive calls across hosts.\n- **Removed types and missed retirement.** Objects of removed types are force-disposed at commit. When a retirement deadline is missed, retirement is forced: those roots are aborted first (their ctx signal, reason `superseded`; a relayed call's root cancels its downstream call), then what only that code version declared is closed, and their next call is `superseded`.\n- **Joining during a reload.** Participants are the child hosts in step when a reload starts. A child whose hello arrives while it runs is decided when it ends: in step, or reconciled to the committed code version; a child already behind is fenced at once, so the calls its roots make meanwhile are admitted in the parent's code version. A reconciliation that is running when a reload starts syncs its child only after that reload ended, against the code version it committed. A reconciliation announces the parent's retired code versions before its `prepare`.\n- **Two-version rule.** An object of a changed type created after a commit has only new code; a previous-code-version root calling it gets `superseded`.\n- **Validation.** Remote calls are validated at reference dispatch, in-host too: a direct binding against the provider's schema, a binding through an adapter against the consumer's. A provider validates each snapshot and reset of a remote state member once per (re)subscription, against the consumer's contract through an `accepts` adapter and the provider's otherwise; ops are validated only in dev mode. At reload, the new provider's description must cover the old provider's when the version is unchanged, in addition to the check against live consumers and facets' `uses`.\n- **Contracts.** Local contracts list their members, so a loading reference knows whether a member is a method, state or opaque. Canonical identity omits input fields equal to their default; with TypeBox, use `Type.Optional(Type.X({ default }))` to let callers omit them. Provider variants must cover consumer variants; members the consumer's contract does not list are hidden. Schema change detection compares canonical JSON, so reordering `anyOf` branches counts as a change. Fingerprints use a non-cryptographic 64-bit hash: synchronous and browser-safe; contracts are trusted.\n- **Principals.** A root ctx at ingress acts as its principal (`host.root(fn, { principal })`); contexts inside objects act as `host`. The acceptor decides a link's kind and name (`accept(link, { principal, kind, name })`); only configured host links are addressable, carry roots and may vote, and control frames are accepted only from the parent. Clients never carry roots or code versions. Relaying needs a matching `route`; `authorize(principal, hostName)` runs before `resolve` and again after, and revoking or re-stamping a principal closes its relays at once. `invalidatePrincipal(match)` re-stamps or closes matching links and propagates pattern matches to child hosts.\n- **Cells and readables.** A value returned from `change` replaces the value and is always validated; `replace` transfers ownership without copying; by default (`{ diff }` overrides) it publishes a diff when the new value reuses a container of the old one at the same key, else `r`. `subscribe(fn)` delivers values, starting with the current one; `events(fn)` delivers `snap`, then `ops`, or `reset`. Reference state members are stable readables that throw `not_ready` while loading and `not_in_variant` outside the variant, and reset when their source moves; a listener added before they are readable gets its snapshot then. Remote state members stay hydrated while the reference is held.\n- **References and calls.** A reference's identity is its address; a second `self.get` of the same address in one run returns the same reference. A reference call without ctx, or with a task ctx, starts a fresh root in the current code version; a ctx whose call or root already settled is refused (`aborted/expired`), one from another host `invalid/foreign_ctx`. Calling a reference inside a factory throws `not_ready`.\n- **Resources and disposal.** New and replaced exclusive resources open at the gate, and so do non-exclusive resources of the same object that depend on them. A new object for an address that is still being disposed waits for that disposal. A resource open that completes after its signal aborted closes its value at once.\n- **Links and the host manager.** Every received frame is validated against its schema; malformed frames, duplicate ids and control frames from the wrong side close the link. Each connection pings every `pingInterval` and closes after `pingTimeout` without a frame. The child holds a kernel lock (an abstract socket on Linux and Android, a named pipe on Windows, a probed socket file elsewhere), acquired by `childControl().lock()` before storage opens. The manager kills the child's process group on every exit path and derives `booting` and `idleFor` from the child's liveness reports. WebSocket support is structural, so Chord does not depend on `ws`.\n- **Flow control holds back state only.** A relay (Cloudflare to a browser) can hide a slow reader, so transport backpressure cannot be relied on; acknowledgements are end to end. Results must not wait behind state, so links are FIFO except that waiting state frames can be overtaken: a result may arrive before state changes made before it. Frames of one subscription stay in order. Waiting state is coalesced rather than queued and capped per link, so a stalled peer costs bounded memory, and the next frame it gets is current. The first unit of a new reference is not held back, so a `get` never waits behind other streams.\n- **The state API stays.** Objects keep named cells and unnamed derivations; giving each object exactly one state value was rejected. Do not reopen this without a new, concrete problem.\n","isInternal":false,"tokens":9580,"sizeBytes":43267},{"name":"tutorial.md","path":"packages/chord/docs/tutorial.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/chord/docs/tutorial.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Build a shared todo list with Chord\n\n## 1. What problem does Chord solve?\n\nA todo list has data, operations on that data, and several people looking at it.\nYou could hand-write object creation, subscription cleanup, messages between\nprocesses, and code replacement. Chord connects these pieces for you.\n\nA **host** is the runtime that owns objects. An **object** combines state with\nmethods at an address, such as “the family todo list.” A **reference** is a caller's\nreference to that object, not the object itself. Several references can reach the\nsame object. References can also reach another host over a connection.\n\n```text\nbrowser host                         server host\nref + subscribed rows -- link --> Todos view (owner or guest)\n                                           |\n                                        ref\n                                           v\n                                   TodoStore: rows + methods\n```\n\nWe will start inside one host, add observable state, then share it with a\nbrowser. Finally we will add background work, plugins, and hot reload.\n**Live state** means subscribers receive new values when data changes, without\nasking for it again.\n\n<!-- example:01 -->\n```ts\nconst host = createHost({ self: \"server\", name: \"todo-server\" });\nassert.equal(host.name, \"todo-server\");\nawait host.close();\n```\n\nThe assertion passes: the host's name is `todo-server`. Creating a host does\nnot create a todo object, start a server socket, or persist data. `close()`\nshuts down its objects and connections.\n\n> **Tip:** These are executable TypeScript examples, using assertions to show\n> results. Shared imports are at the end. Later sections reuse earlier contracts\n> and functions, but each section's test starts with fresh hosts. Test-only\n> cleanup closes hosts even when an assertion fails; snippets omit that cleanup.\n\n## 2. Your first object\n\nBefore callers can use an object, they need to agree on its name and members.\nThat agreement is a **contract**: a runtime value that also gives TypeScript its\ntypes. A **schema** describes a value's shape. We use TypeBox's `Type` to write\nschemas. `method(args, result)` declares a method by its argument and result\nschemas: `method()` takes no arguments and returns nothing, and\n`method(undefined, Type.String())`, used below, takes no arguments and returns a\nstring.\n\nStart with a label for a named list. `local` means only code inside this host\ncan reach it. The input `{ list: \"family\" }` identifies which list we want;\nit is not an argument to `label`.\n\n<!-- example:02-contract -->\n```ts\nconst TodoLabel = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { label: method(undefined, Type.String()) },\n});\n```\n\nA **factory** is a synchronous function that creates the object's implementation:\nthe methods and state callers will see. `serve` registers a contract and its\nfactory. Registration alone does not activate this lazy object; the first\n`get` does.\n\nA **root** is one short tree of calls, such as the work for a button click.\n`host.root` starts it. Its **context**, called `ctx`, carries cancellation,\ncaller identity, and the code version for nested calls. Pass it to methods you\ncall from that root.\n\n<!-- example:02-call -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoLabel, ({ list }) => ({ label: () => `Todos for ${list}` }));\nconst result = await host.root(async (ctx) => {\n\tconst todos = await ctx.get(TodoLabel, { list: \"family\" });\n\treturn todos.label(ctx);\n});\nassert.equal(result, \"Todos for family\");\n```\n\nThe result is `\"Todos for family\"`. `await ctx.get` resolves once the object is\nlive (and, as we will see, has its state). The reference acquired through\n`ctx.get` is released when this root ends. We never receive the factory's\nreturned object directly. A standalone call without `ctx`, such as\n`todos.label()`, starts its own root. A method without arguments takes only\nthe context: `todos.label(ctx)`; its implementation is `(ctx) => …`. Inside a root or method, pass the\nexisting `ctx` so nested calls stay in the same call tree.\n\nThe factory may also receive `principal` (who is asking) and `self` (the\nobject's declaration API). We did not need them yet.\n\n> **Why:** The reference separates callers from implementations. Later, replacing\n> code will not require replacing every caller's reference.\n\n## 3. Give the list live state\n\nA label is not a todo list. We need rows that can change, and a count that always\nagrees with them. A **cell** owns an observable JSON value. A **derivation**\ncomputes another observable value from cells or other readable sources.\n\nExtend the contract with `rows`, `openCount`, and editing methods. State\nmembers describe values, not methods. This adds members at contract version 1;\nolder callers that only need `label` still have everything they need.\n\n`self.state(name, schema, initial)` declares a cell and returns it. The name\n`rows` is important: on reload, Chord matches that name to the existing cell and\nkeeps its value. `self.derive(sources, fn)` declares a derivation; it has no\nname, because it holds nothing a reload must keep.\n\n<!-- example:03-contract -->\n```ts\nconst Todo = Type.Object({ text: Type.String(), done: Type.Boolean() });\nconst Rows = Type.Array(Todo);\nconst TodoStore = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: {\n\t\tlabel: method(undefined, Type.String()),\n\t\trows: Rows,\n\t\topenCount: Type.Number(),\n\t\tadd: method(Type.Object({ text: Type.String() })),\n\t\tfinish: method(Type.Object({ index: Type.Integer() })),\n\t\treset: method(Rows),\n\t},\n});\n```\n\nThe factory now declares a cell and a derivation. It is wrapped in `todoStore`,\nwhich takes the cell's options (section 9 raises its version); for now\n`todoFactory` uses the defaults. Each method receives its arguments and a\ncontext; changing the cell updates its readers and its derived count together.\n`add`, `finish` and `reset` declare no result, so they return nothing.\n\n<!-- example:03-factory -->\n```ts\n/** The store's factory; `options` versions its rows (section 9). */\nconst todoStore =\n\t(options?: StateOptions<Static<typeof Rows>>): Factory<typeof TodoStore> =>\n\t({ list }, _principal, self) => {\n\t\tconst rows = self.state(\"rows\", Rows, [], options);\n\t\tconst openCount = self.derive([rows], (todos) => todos.filter((todo) => !todo.done).length);\n\t\treturn {\n\t\t\tlabel: () => `Todos for ${list}`,\n\t\t\trows,\n\t\t\topenCount,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tdraft.push({ text, done: false });\n\t\t\t\t});\n\t\t\t},\n\t\t\tfinish: ({ index }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tconst todo = draft[index];\n\t\t\t\t\tif (!todo) throw invalid(\"args\", \"No such todo\");\n\t\t\t\t\ttodo.done = true;\n\t\t\t\t});\n\t\t\t},\n\t\t\treset: (next, ctx) => {\n\t\t\t\trows.replace(ctx, next, { diff: true });\n\t\t\t},\n\t\t};\n\t};\nconst todoFactory = todoStore();\n```\n\n`change(ctx, fn)` gives `fn` a temporary mutable **draft**. Here `add`\npushes a row and `finish` changes one field. The draft is discarded if the\ncallback throws. The callback must be synchronous.\n\n`replace(ctx, value)` replaces the whole value. We use it for resetting a\nlist we own completely. `{ diff: true }` asks Chord to publish just the\nstructural changes. Without that option, replacement diffs automatically when\nthe new value shares containers at the same keys with the old one; unrelated\nvalues normally publish a whole-value replacement. Either way, subscribers\nreceive the same resulting value.\n\n<!-- example:03-call -->\n```ts\nconst host = createHost({ self: \"server\", dev: true });\nhost.serve(TodoStore, todoFactory);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nconst counts: (number | undefined)[] = [];\nconst stop = todos.openCount.subscribe((value) => counts.push(value));\nawait host.root(async (ctx) => {\n\tawait todos.add({ text: \"Buy milk\" }, ctx);\n\tawait todos.finish({ index: 0 }, ctx);\n\tawait todos.reset([], ctx);\n});\nassert.deepEqual(counts, [0, 1, 0]);\nassert.deepEqual(todos.rows.value, []);\nstop();\ntodos.release();\n```\n\nThe count subscriber sees `0, 1, 0`: initially empty, one unfinished todo,\nthen no unfinished todos. Resetting to empty does not add another count\nnotification because the derived count is still zero. The final rows are `[]`.\n`subscribe` calls back immediately with the current value and returns an\nunsubscribe function. The awaited `get` makes sure there is a value: a reference\nyou hold is live, with its state.\n\n> **Why:** Store reloadable state in cells, not closure variables. Treat\n> `cell.value`, `ref.rows.value`, and subscriber values as immutable.\n> Only mutate drafts inside `change`; do not keep a draft for later use.\n> A derivation runs synchronously, so use it for projections, not database work.\n\n## 4. Let objects use objects and resources\n\nSaving a todo may require both the list and a database connection. A\n**reference** keeps the object it refers to alive. `self.get` declares a\nreference owned by this object; it lasts longer than a single method call.\n\nA **resource** is something the runtime opens and later closes, such as a\ndatabase connection. Its `open` may be asynchronous even though the factory\nmust be synchronous. The example database is deliberately fake: it just saves\nstrings in memory.\n\n`opaque<T>(\"name@version\")` describes a native, host-only member. It is not\nJSON state and cannot be sent over a link. `self.expose` connects that member\nto a declared resource, without reading the resource during the factory.\n\n<!-- example:04-database -->\n```ts\nconst Database = local(\"tutorial.database\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { db: opaque<{ saved: string[] }>(\"tutorial.database@1\") },\n});\nconst SavedTodos = local(\"tutorial.saved-todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { add: method(Type.Object({ text: Type.String() })) },\n});\nconst host = createHost({ self: \"server\" });\nlet opens = 0;\nconst closed = Promise.withResolvers<void>();\nhost.serve(Database, (_input, _principal, self) => {\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async () => {\n\t\t\topens++;\n\t\t\treturn { saved: [] as string[] };\n\t\t},\n\t\tclose: () => closed.resolve(),\n\t});\n\treturn { db: self.expose(db) };\n});\n```\n\nThe factory returns an exposed marker, not an opened connection. Chord runs\n`open` after declarations finish. The promise named `closed` lets the test\nobserve eventual cleanup.\n\nNow declare `SavedTodos` as an object that holds both the store and the database.\nIts editing method uses these dependencies only after activation.\n\n<!-- example:04-saving -->\n```ts\nhost.serve(TodoStore, todoFactory);\nhost.serve(\n\tSavedTodos,\n\t({ list }, _principal, self) => {\n\t\tconst database = self.get(Database, {});\n\t\tconst todos = self.get(TodoStore, { list });\n\t\treturn {\n\t\t\tadd: async ({ text }, ctx) => {\n\t\t\t\tdatabase.db!.saved.push(text);\n\t\t\t\tawait todos.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t},\n\t{ linger: 0 },\n);\n```\n\nAn outside consumer holds a `SavedTodos` reference. We also hold `Database`\ndirectly so we can inspect what was saved.\n\n<!-- example:04-call -->\n```ts\nconst saved = await host.get(SavedTodos, { list: \"family\" });\nconst database = await host.get(Database, {});\nawait host.root((ctx) => saved.add({ text: \"Buy milk\" }, ctx));\nassert.deepEqual(database.db!.saved, [\"Buy milk\"]);\nassert.equal(opens, 1);\nconst statuses: string[] = [];\nsaved.onStatus((status) => statuses.push(status));\ndatabase.release();\nsaved.release();\nawait closed.promise;\nassert.equal(saved.status, \"released\");\nassert.ok(statuses.includes(\"released\"));\n```\n\nAfter the method, the fake database contains `[\"Buy milk\"]`. It opened once,\neven though two objects referenced it. `SavedTodos` and `Database` become live\nonly after their resources and declared dependencies are ready. That is why\n`database.db!` is safe inside this live object's method: activation has\nalready established that dependency. An outside caller awaits its `get`.\n\nWhen both external references are released, `SavedTodos` is disposed and\nreleases its own database reference. With no references left, the database closes; we\nwait for `closed.promise` to prove it. The released reference stays\n`released`, and `onStatus` reports that transition. `onStatus` watches\nchanges; unlike state subscription, it does not deliver an initial status.\n\n`linger` is a delay in milliseconds after the last reference is released. Zero disposes\nwithout a grace period; a positive value can avoid reopening an expensive\nresource when a screen is briefly hidden. Linger is not persistence.\n\n> **Tip:** A factory only declares dependencies. Never call a referenced object's method or\n> read `resource.value` there, including during reload. Activation happens\n> afterward. Bump a resource's `version` when its configuration should be reopened.\n> Disposal stops tasks and listeners, closes resources, then releases references.\n\n## 5. Who is asking? One store, two views\n\nOwners may add todos; guests may only read them. A **principal** is the\napplication-defined identity of a caller. A **variant** is a named set of\nmembers a remote object offers to that principal. It lets the UI discover\nwhether an editing method is available.\n\nDo not create a separate data cell for each viewer: their lists would diverge.\nInstead, each view holds the same host-local `TodoStore` and exposes its rows.\n`self.get` acts as the host's own principal, so owner and guest views share\nthat store. Remote view objects themselves are separate per principal key.\nA remote contract also says where its objects live: `host: hostNamed(\"server\")`\nputs every `Todos` object on the host named `server`. Section 6 uses that.\n\n<!-- example:05-views -->\n```ts\nconst Todos = remote(\"tutorial.todo-view\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\thost: hostNamed(\"server\"),\n\tvariants: {\n\t\tviewer: { state: { rows: Rows } },\n\t\teditor: {\n\t\t\tstate: { rows: Rows },\n\t\t\tmethods: { add: method(Type.Object({ text: Type.String() })) },\n\t\t},\n\t},\n});\n\n/** Who may ask: the owner, a guest, or the server's own code. */\ntype Principal = \"owner\" | \"guest\" | \"server\";\n\nfunction serveViews(api: Pick<Host<Principal>, \"serve\">): void {\n\tapi.serve(Todos, ({ list }, principal, self) => {\n\t\tif (list !== \"family\") throw forbidden(\"This list is not shared\");\n\t\tconst store = self.get(TodoStore, { list });\n\t\tif (principal === \"guest\") {\n\t\t\treturn { variant: \"viewer\", rows: store.rows };\n\t\t}\n\t\treturn {\n\t\t\tvariant: \"editor\",\n\t\t\trows: store.rows,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\tif (text.trim().length === 0) throw invalid(\"args\", \"A todo needs text\");\n\t\t\t\treturn store.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t});\n}\n```\n\nHere principals are the strings `\"owner\"` and `\"guest\"`, assigned by\ntrusted application code, and `\"server\"`, the host's own code: every host is\ncreated with `self`, the principal its own roots and objects act as. Chord\ninvents no principals. The `Principal` type parameter of `createHost` and\n`Host` makes `principal` in the factory that type, so TypeScript checks the\ncomparisons. `principalKey` maps identity to object identity. Real\napplications should use stable identity, connection provenance, and grant\nrevision, not display names.\n\n<!-- example:05-call -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", principalKey: (principal) => principal });\nhost.serve(TodoStore, todoFactory);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nconst guest = await host.get(Todos, { list: \"family\" }, { principal: \"guest\" });\nassert.ok(owner.variant === \"editor\"); // narrows the reference to the editor variant\nassert.equal(guest.variant, \"viewer\");\nawait host.root((ctx) => owner.add({ text: \"Buy milk\" }, ctx), { principal: \"owner\" });\nassert.deepEqual(guest.rows.value, [{ text: \"Buy milk\", done: false }]);\n// @ts-expect-error A viewer has no add: TypeScript refuses it, and so does the provider.\nawait assert.rejects(guest.add({ text: \"Not allowed\" }), (error) => isChordError(error, \"not_in_variant\"));\nawait assert.rejects(owner.add({ text: \" \" }), (error) => isChordError(error, \"invalid\"));\n```\n\nThe owner gets `editor`; the guest gets `viewer`. Both see the same milk\ntodo. A reference is typed as the union of its variants: `add` is reachable only\nafter narrowing on `variant` (`assert.ok(owner.variant === \"editor\")` does that).\nThe guest's `add` is refused by TypeScript, and, called anyway, rejects with\n`not_in_variant`. Whitespace-only text\nrejects with `invalid`: it is a string, as the schema demands, but it breaks a\nrule only the method knows. Use `invalid` for bad input and `forbidden` only\nfor callers who may not do something.\n\n> **Why:** A variant is not all of authorization. The factory checks which\n> list may be opened; the method checks the operation's arguments. Production\n> code must also enforce grants that can change. Use\n> `host.invalidatePrincipal(match)` to revoke matching objects; held references\n> become `gone`. A UI watches `onStatus` and gets a new reference when appropriate.\n> Guest-provided names or message text must never establish identity.\n\n## 6. Reach the list from another process\n\nA browser cannot call a host-only contract. `remote` contracts describe only\nschema-checked state and methods that can travel as JSON. Our `Todos` view is\nremote; the underlying `TodoStore` and database stay local. A caller never\nnames a host: `browser.get(Todos, …)` goes to `server` because the contract\nsays so, and the server refuses to serve, or to be asked for, a type it is not\nthe place of. A type that lives on one host per session says\n`host: hostFamily(\"session:\", \"session\")`: its input's `session` field picks\nthe host (`{ session: \"S1\" }` lives on `session:S1`).\n\nA **link** carries ordered messages between hosts. A memory link runs both ends\nin one test process, but uses the remote protocol. The accepting host stamps\nthe caller's principal from its authenticator, not from method arguments or a\nclient's claimed identity. The connecting side names the principal its parent's\nown requests act as (`upstream: { link, principal }`, or `connect(link, {\nprincipal })`).\n\n<!-- example:06 -->\n```ts\nconst server = createHost<Principal>({ self: \"server\", name: \"server\" });\nserver.serve(TodoStore, todoFactory);\nserveViews(server);\nconst [down, up] = memoryLinkPair();\nserver.accept(down, { principal: \"owner\", kind: \"client\" });\nconst browser = createHost({\n\tself: \"browser\",\n\tname: \"browser\",\n\tkind: \"client\",\n\tupstream: { link: up, principal: \"server\" },\n});\nconst todos = await browser.get(Todos, { list: \"family\" });\nassert.ok(todos.variant === \"editor\");\nassert.deepEqual(todos.rows.value, []);\nconst updated = Promise.withResolvers<void>();\nconst stop = todos.rows.subscribe((rows) => {\n\tif (rows.length === 1) updated.resolve();\n});\nawait browser.root((ctx) => todos.add({ text: \"Buy milk\" }, ctx));\nawait updated.promise;\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\n// Deliberately bypass TypeScript to simulate a malformed caller.\nawait assert.rejects(todos.add({ text: 123 } as never), (error) => isChordError(error, \"invalid\"));\nstop();\n// A held reference keeps its state without listeners.\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\ntodos.release();\n```\n\nThe browser sees an empty snapshot, then the new milk todo. `ready` means\nthe remote reference has bound to a live object **and** has its state: every\nstate member of its variant has a snapshot. A held reference stays subscribed,\nwith or without listeners, until it is released.\n\nWhat crosses the wire: the input and consumer's contract description, the\nprovider's shape (variant and members), method arguments and results, and the\nstate of the reference's variant. State starts with a **snapshot**, the whole current value;\nsubsequent **deltas** describe changes. Rebinds and reloads can send a\n`reset`, a fresh whole value. Functions, resource connections, and cell\ninstances do not cross the wire.\n\nThe deliberate bad argument rejects with `invalid`. Remote arguments,\nresults, and outgoing state are validated at the provider boundary. Local\nmethods do not get per-call schema validation; they still need app-level checks.\n`dev: true` additionally checks cell writes while developing.\n\n> **Tip:** Replace `memoryLinkPair` with `webSocketLink` for a WebSocket or\n> `ipcLink` for a child-process IPC channel; object contracts stay the same.\n> Those adapters wrap an existing connection, not an authenticator. Configure\n> `accept` with an authenticated principal. A browser is `kind: \"client\"`;\n> a trusted child host is accepted with `kind: \"host\"` and its configured name.\n\n## 7. React to changes and run background work\n\nNot all work comes from method calls. The app may update an activity indicator\nwhen rows change, or run a background reminder. A **listener** reacts to an\nobservable source. A **task** is background execution owned by an object,\nwhich is aborted when the object is disposed or its code is reloaded.\n\nOur activity object mirrors the row count. Its task inserts a sample todo,\nthen waits for shutdown. The promises make those events deterministic in a\ntest; they are not stored application state.\n\n<!-- example:07 -->\n```ts\nconst Activity = local(\"tutorial.activity\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { seen: Type.Number() },\n});\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst observed = Promise.withResolvers<void>();\nconst seeded = Promise.withResolvers<void>();\nconst stopped = Promise.withResolvers<void>();\nhost.serve(Activity, (_input, _principal, self) => {\n\tconst todos = self.get(TodoStore, { list: \"family\" });\n\tconst seen = self.state(\"seen\", Type.Number(), 0);\n\tself.on(\"rows\", todos.rows, (rows, ctx) => {\n\t\tseen.replace(ctx, rows?.length ?? 0);\n\t\tif (rows?.length === 1) observed.resolve();\n\t});\n\tself.task(async (ctx) => {\n\t\tawait todos.add({ text: \"Review the list\" }, ctx);\n\t\tseeded.resolve();\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tif (ctx.signal.aborted) resolve();\n\t\t\telse ctx.signal.addEventListener(\"abort\", () => resolve(), { once: true });\n\t\t});\n\t\tstopped.resolve();\n\t});\n\treturn { seen };\n});\nconst activity = await host.get(Activity, {});\nawait Promise.all([seeded.promise, observed.promise]);\nassert.equal(activity.seen.value, 1);\nactivity.release();\nawait stopped.promise;\n```\n\nThe final `seen` is `1`. Each listener event receives a fresh root's\n`ctx`; its write belongs to that short call tree. A task is not one enormous\nroot: Chord method calls made with its task context start fresh short roots.\nWe pass that context into `todos.add` so cancellation follows the task.\nReleasing the activity reference aborts its task and resolves `stopped`.\n\n`self.on(name, source, handler)` declares a listener; `self.task(fn)` a task.\nA listener on state, like `rows` here, gets the current value when it starts,\nthen every change. It gets the current value again whenever a reload replaced\nits source (a derivation is new at every reload), so its handler must\nreconcile (`seen` is set from the rows, not counted up). Listeners can also\nsubscribe to a resource's event stream with `self.on(name, resource, select,\nhandler)`: `select` gets the opened resource and returns something with\n`subscribe`; an event stream delivers no current value. Kept subscriptions\nsurvive reload while the resource and selector remain unchanged; the handler\nswitches to new code. A selector that captures changing configuration should\nget a new listener name. Tasks have no names: a reload restarts them all.\n\n> **Why:** Do not wait for a person, sleep for minutes, or run an endless loop\n> inside a method. That pins a root and can block state migration. Store a\n> pending request in a cell and answer in another call, or use a task that obeys\n> `ctx.signal`. Never retain a completed method's `ctx` for background work.\n\n## 8. Package features as plugins\n\nNow a plugin can contribute a menu action without editing a central switch.\nA **registry** is a named table of contributions. A **facet** is a plugin's code\nfor one kind of host: it serves object types. A plugin can have server, worker, and browser\nfacets, each installing that part of the feature.\n\nRegistrations belong to objects of **lifetime `\"host\"`**: the host creates such\nan object when its facet loads and keeps it until the host closes, even if no\nscreen asks for it. That keeps our menu contribution present. Objects of the\ndefault lifetime, `\"referenced\"`, are still created only when referenced.\n\n<!-- example:08 -->\n```ts\nconst Actions = defineRegistry<(ctx: Ctx) => Promise<number>>(\"tutorial.actions\");\nconst Menu = local(\"tutorial.menu\", { version: 1, input: Type.Object({}) });\nlet click: (() => Promise<number>) | undefined;\nconst storeFacet: Facet = {\n\tname: \"todo-store\",\n\t...defineFacet((api) => api.serve(TodoStore, todoFactory)),\n};\nconst menuFacet: Facet = {\n\tname: \"todo-menu\",\n\t...defineFacet(\n\t\t(api) => {\n\t\t\tapi.serve(\n\t\t\t\tMenu,\n\t\t\t\t(_input, _principal, self) => {\n\t\t\t\t\tconst todos = self.get(TodoStore, { list: \"family\" });\n\t\t\t\t\tself.register(Actions, {\n\t\t\t\t\t\taddSample: async (ctx) => {\n\t\t\t\t\t\t\tawait todos.add({ text: \"Try plugins\" }, ctx);\n\t\t\t\t\t\t\treturn todos.openCount.value;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\treturn {};\n\t\t\t\t},\n\t\t\t\t{ lifetime: \"host\" },\n\t\t\t);\n\t\t\tclick = () => api.root((ctx) => api.registry(Actions).get(\"addSample\", ctx)!(ctx));\n\t\t},\n\t\t{ uses: [TodoStore] },\n\t),\n};\nconst host = createHost({ self: \"server\" });\nawait host.load([menuFacet, storeFacet], { awaitHostObjects: false });\nawait host.ready();\nassert.equal(await click!(), 1);\nassert.equal(host.registry(Actions).table().size, 1);\n```\n\nClicking returns `1`, and the registry has one entry. The menu's dependency\ncan come from a later facet in the load batch: `load` installs the whole\nbatch before waiting for host objects to activate. The facet declares `uses` so\nreload can also check the contract it consumes.\n\nThe facet API supplies `serve`, `name`, `root`, and `registry`.\nOur ordinary UI callback uses its saved `api.root`, not a process-global\nhost. Looking up a registry entry with `ctx` uses the table belonging to that\nroot's code version. Without `ctx`, lookup uses the current table. Duplicate\nkeys fail by default; `subscribe` can bridge tables to an imperative consumer.\n\nNormally use `await host.load(facets)` (or `await host.install(facet)`),\nwhich waits for those host objects. This example explicitly defers that barrier\nwith `awaitHostObjects: false`, then awaits `host.ready()`. That waits for a\nsnapshot of the host's objects, not for task completion or later loads.\nFailures reject the readiness promise; abandoning startup requires closing the\nhost, because installation is not a rollback transaction.\n\n> **Tip:** Use deferred readiness only when needed. A worker may have to await\n> an independent local bootstrap reference, announce control readiness so its\n> parent admits its link, and only then await `host.ready()`. Waiting for\n> upstream-dependent host objects before admission would deadlock.\n\nFor real plugin files, default-export `defineFacet(...)`. Node hosts configure\n`createHost({ loader: nodeLoader })`, with `nodeLoader` imported from\n`@earendil-works/chord/node`, then pass module URLs to `load` and `reload`.\nThe in-memory facet objects here avoid filesystem setup. Built-in facets are\ntrusted in-process code, not a sandbox; the current Node loader keeps old module\ncopies in memory, so repeated loading has that limitation.\n\n## 9. Change running code without losing the list\n\nA reference is useful precisely because callers keep it while implementations\nchange. An **code version** identifies one committed set of code and registry tables.\nA root keeps the code version it started with. New roots use the latest one.\n\n### Compatible code change: no waiting\n\nGive the same named facet a new factory. It changes the label but keeps the\ncell declaration. This test deliberately pauses one caller to show what an\nold root sees; application methods should not pause like this.\n\n<!-- example:09-compatible -->\n```ts\nconst host = createHost({ self: \"server\" });\nconst facet = (label: string): Facet => ({\n\tname: \"todos\",\n\tsetup: (api) =>\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tlabel: () => label,\n\t\t})),\n});\nawait host.load([facet(\"Original\")]);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nawait todos.add({ text: \"Buy milk\" });\nconst entered = Promise.withResolvers<void>();\nconst resume = Promise.withResolvers<void>();\nconst oldRoot = host.root(async (ctx) => {\n\tconst first = await todos.label(ctx);\n\tentered.resolve();\n\tawait resume.promise; // Test-only pause: pin an old caller during reload.\n\treturn [first, await todos.label(ctx)];\n});\nawait entered.promise;\nconst compatible = await host.reload([facet(\"Updated\")]);\nassert.deepEqual(compatible.gated, []);\nassert.equal(await todos.label(), \"Updated\");\nresume.resolve();\nassert.deepEqual(await oldRoot, [\"Original\", \"Original\"]);\nassert.equal(todos.rows.value?.[0]?.text, \"Buy milk\");\n```\n\nReload commits while the old root is paused: `gated` is empty. New calls see\n`\"Updated\"`; the resumed old root sees `\"Original\"` twice. The milk row\nsurvives. Compatible code changes do not wait for that old root to finish.\n\n### State change: bump its version and migrate\n\nSuppose a new policy converts existing todo text to uppercase. The schema is\nunchanged, but the data's meaning changes. We still bump the cell's **state\nversion** (`self.state`'s `version` option, default 1). It belongs to one cell,\nindependently of the contract version. A **migration** (`migrate`) converts an\nold value of that cell to the new representation.\n\n<!-- example:09-migrate -->\n```ts\nconst events: string[] = [];\nconst stopEvents = todos.rows.events((event) => events.push(event.type));\nconst migrated = await host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tTodoStore,\n\t\t\t\ttodoStore({\n\t\t\t\t\tversion: 2,\n\t\t\t\t\tmigrate: (old, from) => {\n\t\t\t\t\t\tassert.equal(from, 1);\n\t\t\t\t\t\treturn (old as Static<typeof Rows>).map((todo) => ({ ...todo, text: todo.text.toUpperCase() }));\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t),\n\t},\n]);\nassert.equal(migrated.gated.length, 1);\nassert.deepEqual(todos.rows.value, [{ text: \"BUY MILK\", done: false }]);\nassert.deepEqual(events, [\"snap\", \"reset\"]);\nstopEvents();\n```\n\nThe migration receives the old rows and their state version, `1`. It returns\nnew rows; the reference now reads `[{ text: \"BUY MILK\", done: false }]`.\nSubscribers get a `reset` event containing the migrated value. The event\nassertion sees an initial `snap`, then that `reset`.\nOne object is gated. A **gate** waits for calls already inside that object to\nfinish before migrating a private copy, validating it, and committing. Old\nroots that had not entered it cannot later use old code over migrated data:\nthey get `upgrade_conflict` and should retry in a new root.\n\nChanging a cell's schema also requires a version bump, and a migration unless\nthe old value already fits the new schema. A change of meaning with the same schema,\nlike this example, cannot be detected automatically. Real migrations must\nreference each stored version they support, not assume every installation is\nexactly one version behind.\n\n### Contract change: keep an adapter for old callers\n\nRename `label` to `heading`. That is a breaking contract change, so use\ncontract version 2. The provider's `accepts[1]` adapter builds the implementation\nthat version-1 consumers still expect.\n\n<!-- example:09-contract -->\n```ts\n// Remove label from the new provider's contract: it is now called heading.\nconst { label: _label, ...members } = TodoStore.def.members;\nconst RenamedStore = local(\"tutorial.todos\", {\n\tversion: 2,\n\tinput: TodoStore.def.input,\n\tmembers: { ...members, heading: method(undefined, Type.String()) },\n});\nawait host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tRenamedStore,\n\t\t\t\t(input, principal, self) => {\n\t\t\t\t\tconst { label, ...implementation } = todoStore({ version: 2 })(input, principal, self);\n\t\t\t\t\treturn { ...implementation, heading: label };\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\taccepts: {\n\t\t\t\t\t\t1: (current) => ({ ...current, label: current.heading }),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t},\n]);\nassert.equal(await todos.label(), \"Todos for family\");\nconst newer = await host.get(RenamedStore, { list: \"family\" });\nassert.equal(await newer.heading(), \"Todos for family\");\nassert.equal(todos.rows.value?.[0]?.text, \"BUY MILK\");\n```\n\nThe old reference's `label` and the new reference's `heading` both return\n`\"Todos for family\"`. Both still reach the migrated rows. The adapter maps\nimplementations, not references; forwarding the existing method preserves its context.\nWe keep the cell at version 2 here so this code-only change does not repeat\nmigration.\n\n> **Why:** Contract versions protect callers; state versions protect stored\n> data; resource versions control reopening. Do not use one number for all\n> three. Additive compatible contracts can keep their version; breaking shape\n> or meaning changes need a contract bump and adapters for old consumers.\n> Reload prepares candidates first; preparation failure leaves the old code\n> running. A migration gate that misses its deadline aborts rather than\n> switching data underneath active old code.\n\n## 10. Test the objects, not a whole deployment\n\nMost implementation needs only a bare host. Use real factories for state tests, and a\nfake facet when you want to test delegation without a real database or service.\nHere the fake records what the editing view sends to the store.\n\n<!-- example:10 -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", dev: true });\nconst received: string[] = [];\nconst fake: Facet = {\n\tname: \"fake-store\",\n\t...defineFacet((api: FacetApi) => {\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tadd: ({ text }) => {\n\t\t\t\treceived.push(text);\n\t\t\t},\n\t\t}));\n\t}),\n};\nawait host.load([fake]);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nassert.ok(owner.variant === \"editor\");\nawait owner.add({ text: \"Test delegation\" });\nassert.deepEqual(received, [\"Test delegation\"]);\nawait assert.rejects(owner.add({ text: \"\" }), (error) => isChordError(error, \"invalid\"));\nowner.release();\nawait assert.rejects(owner.add({ text: \"Too late\" }), (error) => isChordError(error, \"released\"));\n```\n\nThe fake received `[\"Test delegation\"]`. Empty text failed before reaching\nthe fake. The released reference rejects further calls with `released`.\n`isChordError(error, code)` checks a stable error code; avoid testing only a\nmessage's wording.\n\nOur other tests exercise the real cells (section 3), resource cleanup (4),\nprincipal variants (5), a client over a memory link (6), and callers kept through\nreload (9). After an awaited `get` the state is there; for later changes, await\nthe value you need. Always unsubscribe and close hosts; leaked references can hide\nlifetime bugs or leave background tasks running.\n\n> **Tip:** A fake must honor the contract it serves. This fake delegates all\n> ordinary members to `todoFactory` and replaces only `add`; it deliberately\n> objects calls rather than updating rows. Do not confuse that with persistence.\n\n## 11. Keep these habits\n\nA temporary call reference should not accidentally become a permanent one. A\nscreen should not lose its data merely because a button click finishes. Use\n`ctx.get` for the first lifetime and `host.get` for the second.\n\n<!-- example:11 -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst screen = await host.get(TodoStore, { list: \"family\" });\nawait host.root(async (ctx) => {\n\tconst temporary = await ctx.get(TodoStore, { list: \"family\" });\n\tawait temporary.add({ text: \"Keep roots short\" }, ctx);\n}); // The temporary reference is released here; screen still holds the store.\nassert.equal(screen.openCount.value, 1);\nscreen.release(); // No reference remains; without persistence, disposal loses these rows.\n```\n\nThe screen still sees count `1` after the root finishes, because it owns a\nseparate reference. When the screen releases it, no reference remains. This tutorial's cells\nare in memory: reload preserves a live object, but disposal or process restart\nloses its rows. Real persistence belongs in a database resource, with an\nexplicit load/save policy.\n\n### Checklist and common mistakes\n\n- Keep reloadable state in cells, not closure variables: factories rerun.\n- Change drafts, not published values: mutation outside cells bypasses updates.\n- Use `change` for field edits and `replace` for owned snapshots: replacing\n  can erase fields your code does not know about.\n- Declare dependencies in a synchronous factory, not during methods: activation\n  and reload need a complete declaration list.\n- Use `ctx.get` for temporary method dependencies: their references end with the call.\n- Pass `ctx` into nested calls: cancellation and code versions must stay together.\n- Keep roots short and tasks cancellable: migration and shutdown need work to settle.\n- Check grants and arguments, not just variants: available methods are not a full policy.\n- Stamp principals at trusted ingress, not from client payloads: names are not identities.\n- Await `get` before reading state: a reference you hold is live and hydrated, a\n  loading one (`host.hold`) throws `not_ready`.\n- Release references and unsubscribe: otherwise unused objects remain referenced or observed.\n- Bump state versions, contract versions, and resource versions for their own changes:\n  each protects a different boundary.\n- Await host object readiness before reporting startup success: declared is not yet live.\n- Keep reference dependencies acyclic: an object cannot wait for itself through another object.\n\n### Imports and running the examples\n\nThese are the shared imports from `test/tutorial.test.ts`. The relative\npath is correct from that test directory; applications import the same names\nfrom `@earendil-works/chord`. `assert` expresses expected values; `it` and\n`onTestFinished` come from the test runner; `readFileSync` is used by the\ntutorial's source-matching check, not by the application.\n\n<!-- example:imports -->\n```ts\nimport assert from \"node:assert/strict\";\nimport { readFileSync } from \"node:fs\";\nimport { type Static, Type } from \"typebox\";\nimport { it, onTestFinished } from \"vitest\";\nimport {\n\ttype Ctx,\n\tcreateHost,\n\tdefineFacet,\n\tdefineRegistry,\n\ttype Facet,\n\ttype FacetApi,\n\ttype Factory,\n\tforbidden,\n\ttype Host,\n\thostNamed,\n\tinvalid,\n\tisChordError,\n\tlocal,\n\tmemoryLinkPair,\n\tmethod,\n\topaque,\n\tremote,\n\ttype StateOptions,\n} from \"../src/index.ts\";\n```\n\nFrom `packages/chord`, run:\n\n```sh\nnode ../../node_modules/vitest/dist/cli.js --run test/tutorial.test.ts\n```\n\nThere is one test per numbered section. Every TypeScript block is matched\nagainst its runnable source, so changing either copy without the other fails a\ntest. The test harness adds teardown and drift checks; it never uses a real\ndatabase, network service, or provider API.\n\nNext read [the usage guide](guide.md) for the API reference and\n[the specification](spec.md) for compatibility, lifetime, and reload\nrules, including its known limitations. If a reference disagrees with\n`src`, the current implementation wins.\n","isInternal":false,"tokens":9268,"sizeBytes":39369},{"name":"cli-integration.md","path":"packages/coding-agent/docs/cli-integration.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/cli-integration.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# CLI Integration\n\nBy default, running `pi` opens the interactive terminal interface. When input or output is piped or redirected, Pi uses print mode instead. You can also select print, JSON, or RPC mode explicitly for scripts and applications.\n\nAll four modes use the same agent, sessions, resources, and tools. The mode determines how input enters Pi, how output is exposed, and whether the process remains available for more commands.\n\nThe SDK is not a CLI mode. It embeds the agent directly in a Node.js or Bun process. See the [SDK](sdk.md) when direct TypeScript access is preferable to a process boundary.\n\n## Choose a mode\n\n| Mode | Interface | Lifetime | Use it when |\n|---|---|---|---|\n| Interactive | Terminal UI | Until the user exits | A person is working with Pi directly |\n| Print | Final text on stdout | One invocation | A script needs the final assistant response |\n| JSON | JSONL events on stdout | One invocation | A process needs structured progress from a run |\n| RPC | JSONL commands, responses, and events | Long-lived | A process needs bidirectional control |\n\nCLI options still select the working directory, model, tools, resources, and session persistence independently of the mode. See [Command Line](cli.md) for the complete startup options.\n\n## Print to stdout\n\nPrint mode runs the supplied prompts, writes the final assistant text to stdout, and exits:\n\n```bash\npi --print \"Summarize the changes in this repository\"\n```\n\nUse print mode when only the final text is needed, including command substitution, pipelines, and one-shot jobs. Intermediate events are not exposed.\n\nPrint mode writes errors to stderr. A final assistant response with an `error` or `aborted` stop reason produces a nonzero exit status. A response cut off at the model's output token limit (`length` stop reason) still prints its partial text to stdout, writes a warning to stderr, and produces a nonzero exit status.\n\nWhen no mode is selected explicitly, non-TTY stdin or stdout also selects print mode. This allows piped input and output without adding `--print`.\n\n## Stream JSON events\n\nJSON mode writes a session header followed by agent and session events as newline-delimited JSON:\n\n```bash\npi --mode json \"Review this repository\" > events.jsonl\n```\n\nThis is structured event output, not a single JSON result or a constraint on the format of the model’s response.\n\nAll prompts are supplied when the process starts. The process streams events for that run and then exits; it does not accept later commands.\n\nA failed or aborted assistant response appears in the event stream but does not by itself produce a nonzero exit status. Inspect the events when success or failure matters. Pi still exits nonzero if the invocation throws an error.\n\nStreaming `message_update` records contain deltas rather than a growing message snapshot. Assemble live output from the delta events, then replace it with the authoritative message from `message_end`.\n\n`agent_end` can be followed by automatic recovery or queued work. `agent_settled` marks the end of automatic work for the current run.\n\nStdout is reserved for JSONL. Diagnostics and application logging are written to stderr. See [JSON Event Stream](json.md) for framing, event shapes, and reconstruction rules.\n\n## Control Pi with RPC\n\nRPC mode keeps Pi running while another process sends commands and receives responses and events:\n\n```bash\npi --mode rpc --no-session\n```\n\nCommands are JSON objects written to stdin. Responses and events are JSON objects written to stdout. Every record occupies one line.\n\nAdd an `id` to commands that need correlation. The matching response repeats that ID. Events generally have no command ID because they describe session activity rather than one request.\n\nA successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean the run completed. Continue consuming events through `agent_settled` when completion matters.\n\nRPC commands can change models, inspect state, manage sessions, run shell commands, and answer extension UI requests.\n\nExtension dialogs form a request-response subprotocol. Other extension UI updates are notifications that a client may display or ignore. TUI-only extension capabilities are unavailable or degraded outside interactive mode.\n\nFor Node.js or TypeScript integrations, prefer `RpcClient` from `@earendil-works/pi-coding-agent`. It starts a Pi RPC child process, correlates requests, exposes typed command methods, and delivers session events to listeners.\n\nThe [RPC client example](../examples/rpc-client.ts) sends one prompt, streams text and tool activity, waits for `agent_settled`, and shuts down the child process. It is included in the repository’s TypeScript checks.\n\n`RpcClient.promptAndWait()` installs its event listener before sending the prompt, avoiding a race with fast completions. For separate operations, subscribe before calling `prompt()` and call `waitForIdle()` only while a run is active.\n\nThe client requires a path to a runnable Pi CLI. The repository example points at `dist/cli.js`, so the package must be built before that example runs from a checkout.\n\nIf you are building a client without `RpcClient`, start with [RPC Protocol](rpc.md), then use [RPC Commands](rpc-commands.md) and [JSON Event Stream](json.md) as the wire references.\n\n## Fork and rebrand Pi\n\nA source fork can change the CLI name and configuration directory through `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"my-agent\",\n    \"configDir\": \".my-agent\"\n  }\n}\n```\n\nChange the top-level `bin` field to set the executable name. These settings affect the CLI banner, configuration paths, and derived environment variable names.\n\n## Examples and references\n\n- [RPC client](../examples/rpc-client.ts): typed Node.js integration\n- [RPC extension UI](../examples/rpc-extension-ui.ts): custom terminal client with extension dialogs\n- [Command Line](cli.md): startup options and mode selection\n- [JSON Event Stream](json.md): JSON event reference\n- [RPC Protocol](rpc.md): RPC lifecycle, framing, errors, and shutdown\n- [RPC Commands](rpc-commands.md): command and response reference\n- [RPC Extension UI](rpc-extension-ui.md): extension interaction subprotocol\n- [SDK examples](../examples/sdk/): in-process TypeScript integrations\n","isInternal":false,"tokens":1311,"sizeBytes":6285},{"name":"cli.md","path":"packages/coding-agent/docs/cli.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/cli.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"<a id=\"cli-and-modes-reference\"></a>\n\n# Command Line\n\nThis page documents Pi's built-in command-line commands and options. Run `pi --help` or append `--help` to a command for the exact interface in your installed version. The top-level help also includes options registered by loaded extensions.\n\n```sh\npi [options] [--] [@files...] [messages...]\npi install <source> [options]\npi remove <source> [options]\npi uninstall <source> [options]\npi update [target] [options]\npi list\npi config [options]\npi auth <check|print-api-key|print-bearer-token> [options]\npi mcp <list|login|logout> [options]\n```\n\n<a id=\"modes\"></a>\n\n## Invocation and output\n\n```sh\npi\npi --print \"Summarize this repository\"\ngit diff | pi --print \"Review this change\"\npi --mode json \"Inspect this repository\" > events.jsonl\n```\n\nWith terminal stdin and stdout, Pi opens the terminal UI unless `--print`, `--mode json`, or `--mode rpc` selects another interface. When either stream is redirected and neither JSON nor RPC mode is selected, Pi uses print mode. See [CLI Integration](cli-integration.md) for choosing between interactive, print, JSON, RPC, and SDK integration.\n\n| Input | Behavior |\n|---|---|\n| `message` | Provide an initial prompt |\n| `@path` | Include a text file or image in the first prompt |\n| Piped stdin | Prepend its contents to the first prompt |\n| `--` | Stop option parsing so a prompt can begin with `-` |\n\nPi resolves `@path` from the current working directory. The working directory also controls project configuration, resource discovery, and session grouping.\n\n`--print` controls whether Pi runs once and exits. `--mode` selects the output interface. `--mode text` does not force one-shot execution when stdin and stdout are terminals; use `--print` for that behavior.\n\n| Option | Behavior |\n|---|---|\n| `-p`, `--print` | Run the supplied prompts, write the final assistant text to stdout, then exit |\n| `--mode text` | Select text output; still open the terminal UI when stdin and stdout are terminals |\n| `--mode json` | Run the supplied prompts, write JSONL events to stdout, then exit |\n| `--mode rpc` | Read JSONL commands from stdin and write responses and events to stdout until shutdown |\n| `--export <input> [output]` | Export a session file to HTML and exit; derive the destination when `output` is omitted |\n\nRPC mode rejects `@file` arguments. JSON and RPC modes reserve stdout for protocol records. See [JSON Event Stream](json.md) and [RPC Protocol](rpc.md).\n\n<a id=\"model-options\"></a>\n\n## Models\n\n```sh\npi --model sonnet:high\n```\n\nSee [Choose a Model](models.md) for model selection and [Providers](providers.md) for credentials.\n\n- `--provider <name>`<br>\n  Restricts `--model` lookup to one provider. It requires `--model`.\n- `--model <pattern>`<br>\n  Selects by exact ID or fuzzy ID/name match. It accepts `provider/id` and an optional `:<thinking>` suffix. If several providers match, Pi prefers the one with credentials, otherwise it asks for `--provider`.\n- `--api-key <key>`<br>\n  Uses a non-persistent API-key override. It requires a model selected through `--model` or `--models`. It does not choose between providers that offer the same model.\n- `--thinking <level>`<br>\n  Sets `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. It overrides a `--model` suffix and is clamped to the model's capabilities.\n- `--models <patterns>`<br>\n  Sets a comma-separated scope for startup and cycling. It accepts exact IDs, fuzzy matches, case-insensitive globs, and optional `:<thinking>` suffixes. A model offered by several providers is added for each, with a warning.\n- `--list-models [search]`<br>\n  Lists available models, optionally filtered by a fuzzy search, then exits.\n\n<a id=\"session-options\"></a>\n\n## Sessions\n\n```sh\npi --continue\n```\n\nSee [Sessions and Context](sessions.md) for resuming, forking, naming, and storing sessions.\n\n- `-c`, `--continue`<br>\n  Continues the most recent session for the current project.\n- `-r`, `--resume`<br>\n  Opens the session selector.\n- `--session <path|id>`<br>\n  Opens by file path, exact ID, or partial ID. Pi searches the current project first and offers to fork a cross-project match.\n- `--session-id <id>`<br>\n  Opens the exact project session ID or creates it if absent. IDs accept letters, numbers, `.`, `_`, and `-`.\n- `--fork <path|id>`<br>\n  Forks an existing session into a new session for the current project.\n- `--session-dir <dir>`<br>\n  Overrides storage and lookup. It takes precedence over `PI_CODING_AGENT_SESSION_DIR` and the `sessionDir` setting.\n- `--no-session`<br>\n  Uses an in-memory session that is not persisted.\n- `-n`, `--name <name>`<br>\n  Sets the session display name.\n\nConstraints:\n\n- Session IDs must start and end with a letter or number.\n- `--fork` cannot be combined with `--session`, `--continue`, `--resume`, or `--no-session`.\n- `--session-id` cannot be combined with `--session`, `--continue`, or `--resume`. Combine it with `--fork` to choose the new ID.\n\n<a id=\"tool-options\"></a>\n\n## Tools\n\n```sh\npi --tools read,grep,find,ls --print \"Review this project\"\n```\n\nSee [Settings](settings.md#tools) for configuring the default tool selection.\n\n- `-t`, `--tools <list>`<br>\n  Replaces the default selection with a comma-separated allowlist of built-in, extension, or custom tools. Entries are tool names or patterns where `*` matches any characters. MCP tools are kept unless an entry starts with `mcp__` (see [MCP tools](#mcp-tools)). A list of only `+name` and `-name` entries is not an allowlist; it changes the default selection instead.\n- `-xt`, `--exclude-tools <list>`<br>\n  Disables comma-separated tool names or patterns after all other selection options, MCP tools included.\n- `-nbt`, `--no-builtin-tools`<br>\n  Disables default built-in tools while retaining extension and custom tools.\n- `-nt`, `--no-tools`<br>\n  Starts with all built-in, extension, custom, and MCP tools disabled.\n\nDefault enabled tools are `read`, `bash`, `edit`, and `write`, unless `defaultTools` changes them. `--tools` with plain names replaces the whole selection, so name every tool you want. Like `defaultTools`, it also accepts a list of only `+name` and `-name` entries, which adds tools to or removes them from the default selection: `pi --tools +codemode,-write` keeps the other default tools, enables `codemode`, and disables `write`. These entries take exact tool names, not `*` patterns; use `--exclude-tools` to disable tools by pattern. Plain names and `+name`/`-name` entries cannot be mixed. `/reload` enables tools newly added to `defaultTools`, but a tool removed with `-name` stays removed.\n\n<a id=\"mcp-tools\"></a>\n\n`--tools` selects the tools declared to the model. It does not remove MCP tools, whose reach is set by their [exposure](mcp.md#control-tool-exposure): `pi --tools read,codemode` keeps every MCP tool callable from codemode scripts. An MCP tool that no entry names or matches is never declared directly, whatever its exposure; only `tool_search`, if listed, can load it. Once an entry starts with `mcp__`, `--tools` filters MCP tools too, so this keeps only the tools of the `radius` server:\n\n```sh\npi --tools read,bash,codemode,'mcp__radius__*'\n```\n\nThe MCP resource tools (`list_mcp_resources`, `list_mcp_resource_templates`, `read_mcp_resource`) count as MCP tools. To remove MCP tools, use `--exclude-tools 'mcp__*'` or [`--no-mcp`](#resource-options).\n\n| Built-in | Purpose |\n|---|---|\n| `read` | Read text files and supported images |\n| `bash` | Run shell commands |\n| `powershell` | Run PowerShell commands on Windows |\n| `edit` | Apply exact text replacements to an existing file |\n| `write` | Create or overwrite a file |\n| `grep` | Search file contents |\n| `find` | Find paths using glob patterns |\n| `ls` | List directory contents |\n\nBuilt-in extensions add two more tools. They are off by default; the MCP extension turns them on when an MCP server needs them (see [MCP](mcp.md#exposure)). To enable them yourself, name them in `--tools` or `defaultTools`.\n\n| Built-in extension | Purpose |\n|---|---|\n| `codemode` | Run JavaScript that calls the other tools, for example in parallel with `Promise.allSettled`; only the script's output reaches the model |\n| `tool_search` | Search tools that are not declared to the model (`codemode` and `deferred` exposure, such as MCP tools) and declare the matches for the next call |\n\n### Enable codemode\n\nTo turn on `codemode` for every session, add it to the default tools in `~/.pi/agent/settings.json` or a project's `.pi/settings.json`:\n\n```json\n{\n  \"defaultTools\": [\"+codemode\"]\n}\n```\n\nThis keeps `read`, `bash`, `edit`, and `write` and adds `codemode`. For one invocation, add it with `--tools`:\n\n```sh\npi --tools +codemode\n```\n\nCodemode is useful without MCP: scripts can run several tool calls in parallel, filter large output before it reaches the model, call classifier models such as TypeSafe's Jev through `models.classify()` (see [Classifier models](models.md#use-classifier-models)), and generate images through `models.generateImages()` (see [Image models](models.md#use-image-models)).\n\n### How codemode works\n\nScripts run in a QuickJS sandbox and reach the other tools through `tools.<name>(args)`. [Codemode](codemode.md) describes the script API, how tools are listed and found, the `store()` and `models` globals, and the limits.\n\n### Tool search\n\n`tool_search` is off by default; enable it with `\"defaultTools\": [\"+tool_search\"]` or `--tools`. It uses the same ranking as `searchTools()` over tools that are not declared yet and declares the matches for the next model call. Loaded tools are recorded in the session like other tool changes, so they stay declared on that branch.\n\n<a id=\"resource-options\"></a>\n\n## Resources\n\n```sh\npi --extension ./review.ts\n```\n\nSee [Configuration](configuration.md) for conventional directories and project trust, [Settings](settings.md#resources) for configured paths, and [Pi Packages](packages.md) for package sources.\n\n- `-e`, `--extension <path>`<br>\n  Loads an extension file or directory, or a built-in extension such as `builtin:mcp`, and is repeatable.\n- `-ne`, `--no-extensions`<br>\n  Disables discovered, configured, and built-in extensions. Explicit `-e` paths still load, so `pi -ne -e builtin:mcp` keeps only the built-in MCP support.\n- `--no-mcp`<br>\n  Disables the built-in MCP support for this run: no servers connect, and there are no MCP tools or `/mcp`. It does not affect an extension that replaces the built-in MCP support.\n- `--skill <path>`<br>\n  Loads a skill file or directory and is repeatable.\n- `-ns`, `--no-skills`<br>\n  Disables discovered and configured skills. Explicit `--skill` paths still load.\n- `--prompt-template <path>`<br>\n  Loads a prompt-template file or directory and is repeatable.\n- `-np`, `--no-prompt-templates`<br>\n  Disables discovered and configured templates. Explicit `--prompt-template` paths still load.\n- `--theme <path>`<br>\n  Loads a theme file or directory and is repeatable.\n- `--use-theme <name[/name]>`<br>\n  Selects the initial interactive theme for this run.\n- `--no-themes`<br>\n  Disables discovered and configured themes. Explicit `--theme` paths still load.\n- `-nc`, `--no-context-files`<br>\n  Disables `AGENTS.md` and `CLAUDE.md` discovery.\n\nResource paths apply only to the current process. Relative paths resolve from the current working directory.\n\n<a id=\"prompt-and-display-options\"></a>\n\n## Prompts and process\n\n```sh\npi --append-system-prompt ./instructions.md\n```\n\nSee [Configuration](configuration.md) for saved configuration, [Security](security.md#understand-project-trust) for project trust, and [Environment Variables](environment-variables.md) for process controls.\n\n- `--system-prompt <text|path>`<br>\n  Replaces the default system prompt with text or the contents of an existing file.\n- `--append-system-prompt <text|path>`<br>\n  Appends text or an existing file to the system prompt and is repeatable.\n- `--tui-mode <mode>`<br>\n  Uses `fullscreen` (default) or `regular` terminal mode.\n- `--verbose`<br>\n  Shows verbose interactive startup information, overriding `quietStartup`.\n- `-a`, `--approve`<br>\n  Trusts project-local configuration and resources for this process.\n- `-na`, `--no-approve`<br>\n  Ignores trust-gated project-local configuration and resources for this process.\n- `--offline`<br>\n  Disables automatic network activity, including model catalog refreshes. Equivalent to `PI_OFFLINE=1`.\n- `-h`, `--help`<br>\n  Shows help, including flags registered by loaded extensions, then exits.\n- `-v`, `--version`<br>\n  Shows the Pi version, then exits.\n\nExtensions may register additional long-form options. Unknown short options are rejected.\n\n## Package commands\n\n```sh\npi install npm:@scope/package\n```\n\nSee [Pi Packages](packages.md) for source formats, filtering, installation, and project scope.\n\n### Common tasks\n\n| Task | Command |\n|---|---|\n| Install a package | `pi install <source>` |\n| List configured packages | `pi list` |\n| Remove a package and its settings entry | `pi remove <source>` |\n| Configure which package resources load | `pi config` |\n\nAdd `--local` or `-l` to `install`, `remove`, `uninstall`, or `config` to use project settings instead of global settings.\n\n### Update Pi or packages\n\nRunning `pi update` without a target updates Pi itself.\n\n| Task | Command |\n|---|---|\n| Update Pi | `pi update` |\n| Update all installed packages | `pi update --extensions` |\n| Update one installed package | `pi update <source>` |\n| Refresh model catalogs | `pi update --models` |\n| Update Pi and all installed packages | `pi update --all` |\n\nAdd `--force` to reinstall Pi when the selected update includes Pi.\n\n`pi update` cannot update Pi when another package manager provides it, such as Nix. Update Pi with that package manager, for example `nix profile upgrade pi`. Package and model catalog updates still work.\n\n### Aliases and command options\n\n- `pi uninstall <source>` is an alias for `pi remove <source>`.\n- `pi update --self`, `pi update self`, and `pi update pi` are aliases for `pi update`.\n- `pi update --extension <source>` is an alias for `pi update <source>`.\n- `-a`, `--approve` trusts project-local files for one command. `-na`, `--no-approve` ignores trust-gated project-local files.\n- Append `-h` or `--help` to a command for its exact usage and option constraints.\n\n## Credential commands\n\n```sh\npi auth check --provider openai --json\n```\n\nAuthentication commands require `--provider <provider>` or `--model <model>`. See [Providers](providers.md) for supported methods.\n\n| Command | Description |\n|---|---|\n| `pi auth check` | Print `ready`, `not_ready`, or `invalid`; exit with status `0`, `1`, or `2`, respectively |\n| `pi auth print-api-key` | Print the resolved API key |\n| `pi auth print-bearer-token` | Print a resolved OAuth bearer token |\n\n| Option | Applies to | Description |\n|---|---|---|\n| `--provider <provider>` | All | Resolve credentials for a provider |\n| `--model <model>` | All | Resolve credentials from a model; may be combined with `--provider` |\n| `--json` | `auth check` | Write the structured result as JSON |\n| `--credentials` | `auth check` | Emit the resolved credential when ready |\n| `--no-refresh` | `auth check` | Do not refresh expired OAuth credentials; refresh is the default |\n| `--min-expiry <duration>` | `print-bearer-token` | Require remaining token lifetime using `ms`, `s`, `m`, or `h`, such as `30m` |\n\nCredential-printing commands write secrets to stdout.\n\n## MCP commands\n\nThese commands work outside a session, so agents can run them through `bash`. See [MCP Servers](mcp.md).\n\n| Command | Description |\n|---|---|\n| `pi mcp add <server> [options] -- <command> [args...]` | Add or replace a stdio server in `mcp.json`; `--env KEY=VALUE` (repeatable) and `--cwd <dir>` set its environment and working directory. Arguments after the command are passed to it |\n| `pi mcp add <server> [options] --url <url>` | Add or replace a streamable HTTP server; `--header KEY=VALUE` (repeatable), `--bearer-token-env-var <NAME>` (sends `Authorization: Bearer ${NAME}`), `--oauth-client-id`, `--oauth-client-secret`, `--oauth-callback-port`, and `--oauth-client-name` configure authentication |\n| `pi mcp remove <server>` | Remove a server from `mcp.json`; stored OAuth credentials are kept |\n| `pi mcp list [--json]` | Connect to every enabled server and print its state, tools, and errors; exit with `1` when a config entry is invalid or an enabled server is not connected |\n| `pi mcp login <server> [--timeout <seconds>]` | Sign in to an OAuth server: open the authorization page and wait for the browser; a terminal also accepts the pasted redirect URL. `--timeout` limits the whole sign-in, including requests to the authorization server (default 300 seconds) |\n| `pi mcp logout <server>` | Delete the stored OAuth credentials of a server |\n\n`add` and `remove` change `~/.pi/agent/mcp.json`, or `.pi/mcp.json` in the current directory with `--local` (`-l`). `add` also takes `--exposure <mode>` (see [Exposure](mcp.md#exposure)) and `--description <text>` and does not connect; run `pi mcp list` to check the server.\n\nProject `.pi/mcp.json` files are only read for projects that are already trusted.\n","isInternal":false,"tokens":4339,"sizeBytes":17188},{"name":"codemode.md","path":"packages/coding-agent/docs/codemode.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/codemode.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Codemode\n\nThe `codemode` tool lets the model write a JavaScript script that calls pi's other tools and runs non-LLM models, such as classifiers and image models. Only the script's output reaches the model, so a script can run calls in parallel and filter large results before the model sees them. To turn it on, see [Enable codemode](cli.md#enable-codemode).\n\n## Scripts\n\nThe tool input is raw JavaScript source, not JSON and not a markdown code fence. It runs as the body of an async function in a QuickJS sandbox, so top-level `await` and `return` work. The sandbox has no Node APIs, file system, network, or timers; scripts reach the outside world only through tools and `models`.\n\nA script may start with an options line. It must be the first line; an options line anywhere else fails the call before the script runs:\n\n```js\n// @options: {\"max_output_tokens\": 2000, \"timeout_ms\": 60000}\n```\n\n- `max_output_tokens` (default 10000) limits the output. Longer output keeps its start and end, and the full text is written to a temp file whose path is included in the result. A script fails when its output passes 16777216 characters of text and base64 image data or 100000 `text()`, `image()`, and `console` calls; write large data to a file with a tool instead.\n- `timeout_ms` is a hard deadline for the whole script, including any wait for a free worker. It is unset by default. Image generation can take minutes, so do not set a short deadline for scripts that generate images.\n\nThe result starts with `Script completed` or `Script failed`, the wall time, and the output. Text and image items appear in order, each on its own line. When the output has more than one text item (from `text()` or `return`), each starts with a `==> text N/M <==` line. `console` calls follow in one `<console_output>` block with one line per call. A failed script keeps its partial output, followed by `Script error:` and the error. Tool calls are real: calls made before a failure are not undone. Calls still running when the script ends are cancelled, and unawaited promises are discarded.\n\n## Globals\n\n| Global | Purpose |\n|---|---|\n| `tools.<name>(args)` | Call a tool. See [Call tools](#call-tools). |\n| `text(value)` | Add a text item to the output. Strings are added as is, other values as JSON. |\n| `image(value)` | Add an image to the output: a base64 `data:` URL, an `{ image_url }` object, or an image block `{ type: \"image\", data, mimeType }` such as those returned by MCP tools and `models.generateImages()`. Remote URLs are not supported. PNG, JPEG, GIF, and WebP are accepted. Each image is also saved to a temp file, and the result names the path before the image. |\n| `console.log(...)` | Add a line to the `<console_output>` block after the other output. Arguments are joined with spaces; `info`, `warn`, `error`, and `debug` do the same. |\n| `return value` | A top-level `return` adds the value like `text()`. |\n| `exit()` | End the script successfully. |\n| `store(key, value)` / `load(key)` | Keep small JSON values across `codemode` calls. See [Store values](#store-values). |\n| `ALL_TOOLS` | Every callable tool as `{ name, description }`, including tools the description does not list. |\n| `searchTools(query, { limit?, namespace? })` | Rank callable tools by relevance (BM25, default limit 8). Resolves to `{ name, description }[]`. |\n| `describeTool(name)` | Resolves to a tool's description and TypeScript declaration, or `undefined`. |\n| `describeNamespace(name)` | Resolves to `{ name, description?, instructions?, tools }` for a namespace such as an MCP server, or `undefined`. |\n| `models` | List and run non-LLM models. See [Models](#models). |\n\n## Call tools\n\nEvery tool the session can call is a method of `tools`, named by its identifier: characters that are not valid in a JavaScript identifier become `_`, so the MCP tool `mcp__dev-radius__search` is `tools.mcp__dev_radius__search`. Each method takes one object with the tool's arguments.\n\nWhat a call resolves to depends on the tool:\n\n- Tools with an output schema resolve to a structured value. `bash` resolves to `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`, also for non-zero exit codes. Its `output` is not limited to the 2000 lines or 50KB the model sees: it holds up to 1 MiB, and longer output keeps its first and last 512 KiB around an omission marker, with `truncated` set and the full output in `full_output_path`.\n- MCP tools resolve to their `CallToolResult`, including `isError` and `structuredContent`.\n- `read` resolves to the file's text, or for an image to an image block `{ type: \"image\", data, mimeType, note }` that `image()` shows. `data` is the base64 image the model would see and `note` the text that goes with it, such as resize hints.\n- Other tools, such as `edit` and `write`, resolve to their text output.\n\nA call that fails, is blocked, or gets invalid arguments rejects with an `Error` that carries the tool's error text. Use `Promise.allSettled()` to keep the results of the calls that succeed.\n\nThe `codemode` description lists tools with their TypeScript declarations, grouped by namespace (for example one MCP server). Tools with `deferred` exposure, which includes MCP tools with the default `codemode` exposure, are not listed, so the description stays the same while MCP servers connect. Listed declarations share a budget of 3000 estimated tokens (`codemode.inlineBudget` in [settings](settings.md#tools)). Scripts find the other tools with `searchTools()`, `describeTool()`, `describeNamespace()`, or by filtering `ALL_TOOLS`.\n\nWhile `codemode` is active, `codemode.mode` in [settings](settings.md#tools) decides how the other tools are presented. With `on` (default) declared tools stay declared, and their descriptions say how to call them from scripts. With `only` they are hidden from the model and listed in the `codemode` description instead, so the model calls them through scripts. Tool declarations in the `codemode` description, `describeTool()`, and `ALL_TOOLS` carry the tools' prompt guidelines, since the system prompt rules only cover declared tools.\n\n## Store values\n\n`store(key, value)` keeps a JSON value under a string key for later `codemode` calls; storing `undefined` deletes the key. `load(key)` returns the value, or `undefined`. Writes are kept only when the script succeeds: each successful script that stores values appends a `codemode-store` custom entry to the session, so resumed sessions keep the values and each branch sees only the values written on its path.\n\nThe store is for small state such as IDs, cursors, or summaries. One value may have at most 262144 characters of JSON and all values together at most 1048576. Do not store image data; show images with `image()`, which also saves them to a temp file.\n\n## Models\n\n`models` reaches the model catalog and runs non-LLM models with the session's credentials: classifiers, which answer typed questions about JSON state and, for some models, images, and image models, which generate images. Chat models are listed but cannot be run from scripts. Which classifier and image models exist is described in [Use classifier models](models.md#use-classifier-models) and [Use image models](models.md#use-image-models).\n\n```ts\ntype ModelType = \"chat\" | \"image\" | \"classifier\";\n\n/** A catalog entry. `provider` and `id` identify it; other fields depend on the type. */\ninterface ModelInfo {\n  type?: ModelType;\n  provider: string;\n  id: string;\n  name: string;\n  api: string;\n  input: (\"text\" | \"image\")[];\n  contextWindow?: number;\n  [key: string]: unknown;\n}\n\ndeclare const models: {\n  /** Every known model of a type, optionally for one provider. */\n  getModelsOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** Models of a type whose provider has working credentials. */\n  getAvailableOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** One catalog entry, or undefined. */\n  getModelOfType(type: ModelType, provider: string, id: string): Promise<ModelInfo | undefined>;\n  /** Answer `context.questions` about `context.state`; answers are in `result.answers` by question ID. */\n  classify(model: ModelInfo, context: ClassifierContext): Promise<ClassifierResult>;\n  /** Generate images from `context.input` text and image blocks; show `result.output` blocks with image(). Can take minutes. */\n  generateImages(model: ModelInfo, context: ImagesContext): Promise<ImagesResult>;\n};\n```\n\n`classify()` and `generateImages()` use only the `provider` and `id` of `model`, so `{ provider, id }` works as well. They do not throw on provider errors: check `stopReason` and `errorMessage`. At most four such calls run at once per script; more calls wait for a free slot, so `Promise.all()` over many items is fine. Their usage is added to the `codemode` tool result and counts toward the session cost.\n\nModel IDs differ between providers, for example `typesafe/jev-latest` and `openrouter/typesafe/jev-1.13`. Use `models.getAvailableOfType(type)` to find the IDs that work with the current credentials.\n\n### Classify\n\n```ts\ninterface ClassifierContext {\n  /** The data to classify. */\n  state: Record<string, unknown>;\n  /** Images judged together with `state`. Only models whose `input` includes \"image\" accept them. */\n  images?: { type: \"image\"; data: string; mimeType: string }[];\n  /** Questions by ID. One call answers all of them. */\n  questions: Record<string, ClassifierQuestion>;\n}\n\ntype ClassifierQuestion =\n  /** Pick one label. `criteria` maps each label to what it means. */\n  | { type: \"choice\"; instructions: string; criteria: Record<string, string> }\n  /** Score on an ordered scale. `criteria` describes each level, lowest first. */\n  | { type: \"score\"; instructions: string; criteria: string[] }\n  /** Yes or no. */\n  | { type: \"bool\"; instructions: string; criteria: { true: string; false: string } };\n\ninterface ClassifierResult {\n  provider: string;\n  model: string;\n  /** Answers by question ID. */\n  answers: Record<string, ClassifierAnswer>;\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype ClassifierAnswer =\n  | { type: \"choice\"; choice: string; probabilities: Record<string, number>; confidence: number }\n  /** `score` is the expected level index, from 0 to `criteria.length - 1`. */\n  | { type: \"score\"; score: number; confidence: number }\n  /** Probability of `true`. */\n  | { type: \"bool\"; probability: number };\n\n/** Token counts and cost in USD, when the service reports them. */\ntype ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };\n```\n\nClassify several items by calling `classify()` once per item. This script sorts feedback messages, for example ones a tool returned earlier in the script:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst results = await Promise.all(\n  messages.map((message) =>\n    models.classify(jev, {\n      state: { message },\n      questions: {\n        sentiment: {\n          type: \"choice\",\n          instructions: \"How does the user feel about the product?\",\n          criteria: { positive: \"Satisfied or happy\", negative: \"Unhappy or frustrated\", neutral: \"Neither\" },\n        },\n        urgency: {\n          type: \"score\",\n          instructions: \"How urgently does this need a reply?\",\n          criteria: [\"no reply needed\", \"reply this week\", \"reply today\"],\n        },\n      },\n    }),\n  ),\n);\nreturn results.map((result, i) =>\n  result.stopReason === \"stop\"\n    ? { message: messages[i], sentiment: result.answers.sentiment.choice, urgency: result.answers.urgency.score }\n    : { message: messages[i], error: result.errorMessage },\n);\n```\n\nClassifiers whose `input` includes `\"image\"` also judge images. `tools.read()` returns an image file as an image block that `images` accepts. Other classifiers return an error result when `images` is not empty.\n\n```js\nconst luna = await models.getModelOfType(\"classifier\", \"openai\", \"gpt-6-luna\");\nconst photo = await tools.read({ path: \"screenshot.png\" });\nconst result = await models.classify(luna, {\n  state: { task: \"Settings page redesign\" },\n  images: [photo],\n  questions: {\n    broken: {\n      type: \"bool\",\n      instructions: \"Does the screenshot show a broken layout?\",\n      criteria: { true: \"Overlapping, cut-off, or misaligned elements\", false: \"Clean layout\" },\n    },\n  },\n});\n```\n\n### Generate images\n\n```ts\ninterface ImagesContext {\n  /** The prompt as text blocks, plus image blocks to edit or use as references. */\n  input: (TextBlock | ImageBlock)[];\n}\n\ninterface ImagesResult {\n  provider: string;\n  model: string;\n  /** Generated images, and text blocks for models that also return text. */\n  output: (TextBlock | ImageBlock)[];\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype TextBlock = { type: \"text\"; text: string };\n/** `data` is base64. */\ntype ImageBlock = { type: \"image\"; data: string; mimeType: string };\n```\n\nShow generated images with `image(block)`. Do not print `data` with `text()`, `console`, or `return`: it is large and the model cannot read it as text. `image()` also saves each image to a temp file and puts its path in the result, so a later turn can copy or move the file.\n\n```js\n// @options: {\"timeout_ms\": 300000}\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) {\n  if (block.type === \"image\") image(block);\n  else text(block.text);\n}\n```\n\n## Limits\n\n- A script's VM has 256 MB of memory. Running out throws `InternalError: out of memory`; filter or aggregate large data instead of accumulating it.\n- On Bun, stopping a script or reaching its deadline ends the call promptly, but a built-in that does not poll QuickJS's interrupt handler can keep computing in its worker until it returns.\n- At most two codemode workers may be alive across all sessions in one pi process; `PI_CODEMODE_MAX_WORKERS` changes the limit. A cancelled worker occupies its slot until it exits. At capacity, new scripts wait for a free worker in arrival order. A script that gets no worker within 60 seconds, or before its `timeout_ms`, fails without running.\n- A script that waits on a promise that can never settle (no tool call pending) fails immediately, since there are no timers.\n- Scripts cannot start other `codemode` scripts.\n","isInternal":false,"tokens":3492,"sizeBytes":14585},{"name":"compaction.md","path":"packages/coding-agent/docs/compaction.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/compaction.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Compaction Reference\n\nThis reference describes automatic compaction, branch summarization, persisted entries, and extension hooks. For the user workflow, see [Sessions and Context](sessions.md#manage-conversation-context).\n\n**Source files** ([pi](https://github.com/earendil-works/pi)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - Auto-compaction logic\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - Branch summarization\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - Shared utilities (file tracking, serialization)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts) - Entry types (`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) - Extension event types\n\nFor TypeScript definitions in your project, inspect `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Overview\n\nPi has two summarization mechanisms:\n\n| Mechanism | Trigger | Purpose |\n|-----------|---------|---------|\n| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |\n| Branch summarization | `/tree` navigation | Preserve context when switching branches |\n\nBoth use closely related structured formats and track file operations cumulatively. Summarization requests disable prompt-cache writes because these one-off prompts are unlikely to be reused.\n\n## Compaction\n\n### When It Triggers\n\nAuto-compaction triggers when:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nBy default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`). It only decides when compaction starts. Summary requests use the model's output limit, reduced when needed so input and output fit the context window.\n\nDuring a multi-turn agent run, Pi checks the canonical projected context after tools finish and their results are appended, before starting the next assistant response. If the threshold is crossed, Pi compacts during `prepareNextTurn`, then performs the existing catch-up steering poll before `turn_start`. It skips this between-turn check when the completed tool batch terminates the run and no queued message requires another response. Pi also checks before a new user prompt and performs final-attempt overflow recovery after the low-level run ends.\n\nA provider context-overflow error or an early final `stopReason: \"length\"` can select one compact-and-retry recovery attempt. Length responses with tool calls retain their synthetic failed tool results and follow the ordinary tool/queue scheduler rather than forcing the run to end.\n\nYou can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary.\n\n### How It Works\n\n1. **Find cut point**: Walk backwards through the finalized session projection, accumulating token estimates until `keepRecentTokens` (default 20k, configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`) is reached\n2. **Extract messages**: Collect projected messages from the previous kept boundary (or session start) up to the cut point\n3. **Generate summary**: Call LLM to summarize with structured format, passing the previous summary as iterative context when present\n4. **Append entry**: Save `CompactionEntry` with summary and `firstKeptEntryId`\n5. **Rebuilds context**: Session rebuilds the context for the next request, using summary + messages from `firstKeptEntryId` onwards\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\nOn repeated compactions, the summarized span starts at the previous compaction's kept boundary (`firstKeptEntryId`), not at the compaction entry itself, falling back to the entry after the previous compaction if that kept entry cannot be found in the path. A retain-none compaction records its own ID as `firstKeptEntryId`; repeated compaction starts after that entry. This preserves messages that survived the earlier compaction by including them in the next summarization pass as well. Pi also recalculates `tokensBefore` from the rebuilt, context-edited session projection before writing the new `CompactionEntry`, so the token count reflects the actual pre-compaction context being replaced. Omitted raw entries remain stored but do not affect cut selection, summaries, checkpoints, or token estimates.\n\n### Overflow and Length Recovery Ordering\n\nRecovery preserves the existing lifecycle and queue order. The completed attempt remains visible to `turn_end` and `agent_end`; post-run recovery then repairs persisted model context before a fresh retry:\n\n```text\npersist final assistant response\n→ extension/public turn_end\n→ extension/public agent_end\n→ append context_edit omissions for the selected attempt\n→ for overflow/length: run session_before_compact and append compaction on success\n→ start the retry as a fresh run\n```\n\nIf recovery compaction fails or is cancelled, Pi keeps the omission edits, appends no compaction, and schedules no internal retry. Existing queued work remains governed by ordinary steering and follow-up rules. `agent_before_settle` sees the repaired projection after recovery processing. Raw transcript history, exports, billing totals, and history-search extensions can still inspect the omitted attempt.\n\n### Split user-message spans\n\nA user-message span starts with a user message and includes all turns until the next user message. Normally, compaction cuts at user-message boundaries.\n\nWhen one user-message span exceeds `keepRecentTokens`, the cut point lands within that span at an assistant message. This is a split user-message span:\n\n```\nSplit user-message span (one span exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no earlier user-message spans)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\nFor split user-message spans, Pi generates two summaries and merges them:\n1. **History summary**: Previous context (if any)\n2. **User-message-span prefix summary**: The early part of the split user-message span\n\n### Cut Point Rules\n\nValid cut points are:\n- User messages\n- Assistant messages\n- BashExecution messages\n- Custom messages (custom_message, branch_summary)\n\nNever cut at tool results (they must stay with their tool call).\n\nPreparation advances the kept boundary into a context-invisible suffix only when that suffix contains an omitted assistant attempt and no unomitted context-producing entries. Recovery `context_edit` omissions satisfy this rule; intrinsically context-invisible metadata may coexist with them. Metadata alone and newly appended custom messages do not move the cut. A replacement edit affecting the candidate input or summarized prefix also blocks advancement because the omitted assistant answered the pre-edit input; replacements of suffix entries that are ultimately omitted remain safe. This allows an over-budget recovered input to be summarized while retaining the edits that keep the abandoned attempt omitted, without making bookkeeping change whether new model input is preserved verbatim.\n\n### CompactionEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions can store any JSON-serializable data in `details`. The default compaction tracks file operations, but custom extension implementations can use their own structure. Generated and extension-provided summaries store their LLM `usage` when available so session totals include summarization work.\n\nSee [`prepareCompaction()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) and [`compact()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) for the implementation. For direct programmatic summarization, `generateSummary()` returns the summary text and `generateSummaryWithUsage()` returns `{ text, usage }`.\n\n## Branch Summarization\n\n### When It Triggers\n\nWhen you use `/tree` to navigate to a different branch, Pi offers to summarize the work you're leaving. This injects context from the left branch into the new branch.\n\n### How It Works\n\n1. **Find common ancestor**: Deepest node shared by old and new positions\n2. **Collect entries**: Walk from old leaf back to common ancestor\n3. **Prepare with budget**: Include messages up to token budget (newest first)\n4. **Generate summary**: Call LLM with structured format\n5. **Append entry**: Save `BranchSummaryEntry` at navigation point\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### Cumulative File Tracking\n\nDefault compaction and branch summarization track files cumulatively. Both extract file operations from tool calls in the messages being summarized. Compaction also carries file lists from the previous Pi-generated compaction. Branch summarization carries file lists from Pi-generated branch summaries in the entries it summarizes.\n\nFile tracking therefore accumulates across default compactions and nested default branch summaries. Pi does not automatically carry file lists from extension-generated summaries whose `fromHook` field is `true`; extensions manage their own `details` format.\n\n### BranchSummaryEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nSame as compaction, extensions can store custom data in `details`.\n\nSee [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), and [`generateBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) for the implementation.\n\n## Summary Format\n\nBoth formats include Goal, Constraints & Preferences, Progress, Key Decisions, and Next Steps. Compaction summaries also include Critical Context. Branch summaries stop after Next Steps. Pi appends file lists to either format when relevant.\n\nCompaction summaries use this format:\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### Message Serialization\n\nBefore summarization, messages are serialized to text via [`serializeConversation()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts):\n\n```\n[User]: What they said\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\nThis prevents the model from treating it as a conversation to continue.\n\nThinking blocks are omitted. Providers return summaries of the real reasoning, and replayed thinking is often not part of the model's live context, so including it could make the summarization request larger than the context it summarizes.\n\nTool results are truncated to 2000 characters during serialization. Content beyond that limit is replaced with a marker indicating how many characters were truncated. This keeps summarization requests within reasonable token budgets, since tool results (especially from `read` and `bash`) are typically the largest contributors to context size.\n\n## Custom Summarization via Extensions\n\nExtensions can intercept and customize both compaction and branch summarization. See [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for event type definitions.\n\n### session_before_compact\n\nFired before auto-compaction or `/compact`. Can cancel or provide custom summary. See `SessionBeforeCompactEvent` and `CompactionPreparation` in the types file.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - user-message-span prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - effective settings after applying model overrides\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### Converting Messages to Text\n\nTo generate a summary with your own model, convert messages to text using `serializeConversation`:\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\nSee [custom-compaction.ts](../examples/extensions/custom-compaction.ts) for a complete example using a different model.\n\n### session_compact_failed\n\nFired when manual or automatic compaction fails or is aborted. This is useful for telemetry extensions that need to pair `session_before_compact` attempts with terminal outcomes.\n\n```typescript\npi.on(\"session_compact_failed\", async (event, ctx) => {\n  const { reason, errorMessage, aborted, willRetry, fromExtension } = event;\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // errorMessage - present for non-abort failures\n  // aborted - true for canceled/aborted compactions\n  // willRetry - whether the aborted turn would have retried after compaction\n  // fromExtension - whether extension-provided compaction content was being used\n});\n```\n\n### session_before_tree\n\nFired before `/tree` navigation. Always fires regardless of whether user chose to summarize. Can cancel navigation or provide custom summary.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\nSee `SessionBeforeTreeEvent` and `TreePreparation` in the types file.\n\n## Settings\n\nConfigure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`:\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `enabled` | `true` | Enable auto-compaction |\n| `reserveTokens` | `16384` | Free context tokens below which auto-compaction starts |\n| `keepRecentTokens` | `20000` | Recent tokens to keep (not summarized) |\n\nDisable auto-compaction with `\"enabled\": false`. You can still compact manually with `/compact`.\n\n### Per-model overrides\n\nUse `compaction.modelOverrides` to tune token budgets for different models:\n\n```json\n{\n  \"compaction\": {\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000,\n    \"modelOverrides\": {\n      \"some-provider/big-model\": {\n        \"reserveTokens\": 400000\n      }\n    }\n  }\n}\n```\n\nFor a model with a 1M context window, this override triggers compaction above 600K tokens and keeps the ordinary 20000 recent tokens. Other models retain the ordinary 16384-token reserve.\n\nKeys are exact, case-sensitive `provider/modelId` values, including any slashes within the model ID. Each `reserveTokens` and `keepRecentTokens` value falls back independently from the model override to the ordinary setting to the built-in default. Values must be non-negative safe integers. Invalid values in the matching model override produce an error when read; only omitted fields fall back to the ordinary setting. Model override entries must be objects. Invalid ordinary token settings produce an error when read, even if the active model has a valid override. Only omitted ordinary values use built-in defaults. `enabled` remains global, not model-specific.\n\nThese resolved values are used for manual compaction, all automatic threshold checks, overflow recovery, and extension-visible `preparation.settings`. Model switches affect subsequent checks and compactions without changing ordinary settings. Compaction already in progress uses the model and settings captured for that operation. Branch summarization settings are unaffected.\n\nOverrides work in both global and project settings. The files merge recursively before lookup, so a global model-specific value beats a project-wide fallback; a project must override that model entry to change it. See [Settings](settings.md#per-model-compaction-overrides) for details.\n","isInternal":false,"tokens":5158,"sizeBytes":24093},{"name":"configuration.md","path":"packages/coding-agent/docs/configuration.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/configuration.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Configuration\n\nPi supports user-level and project configuration. User-level configuration lives in the agent directory, which defaults to `~/.pi/agent`. Project configuration lives in `.pi` under the working directory and loads after [project trust](security.md#understand-project-trust) is granted. The only exception is `sessionDir`, which Pi reads before resolving trust so it can locate sessions.\n\nIn interactive mode, use `/settings` to change common preferences. For other options, ask Pi to update the configuration or edit the relevant files directly. Run `/reload` after manually changing settings, keybindings, instructions, or resources.\n\n## Agent directory\n\nThe agent directory is shown as `<agent-dir>` below. Set its location with the `PI_CODING_AGENT_DIR` environment variable or the SDK's [`agentDir`](sdk.md) option.\n\n| Path | Responsibility |\n|---|---|\n| `<agent-dir>/settings.json` | User-level [settings](settings.md), including preferences, defaults, resource paths, and Pi package declarations. |\n| `<agent-dir>/keybindings.json` | Custom terminal UI and application [keybindings](keybindings.md). |\n| `<agent-dir>/mcp.json` | [MCP servers](mcp.md) available in every project. |\n| `<agent-dir>/models.json` | [Compatible endpoints, models, and model overrides](models.md#configure-a-compatible-endpoint). |\n| `<agent-dir>/auth.json` | Saved API keys and OAuth credentials. |\n| `<agent-dir>/AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, or `CLAUDE.MD` | User instructions applied across working directories. |\n| `<agent-dir>/SYSTEM.md` | Replaces Pi’s default system prompt. |\n| `<agent-dir>/APPEND_SYSTEM.md` | Adds instructions to Pi’s system prompt. |\n| `<agent-dir>/extensions/` | User [extensions](extensions.md). |\n| `<agent-dir>/skills/` | User [skills](skills.md) and supporting files. |\n| `<agent-dir>/prompts/` | User [prompt templates](prompt-templates.md) exposed as slash commands. |\n| `<agent-dir>/themes/` | User [theme](themes.md) files. |\n\n## Project `.pi` directory\n\n| Path | Responsibility |\n|---|---|\n| `.pi/settings.json` | Project-level [settings](settings.md), resource paths, and Pi package declarations. |\n| `.pi/mcp.json` | Project [MCP servers](mcp.md). |\n| `.pi/SYSTEM.md` | Replaces the system prompt for the project. |\n| `.pi/APPEND_SYSTEM.md` | Adds project-specific instructions to the system prompt. |\n| `.pi/extensions/` | Project extensions. |\n| `.pi/skills/` | Project skills and supporting files. |\n| `.pi/prompts/` | Project prompt templates exposed as slash commands. |\n| `.pi/themes/` | Project theme files. |\n\nFor `SYSTEM.md` and `APPEND_SYSTEM.md`, the trusted project file takes precedence over the corresponding agent-directory file. Files with the same name are not combined.\n\n## Context files\n\nContext files are separate from project `.pi` configuration. Pi loads them from the agent directory, the working directory, and its parent directories. A context file applies whenever Pi runs in its directory or anywhere below it.\n\nAn `AGENTS.override.md` replaces `AGENTS.md` or `CLAUDE.md` only in the same directory. It does not suppress context files from the agent directory or other directories.\n\nContext-file discovery does not require project trust.\n","isInternal":false,"tokens":754,"sizeBytes":3239},{"name":"containerization.md","path":"packages/coding-agent/docs/containerization.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/containerization.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Run Pi in an isolated environment\n\nUse an isolated environment to limit the files, credentials, processes, and network services that generated commands can access or affect.\n\nYou can isolate the complete Pi process or keep Pi on the host and route selected tools into an isolated environment.\n\n## Choose an isolation method\n\n| Method | Where Pi runs | What is isolated | Credential handling | Best for |\n|---|---|---|---|---|\n| Plain Docker | Container | Pi, built-in tools, `!` commands, and extensions | Credentials passed into the container | A straightforward local container boundary |\n| Docker Sandboxes | Managed sandbox | Pi, built-in tools, `!` commands, and extensions | Provider credentials remain on the host and are substituted by the proxy | Managed local isolation without exposing the real provider key |\n| OpenShell | Local or remote sandbox | Pi, built-in tools, `!` commands, and extensions | Policy-controlled credentials and inference routing | Filesystem, process, network, and credential policies |\n| Gondolin extension | Host | Built-in tools and `!` commands | Stored Pi credentials remain on the host, but commands inherit host environment variables | A local micro-VM for tool execution while retaining the host interface |\n\nThe method changes where extensions run. When the complete Pi process runs inside an isolated environment, its extensions run there too. When host Pi delegates built-in tools through Gondolin, other extension tools still run on the host unless they also delegate their work.\n\n## Decide what Pi can access\n\nAn isolated process can still affect resources you expose to it:\n\n- A read-write host mount lets Pi modify those host files.\n- Mounting `~/.pi/agent` exposes your Pi credentials, settings, extensions, and sessions.\n- Environment variables passed into a container are available to processes inside it.\n- Network access may allow code or tool output to leave the environment.\n- Tool-only isolation does not constrain the host Pi process or extension tools that do not use the isolated backend.\n\nExpose only the working folder, credentials, and network destinations needed for the task. Use read-only mounts or copy files into and out of the environment when you do not want writes to affect the host.\n\n## Run Pi in plain Docker\n\nPlain Docker provides the simplest whole-process container boundary.\n\n### Build the image\n\nCreate `Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\nBuild it from the directory containing the file:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n```\n\n### Start Pi\n\nFrom the working folder you want Pi to access, run:\n\n```bash\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\nReplace `ANTHROPIC_API_KEY` with the credential required by your provider. The named `pi-agent-home` volume keeps container-local settings, credentials, and sessions between runs.\n\nDo not mount the host's `~/.pi/agent` unless the container should have access to your host Pi configuration and credentials.\n\n### Verify the workspace\n\nInside Pi, run:\n\n```text\n!pwd\n```\n\nThe command should report `/workspace`. Changes under `/workspace` write through to the mounted host folder. Remove the bind mount or use a read-only mount when that is not acceptable.\n\n## Run Pi with Docker Sandboxes\n\n[Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) runs the complete Pi process inside a managed sandbox. Its proxy can keep the real provider credential on the host and substitute it when requests leave the sandbox.\n\nConfigure credentials before creating the sandbox. Do not run `/login` inside the sandbox because that writes a real credential into it.\n\n### Use a Claude Pro or Max token\n\nGenerate the token with `claude setup-token` on a machine with Claude Code. If an `anthropic` secret is already configured, remove it first so the proxy does not add an API-key header alongside the bearer token:\n\n```bash\nsbx secret rm anthropic\n\nsbx secret set-custom \\\n  --host api.anthropic.com \\\n  --env ANTHROPIC_OAUTH_TOKEN \\\n  --placeholder 'sk-ant-oat01-{rand}'\n```\n\n`sbx secret set-custom` reads the real token from standard input. The sandbox receives an OAuth-shaped placeholder, which the proxy replaces only for requests to the configured host.\n\nFor an Anthropic API key, use `sbx secret set anthropic` instead.\n\n### Start Pi\n\nRun this from the working folder you want mounted:\n\n```bash\nsbx run --kit \"docker.io/sbx/pi-kit:latest\" pi\n```\n\nFor an existing sandbox, run Pi non-interactively with:\n\n```bash\nsbx exec <sandbox-name> -- pi -p \"list the failing tests\"\n```\n\nSee the [Pi kit documentation](https://github.com/docker/sbx-kits-contrib/tree/main/pi) for other providers, troubleshooting, and image pinning.\n\n## Run Pi with OpenShell\n\n[NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) provides local or remote sandboxes with filesystem, process, network, credential, and inference policies.\n\n### Select a gateway\n\nEvery sandbox requires an active gateway:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\n### Create the sandbox\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nPi, its built-in tools, `!` commands, and extension tools run inside the OpenShell boundary.\n\n### Transfer files to a remote sandbox\n\nA remote gateway does not bind-mount your host working folder. Clone the repository inside the sandbox or transfer files explicitly:\n\n```bash\nopenshell sandbox upload pi-sandbox ./working-folder /workspace\nopenshell sandbox download pi-sandbox /workspace/working-folder ./working-folder-out\n```\n\nOpenShell inference routing can keep raw model credentials outside the sandbox. When configured, point Pi at the corresponding OpenAI-compatible or Anthropic-compatible endpoint exposed by the gateway.\n\n## Route tools through Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) is a local Linux micro-VM. Its example extension keeps the Pi process and file-based provider credentials on the host while routing the built-in tools and user `!` commands into the VM.\n\nCommands inside the VM inherit the host process environment. Provider keys supplied through environment variables can therefore be visible inside the VM. Do not use this pattern as a credential boundary unless you remove sensitive variables or change the extension's environment handling.\n\nGondolin requires Node.js 23.6 or newer and QEMU installed through your operating-system package manager.\n\n### Install the extension\n\nFrom a Pi source checkout:\n\n```bash\nmkdir -p ~/.pi/agent/extensions\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\n### Start Pi\n\nRun Pi from the working folder you want mounted:\n\n```bash\ncd /path/to/working-folder\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nThe extension mounts the host working folder at `/workspace` in the VM and overrides `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls`. File changes under `/workspace` write through to the host.\n\nOther extension tools still run on the host unless they explicitly delegate their operations. Review the [Gondolin example](../examples/extensions/gondolin/) before adding tools that could bypass the VM boundary.\n","isInternal":false,"tokens":1660,"sizeBytes":7589},{"name":"custom-provider.md","path":"packages/coding-agent/docs/custom-provider.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/custom-provider.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Custom Providers\n\nA provider extension connects Pi to a model service that needs custom authentication, model discovery, request handling, or streaming. If the service already speaks a supported API, configure it in `models.json` instead.\n\nProvider extensions run inside Pi and can inspect credentials, prompts, tool definitions, model responses, and usage. Treat them as trusted code and avoid logging secrets or provider payloads.\n\n## Choose the smallest integration\n\n| Requirement | Use |\n|---|---|\n| Add models behind a supported API | [`models.json`](models.md#configure-a-compatible-endpoint) |\n| Change an existing provider endpoint or headers | `models.json` or a small provider extension |\n| Discover models dynamically | A provider with `refreshModels` |\n| Add a `/login` flow | A provider with native or legacy OAuth configuration |\n| Implement an unsupported wire protocol | A provider with `stream` or `streamSimple` |\n\nA provider extension is an [extension](extensions.md), so it follows the same loading, trust, reload, and error behavior.\n\n## Register a provider\n\nCall `pi.registerProvider()` from the extension factory. Pi waits for asynchronous factories before startup continues, so providers registered there are available to startup model selection and `pi --list-models`.\n\nThere are two registration forms:\n\n- Register a complete `Provider` from `@earendil-works/pi-ai` for native authentication, filtering, discovery, refresh, and streaming behavior.\n- Register a provider name with `ProviderConfig` for the legacy configuration form used by existing extensions.\n\nPrefer a complete provider for new integrations that own more than static endpoint and model metadata. Pi composes `models.json` overrides above a registered native provider.\n\nRegistering only `baseUrl` or `headers` for an existing provider preserves its built-in models. Supplying `models` in the legacy form replaces that provider's models across chat, image, and classifier operations. An omitted `type` means `\"chat\"`; image and classifier models require explicit discriminants and implementations keyed by their `api` values through the `images` and `classifiers` fields.\n\n### Redirect an existing provider\n\nRegister only the fields that should change. This keeps the provider's built-in catalog, authentication, API implementation, and compatibility settings:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://gateway.example.com/anthropic\",\n    headers: {\n      \"X-Gateway-Key\": \"$GATEWAY_API_KEY\",\n    },\n  });\n}\n```\n\nUse this form for a proxy or gateway that preserves the upstream protocol. Do not copy the built-in model list into the extension because doing so turns a small endpoint override into a catalog replacement that can drift.\n\n### Register a compatible endpoint\n\nWhen a service implements a supported protocol but has its own model catalog, register the endpoint and only the models the service actually exposes:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"acme\", {\n    name: \"Acme AI\",\n    baseUrl: \"https://api.acme.example/v1\",\n    apiKey: \"$ACME_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        type: \"chat\",\n        id: \"acme-chat\",\n        name: \"Acme Chat\",\n        reasoning: false,\n        input: [\"text\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 8192,\n      },\n    ],\n  });\n}\n```\n\nPrefer `models.json` for this case when no executable extension behavior is needed. Use an extension when registration depends on runtime discovery, custom authentication, or code shared with other extension features.\n\n### Register mixed-operation models\n\nA mixed-operation provider can register non-chat models and their implementations together:\n\n```typescript\npi.registerProvider(\"media-tools\", {\n  apiKey: \"$MEDIA_TOOLS_API_KEY\",\n  models: [\n    {\n      type: \"image\",\n      id: \"image-v1\",\n      name: \"Image V1\",\n      api: \"media-images\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      output: [\"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n    },\n    {\n      type: \"classifier\",\n      id: \"classifier-v1\",\n      name: \"Classifier V1\",\n      api: \"media-classifier\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 64000,\n    },\n  ],\n  images: {\n    \"media-images\": { generateImages: async (model, context, options) => result },\n  },\n  classifiers: {\n    \"media-classifier\": { classify: async (model, context, options) => result },\n  },\n});\n```\n\nModel-level `baseUrl` values take precedence over the provider endpoint. If no `models` list is supplied, built-in models of every operation remain registered. Equal model IDs in different operations remain distinct, including their model-specific headers.\n\nCalls made after initial extension loading take effect immediately. Use `pi.unregisterProvider()` to remove the dynamic provider and restore built-in behavior that it replaced.\n\nSee the checked [GitLab Duo provider](../examples/extensions/custom-provider-gitlab-duo/) for a complete registration that delegates streaming to built-in API implementations.\n\n## Provide authentication\n\nStatic providers can resolve an API key from a literal, environment interpolation, or a command. These values use the same syntax as `models.json`:\n\n- `$NAME` and `${NAME}` read environment variables.\n- A leading `!command` uses command output.\n- `$$` emits a literal `$`.\n- `$!` emits a literal leading `!`.\n\nUse native provider authentication when the integration needs stored credentials, custom resolution, provider-scoped environment, or multiple login methods.\n\nAn OAuth provider supplies a display name, login flow, token refresh, and access-token resolution. After registration it appears in `/login`, and Pi stores returned credentials in `~/.pi/agent/auth.json`.\n\nOAuth callbacks are UI-neutral. They can open an authorization URL, show a device code, report progress, request input, or ask the user to choose a login method. Honor cancellation and the supplied abort signal during network requests.\n\nPress Enter (`tui.select.confirm`) to open the device-login URL and copy the code, or log in manually. Providers can disable either action with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false` in `onDeviceCode` or native `AuthEvent` notifications.\n\nNever write access tokens, refresh tokens, authorization headers, or complete provider responses to ordinary logs.\n\n## Supply and refresh models\n\nEvery model needs an ID, display name, input capabilities, and cost metadata. Chat and classifier models also need a context window; chat models need an output limit and reasoning support; image models declare their output modalities. Choose the API implementation at the provider level unless one model requires an override.\n\nSet `promptCache.short` or `promptCache.long` to the provider's best-effort cache lifetime in seconds when Pi should keep an idle prompt cache warm. Leave them unset to disable cache warming for that retention tier.\n\nCompatibility flags describe verified differences in an otherwise supported API. Do not enable them based only on an endpoint claiming compatibility.\n\nConfirm the request fields and response behavior against the actual server.\n\nUse `refreshModels` when the available catalog comes from a live service. Pass `context.signal` to blocking I/O so callers can cancel refreshes.\n\nThe two registration forms have different refresh contracts:\n\n- A complete `Provider` returns nothing. It calls `context.publish({ update })` to install provider-owned model state, after which its synchronous `getModels()` exposes the latest list.\n- Legacy `ProviderConfig.refreshModels` returns mixed-operation model definitions. Pi replaces that registration’s live models with the returned list and applies any requested persistence.\n\nPublish persisted catalog data only when it should survive across runs. A live service such as llama.cpp can update its in-memory list without persisting it; a remote catalog can retain a snapshot for offline startup.\n\n## Reuse a supported streaming API\n\nUse one of Pi AI’s API implementations whenever the provider protocol matches it.\n\nSupported implementations cover Anthropic Messages, OpenAI Chat Completions and Responses, Google Generative AI and Vertex, Azure OpenAI Responses, Mistral Conversations, and Bedrock Converse.\n\nThe provider can still customize authentication, base URLs, headers, model filtering, and discovery while delegating request conversion and streaming to an existing API implementation.\n\nThis is safer than copying a stream implementation because it preserves Pi’s message conversion, tool handling, usage accounting, cancellation, and compatibility behavior.\n\n## Implement custom streaming\n\nImplement `streamSimple` only when no existing API implementation can represent the service. Study the implementations under [`packages/ai/src/api`](https://github.com/earendil-works/pi/tree/main/packages/ai/src/api) first.\n\nThe stream receives a normalized `TranscriptContext`. System prompts and tool declarations live in transcript system messages, so read them with `getCurrentSystemPrompt(context.messages)` and `getCurrentTools(context.messages)` rather than expecting `context.systemPrompt` or `context.tools`. A model that supports mid-conversation system messages can receive them in place; otherwise call `collapseSystemMessages(context)` to fold later system messages into the leading one.\n\nA custom stream must:\n\n1. Create an assistant message with provider, model, timestamp, pending stop reason, content, and zeroed usage.\n2. After request setup succeeds, emit one `start` event before content events.\n3. Update the message while emitting balanced text, thinking, and tool-call events.\n4. Finalize usage, cost, content, and stop reason.\n5. Emit exactly one terminal `done` or `error` event and close the stream.\n6. Convert cancellation into an aborted result.\n\nRequest setup can fail before `start`; in that case the stream can terminate directly with `error`. Missing request authentication may also throw synchronously before a stream is returned.\n\nContent indexes refer to blocks in the assistant message. Update each block before emitting the event whose `partial` field exposes that state. Tool-call arguments must contain valid parsed input by `toolcall_end`.\n\nThe stream must also honor request instrumentation supplied through `SimpleStreamOptions`:\n\n- Call `options.onPayload` before sending the provider request and use any replacement payload it returns.\n- Call `options.onResponse` after receiving the response but before consuming its body.\n- Await `options.onProviderStreamEvent?.(providerEvent, model)` for each parsed provider event before normalizing it.\n- Pass through the abort signal and provider-scoped environment.\n\nThese hooks power extension request inspection, response-header events, and provider-stream observation. Omitting them makes the provider behave differently from Pi’s built-in providers.\n\n## Report failures and usage\n\nSet a concrete terminal stop reason. Error and aborted messages need an `errorMessage`; successful messages need accurate input, output, cache, total-token, and cost values.\n\nPi can compact and retry after recognized context-overflow errors. If the service uses an unknown message, normalize only that provider’s overflow response to `context_length_exceeded` in a guarded `message_end` handler.\n\nDo not rewrite rate limits or transient provider failures as context overflow. Those failures use Pi’s normal retry behavior instead.\n\n## Test the integration\n\nTest at least:\n\n- ordinary and empty text responses\n- tool calls and tool results\n- image input and image tool results when supported\n- usage and cost accounting\n- abort behavior\n- context overflow\n- malformed or partial streams\n- Unicode boundaries\n- cross-provider session handoff\n- authentication refresh and cancellation\n\nThe provider tests under [`packages/ai/test`](https://github.com/earendil-works/pi/tree/main/packages/ai/test) define the behavior expected from built-in providers. Adapt the relevant suites rather than relying only on manual prompts.\n\nRun the extension directly while developing, then move it to a discovered extension location or distribute it through a [Pi package](packages.md). Use `/reload` after changing a discovered provider extension in an active session.\n","isInternal":false,"tokens":2583,"sizeBytes":12737},{"name":"docs.json","path":"packages/coding-agent/docs/docs.json","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/docs.json","title":"Docs Skill","category":"anthropic-skill","format":"json","content":"{\n  \"navigation\": [\n    {\n      \"title\": \"Get Started\",\n      \"items\": [\n        {\n          \"title\": \"Overview\",\n          \"path\": \"index.md\"\n        },\n        {\n          \"title\": \"Quickstart\",\n          \"path\": \"quickstart.md\"\n        },\n        {\n          \"title\": \"How Pi Works\",\n          \"path\": \"how-pi-works.md\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Guides\",\n      \"items\": [\n        {\n          \"title\": \"Run Pi\",\n          \"items\": [\n            {\n              \"title\": \"Use Pi in the Terminal\",\n              \"path\": \"usage.md\"\n            },\n            {\n              \"title\": \"Choose a Model\",\n              \"path\": \"models.md\"\n            },\n            {\n              \"title\": \"Manage Sessions and Context\",\n              \"path\": \"sessions.md\"\n            },\n            {\n              \"title\": \"Run Pi Safely\",\n              \"path\": \"security.md\"\n            },\n            {\n              \"title\": \"Isolate Pi\",\n              \"path\": \"containerization.md\"\n            },\n            {\n              \"title\": \"Run Local Models\",\n              \"path\": \"llama-cpp.md\"\n            },\n            {\n              \"title\": \"Run Local Models on NVIDIA RTX\",\n              \"path\": \"nvidia-rtx-llama-cpp.md\"\n            },\n            {\n              \"title\": \"Configure Your Terminal\",\n              \"path\": \"terminal-setup.md\"\n            },\n            {\n              \"title\": \"Configure Shell Commands\",\n              \"path\": \"shell-aliases.md\"\n            },\n            {\n              \"title\": \"Run Pi in tmux\",\n              \"path\": \"tmux.md\"\n            },\n            {\n              \"title\": \"Run Pi on Windows\",\n              \"path\": \"windows.md\"\n            },\n            {\n              \"title\": \"Run Pi on Android\",\n              \"path\": \"termux.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Customize Pi\",\n          \"items\": [\n            {\n              \"title\": \"Configure Pi\",\n              \"path\": \"configuration.md\"\n            },\n            {\n              \"title\": \"Create Prompt Templates\",\n              \"path\": \"prompt-templates.md\"\n            },\n            {\n              \"title\": \"Add Skills\",\n              \"path\": \"skills.md\"\n            },\n            {\n              \"title\": \"Create Themes\",\n              \"path\": \"themes.md\"\n            },\n            {\n              \"title\": \"Use Pi Packages\",\n              \"path\": \"packages.md\"\n            },\n            {\n              \"title\": \"Connect MCP Servers\",\n              \"path\": \"mcp.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Build on Pi\",\n          \"items\": [\n            {\n              \"title\": \"Build Extensions\",\n              \"path\": \"extensions.md\"\n            },\n            {\n              \"title\": \"Add Custom Providers\",\n              \"path\": \"custom-provider.md\"\n            },\n            {\n              \"title\": \"Route with Virtual Models\",\n              \"path\": \"virtual-models.md\"\n            },\n            {\n              \"title\": \"Build Terminal UI Components\",\n              \"path\": \"tui.md\"\n            },\n            {\n              \"title\": \"Integrate with the CLI\",\n              \"path\": \"cli-integration.md\"\n            },\n            {\n              \"title\": \"Use the SDK\",\n              \"path\": \"sdk.md\"\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"title\": \"Reference\",\n      \"items\": [\n        {\n          \"title\": \"CLI\",\n          \"path\": \"cli.md\"\n        },\n        {\n          \"title\": \"Codemode\",\n          \"path\": \"codemode.md\"\n        },\n        {\n          \"title\": \"Slash Commands\",\n          \"path\": \"slash-commands.md\"\n        },\n        {\n          \"title\": \"Settings\",\n          \"path\": \"settings.md\"\n        },\n        {\n          \"title\": \"Models Configuration\",\n          \"path\": \"models-configuration.md\"\n        },\n        {\n          \"title\": \"Environment Variables\",\n          \"path\": \"environment-variables.md\"\n        },\n        {\n          \"title\": \"Keybindings\",\n          \"path\": \"keybindings.md\"\n        },\n        {\n          \"title\": \"Providers\",\n          \"path\": \"providers.md\"\n        },\n        {\n          \"title\": \"Session File Format\",\n          \"path\": \"session-format.md\"\n        },\n        {\n          \"title\": \"Compaction and Branch Summaries\",\n          \"path\": \"compaction.md\"\n        },\n        {\n          \"title\": \"JSON Event Stream\",\n          \"path\": \"json.md\"\n        },\n        {\n          \"title\": \"RPC Protocol\",\n          \"path\": \"rpc.md\"\n        },\n        {\n          \"title\": \"RPC Commands\",\n          \"path\": \"rpc-commands.md\"\n        },\n        {\n          \"title\": \"RPC Extension UI\",\n          \"path\": \"rpc-extension-ui.md\"\n        },\n        {\n          \"title\": \"Message Types\",\n          \"path\": \"message-types.md\"\n        }\n      ]\n    }\n  ],\n  \"redirects\": [\n    {\n      \"from\": \"development.md\",\n      \"to\": \"index.md\"\n    },\n    {\n      \"from\": \"session.md\",\n      \"to\": \"session-format.md\"\n    },\n    {\n      \"from\": \"tree.md\",\n      \"to\": \"sessions.md\"\n    },\n    {\n      \"from\": \"process-integration.md\",\n      \"to\": \"cli-integration.md\"\n    }\n  ]\n}\n","frontmatter":{"navigation":[{"title":"Get Started","items":[{"title":"Overview","path":"index.md"},{"title":"Quickstart","path":"quickstart.md"},{"title":"How Pi Works","path":"how-pi-works.md"}]},{"title":"Guides","items":[{"title":"Run Pi","items":[{"title":"Use Pi in the Terminal","path":"usage.md"},{"title":"Choose a Model","path":"models.md"},{"title":"Manage Sessions and Context","path":"sessions.md"},{"title":"Run Pi Safely","path":"security.md"},{"title":"Isolate Pi","path":"containerization.md"},{"title":"Run Local Models","path":"llama-cpp.md"},{"title":"Run Local Models on NVIDIA RTX","path":"nvidia-rtx-llama-cpp.md"},{"title":"Configure Your Terminal","path":"terminal-setup.md"},{"title":"Configure Shell Commands","path":"shell-aliases.md"},{"title":"Run Pi in tmux","path":"tmux.md"},{"title":"Run Pi on Windows","path":"windows.md"},{"title":"Run Pi on Android","path":"termux.md"}]},{"title":"Customize Pi","items":[{"title":"Configure Pi","path":"configuration.md"},{"title":"Create Prompt Templates","path":"prompt-templates.md"},{"title":"Add Skills","path":"skills.md"},{"title":"Create Themes","path":"themes.md"},{"title":"Use Pi Packages","path":"packages.md"},{"title":"Connect MCP Servers","path":"mcp.md"}]},{"title":"Build on Pi","items":[{"title":"Build Extensions","path":"extensions.md"},{"title":"Add Custom Providers","path":"custom-provider.md"},{"title":"Route with Virtual Models","path":"virtual-models.md"},{"title":"Build Terminal UI Components","path":"tui.md"},{"title":"Integrate with the CLI","path":"cli-integration.md"},{"title":"Use the SDK","path":"sdk.md"}]}]},{"title":"Reference","items":[{"title":"CLI","path":"cli.md"},{"title":"Codemode","path":"codemode.md"},{"title":"Slash Commands","path":"slash-commands.md"},{"title":"Settings","path":"settings.md"},{"title":"Models Configuration","path":"models-configuration.md"},{"title":"Environment Variables","path":"environment-variables.md"},{"title":"Keybindings","path":"keybindings.md"},{"title":"Providers","path":"providers.md"},{"title":"Session File Format","path":"session-format.md"},{"title":"Compaction and Branch Summaries","path":"compaction.md"},{"title":"JSON Event Stream","path":"json.md"},{"title":"RPC Protocol","path":"rpc.md"},{"title":"RPC Commands","path":"rpc-commands.md"},{"title":"RPC Extension UI","path":"rpc-extension-ui.md"},{"title":"Message Types","path":"message-types.md"}]}],"redirects":[{"from":"development.md","to":"index.md"},{"from":"session.md","to":"session-format.md"},{"from":"tree.md","to":"sessions.md"},{"from":"process-integration.md","to":"cli-integration.md"}]},"isInternal":false,"tokens":1144,"sizeBytes":5134},{"name":"environment-variables.md","path":"packages/coding-agent/docs/environment-variables.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/environment-variables.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Environment Variables\n\nPi uses environment variables in three ways:\n\n- Variables such as `PI_OFFLINE` configure the Pi process.\n- Pi sets process markers so child processes can identify Pi as the launching agent.\n- Commands run by the LLM-callable shell tools receive `PI_*` variables describing the current session.\n\nProvider API-key variables are documented separately in [Providers](providers.md#use-an-api-key-from-the-environment).\n\n## Process Marker\n\nThe CLI and RPC entry points set two process markers:\n\n- `AI_AGENT=pi` is a generic marker that lets tooling identify Pi as the agent that launched the process.\n- `PI_CODING_AGENT=true` is Pi-specific and lets child processes detect that they run inside Pi.\n\nChild processes inherit both markers. They are not session-specific and are not set automatically when Pi is embedded through the SDK.\n\n## Shell Tool Session Environment\n\nCommands run by the `bash` and `powershell` tools receive the current Pi session state:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_SESSION_ID` | Current session ID |\n| `PI_SESSION_FILE` | Absolute path to the current session JSONL file; unset for ephemeral sessions |\n| `PI_PROVIDER` | Currently selected model provider |\n| `PI_MODEL` | Currently selected model ID |\n| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |\n\nThe values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next shell command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.\n\nWhen asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt:\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\nThe session file can be inspected directly when the session is persistent:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nThese variables are injected into the LLM-callable `bash` and `powershell` tools. They are not injected into user-entered `!` or `!!` commands.\n\n### Custom Shell Tools\n\nTools created with `createBashTool()` or `createPowerShellTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nDisable session metadata independently of the spawn hook:\n\n```typescript\nconst powershellTool = createPowerShellTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nWhen disabled, Pi removes inherited values for these variables so nested Pi processes do not expose stale parent-session metadata.\n\n## Pi Process Configuration\n\nThese variables are read by Pi itself:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Override the config directory; default is `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Override session storage; overridden by `--session-dir` |\n| `PI_PACKAGE_DIR` | Override the package directory, useful for Nix/Guix store paths |\n| `PI_OFFLINE` | Disable automatic network activity, including model catalog refreshes |\n| `PI_SKIP_VERSION_CHECK` | Disable the `pi.dev` latest-version request |\n| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no` |\n| `PI_CODEMODE_MAX_WORKERS` | Maximum codemode workers alive at once across all sessions in the process; default `2`. Each worker's script may use up to 256 MB. Raise it when one process runs several sessions, such as in-process subagents. Further scripts wait for a free worker. See [Codemode](codemode.md) |\n| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |\n| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |\n| `PI_RADIUS_GATEWAY` | Override the Radius gateway origin used by `/bug` uploads |\n| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |\n| `PI_CLIPBOARD_OSC52` | Set to `1` to also send copied text to the terminal with OSC 52 after the local clipboard write, as Pi does over SSH. Use it when a remote client reaches Pi without SSH variables, such as a multiplexer attached from another machine or a remote IDE daemon |\n| `PI_HYPERLINKS` | Override OSC 8 hyperlink detection with `1`, `0`, or `auto` |\n| `PI_PROGRAM_STATUS` | Override OSC 7501 program status detection: `1` always reports, `0` never reports; otherwise Pi reports only after the terminal confirms support. See [Terminal setup](terminal-setup.md#program-status) |\n| `PI_IMAGE_PROTOCOL` | Override inline image detection with `kitty`, `iterm2`, `none`, or `auto` |\n| `PI_TRUE_COLOR` | Override truecolor detection with `1`, `0`, or `auto` |\n| `PI_TUI_ESC_TIMEOUT` | How long to wait after a lone ESC before treating it as Escape, in milliseconds; defaults to `100` over SSH and `10` otherwise. Increase if Alt-key input is misread as Escape |\n| `VISUAL`, `EDITOR` | External editor fallback when `externalEditor` is unset |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |\n\nProvider credentials such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and provider-specific configuration are listed in [Providers](providers.md#use-an-api-key-from-the-environment).\n","isInternal":false,"tokens":1321,"sizeBytes":5594},{"name":"extensions.md","path":"packages/coding-agent/docs/extensions.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/extensions.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Extensions\n\nExtensions are TypeScript modules that add executable behavior to Pi. Use one when a workflow needs tools, commands, event handlers, model providers, session state, or terminal UI rather than instructions alone.\n\nAn extension runs inside the Pi process with the same operating-system permissions. It can inspect prompts, tool calls, files, credentials, and session history, so load extensions only from sources you trust.\n\nTypical extensions add an agent tool, protect paths, confirm dangerous commands, react to session events, modify context, expose a command, or display persistent status.\n\n<a id=\"quick-start\"></a>\n<a id=\"writing-an-extension\"></a>\n<a id=\"create-an-extension\"></a>\n\n## Create and load an extension\n\nAn extension exports a default factory that receives `ExtensionAPI`. The factory registers capabilities for the current extension runtime.\n\nCreate `~/.pi/agent/extensions/hello.ts`:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"hello\", {\n    description: \"Show a greeting\",\n    handler: async (name, ctx) => {\n      ctx.ui.notify(`Hello, ${name || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\nStart Pi and run `/hello`. During development, load a file directly:\n\n```bash\npi --extension ./hello.ts\n```\n\nPi uses `jiti`, so local TypeScript extensions do not need a separate compilation step. Use [Pi packages](packages.md) for distributed extensions and dependencies.\n\n<a id=\"extension-locations\"></a>\n<a id=\"available-imports\"></a>\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the extension in your user or project extensions directory. Pi loads direct TypeScript or JavaScript files and subdirectories containing an `index.ts` or `index.js` entry point.\n\nUse a single file for a small extension and a directory for a multi-file implementation. Put npm dependencies in a nearby `package.json`. See [Configuration](configuration.md) for conventional locations and [Settings](settings.md#resources) for additional paths.\n\nReload replaces the extension runtime, so code after `await ctx.reload()` must not reuse state from the old runtime. Only personal and explicit command-line extensions can participate in the `project_trust` event that runs before project extensions load.\n\n<a id=\"understand-the-lifecycle\"></a>\n\n## Understand the runtime lifecycle\n\nThe factory can be synchronous or asynchronous. Pi waits for an asynchronous factory before startup continues, allowing it to fetch configuration or register providers needed during startup.\n\nDo not start processes, sockets, watchers, or timers in the factory because some invocations load extensions without starting a session.\nStart long-lived resources from `session_start` or from the command or tool that needs them.\nClose session-scoped resources from an idempotent `session_shutdown` handler.\n\nA normal prompt starts a run. The run repeats turns, then checks whether more work starts another low-level agent run:\n\n```mermaid\nflowchart TD\n    Input[\"input\"] --> Before[\"before_agent_start\"]\n    Before --> AgentStart[\"agent_start\"]\n    AgentStart --> Turns[[\"turns\"]]\n    Turns --> AgentEnd[\"agent_end\"]\n    AgentEnd --> MoreRuns{\"More work?\"}\n    MoreRuns -->|yes| AgentStart\n    MoreRuns -->|no| BeforeSettle[\"agent_before_settle\"]\n    BeforeSettle --> Continue{\"Continuation requested?\"}\n    Continue -->|yes| AgentStart\n    Continue -->|no| Settled[\"agent_settled\"]\n```\n\nMore work means a retry, recovery, compaction retry, or queued work.\n\nEach turn sends one model request and executes the tool calls in the assistant response:\n\n```mermaid\nflowchart TD\n    TurnStart[\"turn_start\"] --> Request[[\"model request\"]]\n    Request --> ToolDecision{\"Tool calls?\"}\n    ToolDecision -->|yes| Tools[[\"tool execution\"]]\n    Tools --> TurnEnd[\"turn_end\"]\n    ToolDecision -->|no| TurnEnd\n    TurnEnd --> MoreTurns{\"Another turn?\"}\n    MoreTurns -->|yes| TurnStart\n    MoreTurns -->|no| AgentEnd([\"agent_end\"])\n```\n\nThe model request runs these hooks in order:\n\n1. `context`\n2. `context_with_system`\n3. `before_provider_headers`\n4. `before_provider_request`\n5. `after_provider_response`\n6. `provider_stream_event` and assistant message events\n\nTool execution runs these hooks in order for each tool call:\n\n1. `tool_execution_start`\n2. `tool_call`\n3. `tool_execution_update` while the tool executes\n4. `tool_result`\n5. `tool_execution_end`\n\nThe lifecycle involves these event groups:\n\n| Area | Events |\n|---|---|\n| Input preparation | `input`, `before_agent_start` |\n| Agent run | `agent_start`, `agent_end` |\n| Model turn | `turn_start`, `context`, `context_with_system`, `turn_end` |\n| Provider call | `before_provider_headers`, `before_provider_request`, `after_provider_response`, `provider_stream_event` |\n| Message lifecycle | `message_start`, `message_update`, `message_end` |\n| Tool execution | `tool_execution_start`, `tool_call`, `tool_execution_update`, `tool_result`, `tool_execution_end` |\n| Settlement | `agent_before_settle`, `agent_settled` |\n\nPi counts as busy while a run starts, including the compaction check and `before_agent_start`, so messages sent meanwhile go into the starting run. Message lifecycle events also cover user and tool-result messages; the model request list places only the assistant stream where it occurs in each turn.\n\nTool hooks run for each tool call, and one assistant response can start another turn by returning tool calls. Provider hooks run only when that transport exposes the corresponding stage. See the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event payloads and result types.\n\nSession startup and replacement follow a separate lifecycle:\n\n```mermaid\nflowchart TD\n    Startup[\"Process startup\"] --> Trust[\"project_trust when needed\"]\n    Trust --> SessionStart[\"session_start\"]\n    Switch[\"/new or /resume\"] --> BeforeSwitch[\"session_before_switch\"]\n    Fork[\"/fork or /clone\"] --> BeforeFork[\"session_before_fork\"]\n    BeforeSwitch --> Shutdown[\"session_shutdown\"]\n    BeforeFork --> Shutdown\n    Reload[\"reload\"] --> Shutdown\n    Shutdown --> Replacing{\"Replacing the runtime?\"}\n    Replacing -->|yes| Replacement[\"load replacement extension runtime\"]\n    Replacement --> SessionStart\n    Replacing -->|no| Stop[\"process exits\"]\n    SessionStart --> Discover[\"resources_discover\"]\n```\n\n`project_trust` runs before trusted project resources load and can run again when session replacement enters a new working directory. Retries, recovery, compaction, and queued messages can start another agent run after `agent_end`, so `agent_end` is not a final-idle signal. Reload and session replacement invalidate the old runtime. Code after `await ctx.reload()` still belongs to the old runtime and must not reuse its session-bound state.\n\n### Choose an event by intent\n\n| Goal | Event |\n|---|---|\n| Inspect, transform, or consume raw user input | `input` |\n| Change prompt sections, active tools, or add context before a run | `before_agent_start` |\n| Observe one low-level agent run | `agent_start`, `agent_end` |\n| Act after one model response and its tool calls | `turn_end` |\n| Filter conversation messages for one provider request | `context` |\n| Transform the complete transcript, including system messages | `context_with_system` |\n| Change request headers or the serialized provider payload | `before_provider_headers`, `before_provider_request` |\n| Observe provider response metadata or parsed stream events | `after_provider_response`, `provider_stream_event` |\n| Authorize or modify a tool call | `tool_call` |\n| Redact, annotate, or replace a tool result | `tool_result` |\n| Append final entries or request one last continuation | `agent_before_settle` |\n| Run notification work after automatic processing has stopped | `agent_settled` |\n| Initialize or release session-scoped resources | `session_start`, `session_shutdown` |\n\n<a id=\"agent_start--agent_end--agent_before_settle--agent_settled\"></a>\n\nUse `agent_before_settle` when an extension must append entries or request one final continuation. Use `agent_settled` for notification-only work that should run after Pi has no automatic continuation remaining.\n\n<a id=\"extensionapi-methods\"></a>\n\n## Choose an integration point\n\n| Capability | Main API |\n|---|---|\n| Observe or modify lifecycle behavior | `pi.on()` |\n| Add a model-callable operation | `pi.registerTool()` |\n| Add a `/` command | `pi.registerCommand()` |\n| Add a shortcut or CLI flag | `pi.registerShortcut()` or `pi.registerFlag()` |\n| Send user or custom messages | `pi.sendUserMessage()` or `pi.sendMessage()` |\n| Persist non-context session data | `pi.appendEntry()` |\n| Change active tools, model, or thinking level | Session control methods on `pi` |\n| Add a model provider | `pi.registerProvider()` |\n| Add an MCP server | `pi.registerMcpServer()` |\n| Route each request to a model | [`pi.registerVirtualModel()`](virtual-models.md) |\n| Add terminal rendering | Renderer registration and `ctx.ui` |\n| Communicate with another extension | `pi.events` |\n\nUse the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event, context, tool, and result types.\n\n## Follow the extension contracts\n\n<a id=\"events\"></a>\n<a id=\"work-with-events\"></a>\n\n### Events and concurrency\n\nHandlers run in extension load and registration order. `pi.on()` returns a function that unsubscribes that registration; changes do not affect a dispatch already in progress.\nSome events notify; others transform data, replace results, or cancel an operation.\nUse each event’s declared result type rather than assuming every return value has an effect.\n\nEvents cover resource discovery, sessions, agent and message lifecycle, providers, tools, and raw input.\n\n#### Understand handler composition\n\n| Pattern | Behavior |\n|---|---|\n| Notification | Return values are ignored. Use the event for observation only. |\n| In-place mutation | Later handlers see changes to values such as `before_provider_headers.headers`, `before_agent_start.systemPromptOptions`, and `tool_call.input`. |\n| Chained transformation | Later handlers receive the value returned by earlier `input`, `context`, `context_with_system`, `message_end`, `tool_result`, and `before_provider_request` handlers. |\n| Gate | A handled input, blocked tool call, cancelled session operation, or handled `user_bash` command stops that operation's normal fallback path. |\n| Actionable boundary | Each `turn_end` or `agent_before_settle` handler sees the entries and continuation decision proposed so far. |\n| Last override | The last `cache_warming_decision` handler that returns an action wins. |\n\nNotification handlers should not perform slow work on a streaming event. Transform handlers should preserve fields they do not intentionally own, and gate handlers should fail closed when their safety decision cannot be made.\n\n`before_agent_start` exposes both the current prompt and its structured `systemPromptOptions`. `event.messageType` says what started the run: `\"user\"` for a prompt or `pi.sendUserMessage()`, with `prompt` and `images`; `\"custom\"` for `pi.sendMessage(..., { triggerTurn: true })`, with the starting `message` and no `prompt`. A returned `message` is added to either kind of run; check `messageType` to skip runs started by custom messages. Prefer changing prompt sections, selected tools, or guidelines so Pi can append a transcript delta. Returning `systemPrompt`, or setting `forceSystemPrompt`, replaces the whole prompt for that run while the transcript continues recording the structured sections. Providers receive the forced text as their leading system prompt.\n\n`message_end` can replace a finalized message while preserving its role. `tool_call` can mutate input or block execution. `tool_result` handlers compose, with each handler seeing prior changes.\n\n<a id=\"provider_stream_event\"></a>\n\n`provider_stream_event` fires for each parsed provider stream event before Pi normalizes it. The event identifies the provider, API, and model; `event.data` is the earliest structured value available to Pi, not necessarily the original HTTP bytes or SSE frame. Treat it as read-only because mutation can affect normalization. The event is notification-only and is not persisted.\n\nHandlers are awaited in stream order, so slow handlers delay stream consumption. Handler errors are reported without changing the provider response. See [`debug-provider.ts`](../examples/extensions/debug-provider.ts) for an opt-in viewer that groups raw events by assistant message.\n\n<a id=\"context_with_system\"></a>\n\n`context` transforms conversation messages without prompt and tool system messages; Pi restores that state afterward. Use `context_with_system` only when a request-local transformation must own the complete transcript, and keep a system message at index zero.\n\n`turn_end` and `agent_before_settle` are actionable boundaries. Their handlers can chain proposed `custom`, `custom_message`, `context_edit`, or `compaction` entries and return `continue: true` for one next model request. Guard continuation conditions because an unconditional continuation can loop. Use the exported event declarations for the complete validation and ordering contract.\n\n<a id=\"cache_warming_decision\"></a>\n\n`cache_warming_decision` can override an idle prompt-cache refresh with `{ action: \"warm\" }` or `{ action: \"stop\" }`. The last handler that returns an action wins.\n\nTool calls from one assistant message can run in parallel.\nDo not assume a sibling call or result exists when another tool event runs.\nUse `ctx.signal` for nested work owned by an active turn; commands and idle session events often have no operation signal.\n\nA `user_bash` handler that returns `undefined` passes the command to the next handler and then to local execution if no handler handles it. Returning `operations` or `result` stops propagation. A handler failure blocks the command rather than falling through to local execution.\n\n<a id=\"custom-tools\"></a>\n<a id=\"register-tools\"></a>\n\n### Tools\n\nA custom tool defines a name, model-facing description, TypeBox parameter schema, and `execute()` function.\nIts result requires model-facing `content` and a `details` field for rendering or state reconstruction.\nUse `details: undefined` when there are no structured details. If the tool makes nested model calls, include their `usage` in the result so session totals remain accurate.\n\nThrow from `execute()` to produce a failed tool result.\nReturning an object does not mark it as an error.\nReturn `terminate: true` only when the agent should skip its automatic follow-up after every completed tool in that batch agrees to terminate.\n\nUse sequential execution when tools share mutable in-memory state.\nFile-mutating tools should wrap the complete read-modify-write operation with `withFileMutationQueue()`.\nTruncate large model-facing results and tell the model where to read the complete output.\n\nDeclare `outputSchema` and return a matching `structuredContent` when the result is data. The model still receives `content`; programmatic callers such as codemode scripts receive `structuredContent` instead of the text. Tools without `outputSchema` are passed to scripts as their text content. To report a failure that still carries data, return the result with `isError: true` instead of throwing: the model sees an error, and scripts still receive `structuredContent`.\n\nA tool can run other tools with `ctx.executeTool(name, args, { signal, onUpdate })`. Nested calls go through argument validation and the `tool_call` and `tool_result` handlers like model-issued calls, and emit `tool_execution_start`, `tool_execution_update`, and `tool_execution_end`; all of these events carry `parentToolCallId`, and their `toolCallId` is assigned by pi as `<parent id>/<n>`. These ids do not appear as tool calls or tool results in the transcript. Nested calls do not add transcript entries: their results only reach the calling tool, which reports them itself, for example through `onUpdate` and `details`. The session keeps a bounded record of them (name, arguments, status, duration, error; never results) as `nestedCalls` on the calling tool's result message. It is used for compaction file lists and shown in HTML exports. Arguments over 8 KiB per call or 32 KiB per tool result are omitted, at most 256 calls are kept, and `complete: false` marks a record that lost anything. The `usage` of nested results, at every depth, is added to the calling tool's result `usage`, so a tool reports only its own usage, not that of the tools it called. `ctx.tools` lists the tools `ctx.executeTool()` can call. `tool_result` handlers that redact `content` should also replace `structuredContent`; replacing only `content` drops it.\n\nSee [`hello.ts`](../examples/extensions/hello.ts), [`todo.ts`](../examples/extensions/todo.ts), [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts), and [`truncated-tool.ts`](../examples/extensions/truncated-tool.ts).\n\n### Tool exposure\n\n`exposure` controls how the model reaches a tool. \"Callable\" means callable from other tools through `ctx.executeTool()` (`ctx.tools`), as the `codemode` tool's scripts do:\n\n- `direct` (default): declared to the model while active, and callable while active.\n- `model-only`: declared to the model while active, never callable. Use it for tools that orchestrate other tools or ask the user.\n- `codemode`: callable whenever registered, and listed by the `codemode` tool. Not declared to the model unless activated explicitly.\n- `deferred`: like `codemode`, but codemode tools do not list it; `tool_search` can find and activate it.\n- `hidden`: registered but unreachable. Re-register a tool with `exposure: \"hidden\"` to withdraw it, since tools cannot be unregistered.\n\n`namespace: { name, aliases, description, instructions }` groups related tools, as MCP servers do. Codemode tools list a namespace under one heading with its `description`. `instructions` holds longer usage guidance; it is not listed, and codemode scripts read it with `describeNamespace(name)`. `aliases` are other names scripts may look the namespace up by, for example the server name `docs` for `mcp__docs`; a name and the same name as a script identifier (`my-docs`, `my_docs`) match alike.\n\nRegistering a `direct` or `model-only` tool activates it; the other exposures are not activated on registration. The active set (`pi.getActiveTools()`, `pi.setActiveTools()`) is the set of tools declared to the model. `pi.getAllTools()` reports each tool's `exposure`, `namespace`, `annotations`, and `reachesTools`.\n\n`annotations` are hints about what a tool does, with the meaning of MCP tool annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. MCP tools carry the hints their server declares. Missing hints take the MCP defaults: a tool is not read-only, and may be destructive and reach an open world. The hints are not verified, but a permission extension can use them to decide which calls to confirm. This confirms the calls Codex asks approval for:\n\n```typescript\npi.on(\"tool_call\", async (event, ctx) => {\n  const hints = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;\n  const needsApproval =\n    hints?.destructiveHint === true ||\n    (!hints?.readOnlyHint && ((hints?.destructiveHint ?? true) || (hints?.openWorldHint ?? true)));\n  if (needsApproval && !(await ctx.ui.confirm(\"Allow tool call?\", event.toolName))) {\n    return { block: true, reason: `${event.toolName} was not approved` };\n  }\n});\n```\n\nA tool that orchestrates other tools can adjust what the model sees while it is active with `prepareLoadout(loadout)`. It runs whenever the active tools change and receives the declared tools, the callable tools, and every registered tool with its exposure, namespace, and prompt guidelines. It returns replacement `descriptions` for declared tools (including its own) and `hiddenDeclarations`: active tools whose declarations requests leave out while they stay active and callable. The default system prompt leaves hidden tools out of its tool list and rules, and names no tool in the skills hint when the file reader is hidden, so the orchestrating tool should present their guidelines itself. `codemode` uses only this hook, `exposure`, `reachesTools`, and `ctx.executeTool()`, so another tool can implement the same behavior under a different name.\n\nA tool that reaches other tools declares which ones a call may reach with `reachesTools(input)`: `\"all\"`, or the words of the input that may name them, where a word refers to every tool or namespace whose name it contains. `codemode` returns `\"all\"` for scripts that search or list tools and the words of the script otherwise; `tool_search` always returns `\"all\"`. Extensions that register tools late use it from their `tool_call` handler to finish registering the tools a call needs before it runs, as the MCP extension does for servers that are still connecting. The MCP extension also activates `codemode` or `tool_search` for MCP tools only when the tool of that name declares `reachesTools`.\n\n### Activate tools dynamically\n\nRegister every tool first, keep optional tools inactive, and use `pi.setActiveTools()` from a loader tool to select the desired active tools. Names must already be registered; unknown names are ignored.\n\nPi records the initial prompt and tool set in the transcript's first system message, then appends tool and prompt changes before the next model request. Providers that cannot represent the transition receive a complete transcript checkpoint, which can invalidate the cached prefix.\n\n### Tool rendering\n\nA tool's `renderCall` and `renderResult` draw its calls in the interactive transcript and in HTML exports. `pi.registerToolRenderer((toolName, next) => renderers)` chooses renderers for calls to any tool, including tools that are not registered yet, such as MCP tools in a resumed session before their server connected. `next()` returns what the remaining resolvers (in extension load order), then the registered tool, would use, so `next() ?? mine` only fills in.\n\n### MCP servers\n\n`pi.registerMcpServer(name, config)` adds an MCP server for the current session. `config` has the shape of an `mcpServers` entry in [`mcp.json`](mcp.md): `command`, `args`, `env`, and `cwd` for stdio servers, `url`, `headers`, and `oauth` for HTTP servers, plus `exposure`, `toolExposure`, `description`, `enabled`, and `timeout`.\n\n```typescript\npi.registerMcpServer(\"jira\", { url: \"https://mcp.example.com/jira\", exposure: \"codemode\" });\npi.unregisterMcpServer(\"jira\");\n```\n\nServers registered while the extension loads connect when the session starts, together with the `mcp.json` servers; servers registered later connect right away, and `pi.unregisterMcpServer()` closes the connection and makes the server's tools unreachable. Registrations are not saved: register again on every load, for example based on the extension's own settings. A server in `mcp.json` with the same name takes precedence, and `/mcp` shows the override. Registering the same name again replaces the extension's earlier registration; names registered by another extension, invalid names, and invalid configs throw.\n\nThe built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#other-mcp-extensions)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `pi.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.\n\n<a id=\"extensioncontext\"></a>\n<a id=\"extensioncommandcontext\"></a>\n<a id=\"use-extension-context\"></a>\n\n### Context and session changes\n\n`ExtensionContext` provides the working directory, mode, UI, session manager, model runtime, abort signal, context usage, and controls for compaction and shutdown.\nUse `ctx.modelRegistry.streamSimple()` for provider-neutral nested model calls.\n\nCommand handlers receive `ExtensionCommandContext`, which adds operations for waiting until idle, reloading, tree navigation, and session replacement.\nThese operations are command-only because calling them from lifecycle handlers can deadlock the runtime.\n\nSession replacement invalidates the old context. Capture only plain data before switching, then use the fresh context supplied to `withSession` for session-bound work.\n\n<a id=\"state-management\"></a>\n<a id=\"persist-state\"></a>\n\n### State\n\nChoose storage based on how state participates in the conversation:\n\n| State | Storage |\n|---|---|\n| Tool state that follows the active branch | Tool-result `details` |\n| Durable data excluded from model context | `pi.appendEntry()` |\n| Custom content stored and sent to the model | `pi.sendMessage()` |\n| Data outside one session | External storage |\n\nReconstruct branch-sensitive state from `ctx.sessionManager.getBranch()` during `session_start`.\nDo not rebuild it from every file entry because abandoned branches represent alternative histories.\nRegister an entry or message renderer when custom stored content should appear in the transcript.\n\n<a id=\"custom-ui\"></a>\n<a id=\"mode-behavior\"></a>\n<a id=\"interact-with-the-user\"></a>\n<a id=\"account-for-each-mode\"></a>\n\n### UI and modes\n\n`ctx.ui` provides dialogs, notifications, status text, widgets, titles, editor access, and custom components.\nUse `ctx.ui.custom()` only when the interaction needs its own rendering and input.\nSee [Terminal UI](tui.md) for component, focus, overlay, theme, and performance guidance.\n\nExtensions load in interactive, RPC, JSON, and print modes.\nInteractive mode provides the complete terminal UI.\nRPC can forward supported dialogs and notifications through the [RPC Extension UI protocol](rpc-extension-ui.md), but not custom terminal components; JSON and print modes have no UI.\nGuard terminal-only behavior with `ctx.mode === \"tui\"` and use `ctx.hasUI` for interactions supported by interactive and RPC clients.\n\nKeep tool and event behavior independent from rendering so non-interactive modes remain functional.\n\n<a id=\"error-handling\"></a>\n<a id=\"handle-errors-and-shutdown\"></a>\n\n### Errors and cleanup\n\nPi reports handler errors and continues where possible. A `tool_call` handler failure blocks the tool as a fail-safe; a tool execution failure becomes an error result for the model.\n\nRelease resources in `session_shutdown` even when normal operation attempted cleanup.\nKeep cleanup idempotent because cancellation, reload, session replacement, and process exit can converge on the same path.\nUse `ctx.shutdown()` to request an orderly process shutdown.\n\n<a id=\"examples-reference\"></a>\n<a id=\"use-examples-as-the-implementation-reference\"></a>\n\n## Examples and reference\n\nThe checked [extension examples](../examples/extensions/) are the implementation reference. Start with the smallest example matching the behavior you need:\n\n| Goal | Start with | Why |\n|---|---|---|\n| Confirm or block tool calls | [`permission-gate.ts`](../examples/extensions/permission-gate.ts) | Combines `tool_call`, mode-aware UI, and fail-closed behavior. |\n| Modify prompt sections for a run | [`prompt-customizer.ts`](../examples/extensions/prompt-customizer.ts) | Uses structured prompt options instead of replacing the complete prompt. |\n| Keep branch-aware tool state | [`todo.ts`](../examples/extensions/todo.ts) | Reconstructs state from persisted entries and renders it in the TUI. |\n| Register tools after startup | [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts) | Shares registration logic between lifecycle and command handlers. |\n| Run cancellable model work from a command | [`qna.ts`](../examples/extensions/qna.ts) | Combines nested model calls, cancellation, and custom UI. |\n| Replace the main editor | [`modal-editor.ts`](../examples/extensions/modal-editor.ts) | Extends `CustomEditor` while preserving application controls. |\n| Run finalization at shutdown | [`auto-commit-on-exit.ts`](../examples/extensions/auto-commit-on-exit.ts) | Uses the session shutdown hook for finalization work. |\n| Route built-in tools to another machine | [`ssh.ts`](../examples/extensions/ssh.ts) | Reuses tool operation interfaces instead of recreating tool contracts. |\n| Add OAuth and provider-specific streaming | [`custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/) | Shows a complete provider integration while delegating supported protocols. |\n\nUse [Custom Providers](custom-provider.md) for model-service integrations, [Terminal UI](tui.md) for custom components, and [Pi Packages](packages.md) to install or distribute extensions with other resources. Use the exported declarations for exact signatures, payloads, and result schemas; this guide focuses on lifecycle choices, composition, and durable implementation patterns.\n","isInternal":false,"tokens":6204,"sizeBytes":28998},{"name":"how-pi-works.md","path":"packages/coding-agent/docs/how-pi-works.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/how-pi-works.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# How Pi Works\n\nPi coordinates model requests, tool execution, context assembly, and session storage. A session is Pi's record of a conversation, including messages, tool calls and results, model changes, compactions, and other events.\n\nMessages and events in a session form a tree. Each path through that tree is a branch. The branch ending at the current entry is the active branch and supplies the history for the next model request.\n\n## Agent loop\n\nA submitted message is added to the active branch. Pi builds a model request from the system prompt, active branch, available tools, and model settings, then sends it through the selected provider.\n\nThe provider streams an assistant response, which can contain text and tool calls. Pi records the response, executes each tool call, and records the results. That completes one turn. If tool results or queued messages require another model request, Pi starts another turn. Otherwise, the run ends.\n\nSteering messages enter after the current assistant turn. Follow-up messages enter after the agent has finished its pending work. Aborting stops the current run and returns queued messages to the editor.\n\n## Context\n\nThe active branch supplies conversation history. Pi converts its session entries into model-compatible user, assistant, and tool-result messages.\n\nPi builds the system prompt from its base instructions and discovered context files. The request also carries tool definitions and skill descriptions.\n\nFull skill instructions are loaded on demand. Extensions can add instructions or transform context.\n\nPrompt templates expand editor input before it becomes a user message. Selected files, images, pasted text, and shell output can become message content.\n\n## Sessions\n\nPersistent sessions are JSONL files. Each tree entry has an ID and refers to its parent. The current entry identifies the active branch.\n\nContinuing from an earlier entry creates another branch in the same file. Forking and cloning copy selected history into a new session file.\n\nModel context is reconstructed from the active branch. Compaction inserts a summary entry that replaces older messages in subsequent model requests. The original entries remain in the session tree.\n\n## Interfaces\n\nInteractive mode renders session and agent events in the terminal. Print mode runs a prompt and writes the final response. JSON mode writes agent events as JSONL.\n\nRPC mode accepts JSONL commands on stdin and writes responses and events to stdout. The TypeScript SDK creates and controls agent sessions in process.\n\nAll interfaces use the same agent and session mechanisms.\n\n## Extensions and resources\n\nExtensions are TypeScript modules loaded into the Pi process. Their factory functions register tools, commands, shortcuts, providers, event handlers, renderers, and terminal UI.\n\nSkills provide on-demand instructions and supporting files. Prompt templates provide reusable message text. Themes provide terminal colors. Pi packages distribute these resources through npm or git.\n\n## Trust and permissions\n\nPi resolves project trust before loading project settings and resources. After the trust decision and project-resource loading, Pi loads context files. Enabled tools use the operating-system permissions of the Pi process. Extensions execute inside that process.\n","isInternal":false,"tokens":597,"sizeBytes":3300},{"name":"index.md","path":"packages/coding-agent/docs/index.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/index.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Pi\n\nPi is an extensible AI agent that works from your terminal. Give it a goal and a working folder, and it can inspect files, run commands, edit content, and work through multi-step tasks.\n\nUse Pi for software development, research notes, writing projects, data files, or hobby work. You can use Pi as is, prompt it to adapt itself to your workflow, or build other applications powered by Pi using the SDK.\n\n## Start using Pi\n\nNew to Pi? Follow the [Quickstart](quickstart.md) to install Pi, connect a model, and complete your first task.\n\nIf Pi is already installed, choose what you want to do:\n\n- [Use Pi interactively](usage.md) to add files, run commands, direct ongoing work, and export results.\n- [Choose a model](models.md) or connect a subscription, API key, local model, or compatible endpoint.\n- [Continue or branch a session](sessions.md) to resume work or explore another approach without losing history.\n- [Configure Pi](configuration.md) for your preferences, working folders, instructions, and reusable resources.\n- [Understand how Pi works](how-pi-works.md), including tools, context, sessions, and the agent loop.\n\n## Customize Pi\n\nPi can reuse prompts, load specialized instructions, add executable integrations, change its terminal interface, connect model services, and distribute these resources as packages.\nUse the [Quickstart customization chooser](quickstart.md#choose-how-to-customize-pi) to select the smallest mechanism that meets your need.\n\n## Automate or embed Pi\n\n- Use [print mode](cli.md#invocation-and-output) for one-off and scripted tasks.\n- Use [JSON event stream mode](json.md) to consume structured events from one run.\n- Use [RPC mode](rpc.md) to control a separate Pi process.\n- Use the [TypeScript SDK](sdk.md) to run Pi inside an application.\n\n## Find reference and setup information\n\nUse the reference pages to look up [CLI options](cli.md), [settings](settings.md), [providers](providers.md), [keybindings](keybindings.md), and [environment variables](environment-variables.md).\n\nFor platform-specific help, see [Terminal Setup](terminal-setup.md), [Windows](windows.md), [tmux](tmux.md), [Termux on Android](termux.md), or [Containerization](containerization.md).\n\n## Work safely\n\nPi's tools and extensions run with the permissions of the Pi process. Project trust controls which project resources Pi loads, but it does not sandbox tool calls. Review [Security](security.md) before using untrusted files, repositories, extensions, or unattended automation.\n","isInternal":false,"tokens":555,"sizeBytes":2507},{"name":"json.md","path":"packages/coding-agent/docs/json.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/json.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# JSON Event Stream\n\nJSON mode emits structured progress for one invocation:\n\n```bash\npi --mode json \"Review this repository\"\n```\n\nPi writes one session header followed by session events, then exits after the supplied prompts finish. RPC mode emits the same session-event shapes but has no session header because it is a bidirectional, long-lived protocol. See [RPC Mode](rpc.md).\n\nThis page is the canonical reference for events shared by JSON and RPC mode. Message values use the [shared message types](message-types.md).\n\n## Framing and process I/O\n\nThe stream uses strict JSONL framing. Each record is one JSON object terminated by LF (`\\n`). Split records only on LF and strip an optional preceding carriage return. Unicode line and paragraph separators are valid inside JSON strings and are not record boundaries.\n\nNode.js `readline` is not suitable for this stream because it also recognizes those Unicode separators. Use a byte or UTF-8 stream decoder and split on LF.\n\nRead stdout continuously. A reader that stops consuming records can stall Pi when the pipe buffer fills. Stdout is reserved for JSONL; diagnostics and application logging go to stderr.\n\n## Session header\n\nThe first JSON-mode record is the current [session header](session-format.md#sessionheader):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path\"}\n```\n\nRPC mode does not emit this record. Use [`get_state`](rpc-commands.md#get_state) for its current session ID and file.\n\n## Event sequence\n\nA basic run produces records like these:\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],\"stopReason\":\"pending\",\"...\":\"...\"}}\n{\"type\":\"message_update\",\"usage\":{\"...\":\"...\"},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"}}\n{\"type\":\"turn_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[{\"...\":\"...\"}],\"willRetry\":false}\n{\"type\":\"agent_settled\",\"aborted\":false}\n```\n\n`agent_end` closes one low-level agent run. Automatic retry, overflow recovery, compaction retry, steering, or follow-up work can still continue. `agent_settled` means Pi has no remaining automatic work for that session-level run.\n\n## Agent and turn events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `agent_start` | None | A low-level agent run started. |\n| `agent_end` | `messages`, `willRetry` | That low-level run ended. `messages` contains messages generated by the run. |\n| `agent_settled` | `aborted` | Pi will not continue automatically through retries, compaction recovery, or queued messages. `aborted` is `true` when the run ended because it was aborted. |\n| `turn_start` | None | One assistant turn started. |\n| `turn_end` | `message`, `toolResults` | One assistant response and its resulting tool calls finished. |\n\nA turn is one assistant response plus any tool calls and tool results produced by that response.\n\n## Message events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `message_start` | `message` | A message started. |\n| `message_update` | `usage`, `assistantMessageEvent` | An assistant message emitted a content-block update. |\n| `message_end` | `message` | A message completed. This is the authoritative final message. |\n\n### Reconstruct streaming messages\n\nWire `message_update` records are delta-only. They omit the SDK event's cumulative `message` field and every `assistantMessageEvent.partial` snapshot so stream size remains linear.\n\nThe nested event is one of:\n\n| Type | Fields in addition to `type` | Meaning |\n|---|---|---|\n| `start` | None | The provider stream started; its cumulative `partial` field is removed on the wire. |\n| `text_start` | `contentIndex` | A text block started. |\n| `text_delta` | `contentIndex`, `delta` | Append text to the block. |\n| `text_end` | `contentIndex`, `content` | The text block ended with authoritative content. |\n| `thinking_start` | `contentIndex` | A thinking block started. |\n| `thinking_delta` | `contentIndex`, `delta` | Append thinking text to the block. |\n| `thinking_end` | `contentIndex`, `content` | The thinking block ended with authoritative content. |\n| `toolcall_start` | `contentIndex`, `id`, `toolName` | A tool-call block started. |\n| `toolcall_delta` | `contentIndex`, `delta` | Append serialized argument data. |\n| `toolcall_end` | `contentIndex`, `toolCall` | The tool call ended with the complete `ToolCall`. |\n| `done` | `reason`, `message` | The provider stream completed successfully. |\n| `error` | `reason`, `error` | The provider stream ended with an error or abort message. |\n\nThe normal agent loop translates provider-level `start`, `done`, and `error` into `message_start` and `message_end` session events rather than emitting them as `message_update`. They remain admitted by the exported `JsonAgentSessionEvent` transformation for callers that construct a matching session event.\n\nUse `contentIndex` to identify the content block. Buffer `delta` fields for a live display, but replace reconstructed data with the completed content in `text_end`, `thinking_end`, or `toolcall_end`. Replace the whole partial message with `message_end.message` when it arrives.\n\nThe top-level `usage` is the latest cumulative provider-reported usage for the assistant response. It can remain zero until completion when a provider does not report usage while streaming.\n\n```json\n{\"type\":\"message_update\",\"usage\":{\"input\":100,\"output\":1,\"cacheRead\":0,\"cacheWrite\":0,\"totalTokens\":101,\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0,\"total\":0}},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello \"}}\n```\n\n## Tool execution events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `tool_execution_start` | `toolCallId`, `toolName`, `args` | Tool execution started. |\n| `tool_execution_update` | `toolCallId`, `toolName`, `args`, `partialResult` | The tool reported a partial result. |\n| `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError`, `durationMs` | Tool execution finished. `durationMs` is how long the tool's `execute()` took, measured with a monotonic clock; absent when the tool did not run. |\n\nUse `toolCallId` to correlate the lifecycle. `partialResult` is the latest partial result supplied by the tool. Whether it replaces or extends an earlier update depends on that tool's result contract.\n\n```json\n{\"type\":\"tool_execution_start\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"}}\n{\"type\":\"tool_execution_update\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"},\"partialResult\":{\"content\":[{\"type\":\"text\",\"text\":\"partial output\"}],\"details\":{}}}\n{\"type\":\"tool_execution_end\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"complete output\"}],\"details\":{}},\"isError\":false}\n```\n\n## Queue and state events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `queue_update` | `steering`, `followUp` | The pending steering or follow-up queue changed. Both fields contain the complete current queue. |\n| `entry_appended` | `entry` | An extension appended a custom session entry through `pi.appendEntry()`. |\n| `session_info_changed` | `name` | The session display name changed. An absent `name` means it was cleared. |\n| `thinking_level_changed` | `level` | The active thinking level changed. |\n\nThe `entry` value uses a persisted [session entry type](session-format.md#entry-types).\n\n## Compaction events\n\n`compaction_start` reports why compaction began:\n\n```json\n{\"type\":\"compaction_start\",\"reason\":\"threshold\"}\n```\n\n`reason` is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n`compaction_end` contains the result when compaction succeeds:\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\"...\": \"...\"},\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\nIf compaction was aborted, `result` is absent and `aborted` is true. If it failed, `result` is absent, `aborted` is false, and `errorMessage` describes the failure. Successful overflow recovery sets `willRetry` to true before Pi retries the prompt.\n\nSee [Compaction and Branch Summaries](compaction.md) for result semantics.\n\n## Retry events\n\nAssistant-turn retry emits:\n\n```json\n{\"type\":\"auto_retry_start\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"529 overloaded\"}\n{\"type\":\"auto_retry_end\",\"success\":true,\"attempt\":2}\n```\n\nOn final failure, `auto_retry_end` has `success: false` and a `finalError` string.\n\nCompaction and branch-summary retry emit:\n\n```json\n{\"type\":\"summarization_retry_scheduled\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"terminated\"}\n{\"type\":\"summarization_retry_attempt_start\",\"source\":\"compaction\",\"reason\":\"threshold\"}\n{\"type\":\"summarization_retry_finished\"}\n```\n\nFor a branch summary, `source` is `\"branchSummary\"` and `reason` is absent. The `reason` on a compaction retry is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n## RPC-only events\n\nA direct RPC [`bash`](rpc-commands.md#bash) command emits one `bash_execution_update` for each output chunk. Its optional `id` matches the command ID. The final command response can contain truncated output, but these events stream all output:\n\n```json\n{\"type\":\"bash_execution_update\",\"id\":\"req-1\",\"delta\":\"total 48\\n\"}\n```\n\nRPC also adds `extension_error` when an extension handler throws:\n\n```json\n{\"type\":\"extension_error\",\"extensionPath\":\"/path/to/extension.ts\",\"event\":\"tool_call\",\"error\":\"Error message\"}\n```\n\nExtension UI records are a separate RPC subprotocol, not `AgentSessionEvent` values. See [RPC Extension UI](rpc-extension-ui.md).\n\n## TypeScript types\n\nThe SDK's `AgentSessionEvent` contains cumulative streaming snapshots for in-process consumers. JSON and RPC transform only `message_update`:\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAssistantMessageEvent<T> = T extends { type: \"toolcall_start\"; partial: unknown }\n  ? WithoutPartial<T> & { id: string; toolName: string }\n  : WithoutPartial<T>;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      usage: Usage;\n      assistantMessageEvent: JsonAssistantMessageEvent<AssistantMessageEvent>;\n    };\n```\n\nUse the exported `JsonAgentSessionEvent` type from `@earendil-works/pi-coding-agent`. Its implementation is in [`json-event.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/json-event.ts).\n\n## Example\n\nPrint completed messages from a one-shot run:\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```\n","isInternal":false,"tokens":2689,"sizeBytes":11106},{"name":"keybindings.md","path":"packages/coding-agent/docs/keybindings.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/keybindings.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Keybindings Reference\n\nPi exposes named actions, such as `app.session.new`, that can be assigned keybindings. You can change default assignments or bind unassigned actions in Pi's [user configuration](configuration.md#agent-directory).\n\nRun `/hotkeys` to see the active shortcuts for the main editor and application.\n\n## Assign keybindings\n\nCreate `<agent-dir>/keybindings.json`. The agent directory defaults to `~/.pi/agent` and is described in [Agent directory](configuration.md#agent-directory).\n\nMap each action identifier to one key or a list of keys:\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/keybindings.schema.json\",\n  \"app.session.new\": \"ctrl+shift+n\",\n  \"app.session.tree\": [\"ctrl+shift+t\", \"alt+shift+t\"]\n}\n```\n\nA configured value replaces the default for that action. Use an empty list to disable an action's keybindings:\n\n```json\n{\n  \"tui.altScreen.pageUp\": []\n}\n```\n\nAfter editing the file, run `/reload` to apply the changes to the active session.\n\n## Key syntax\n\nWrite a key as `modifier+key`. Modifiers are `ctrl`, `shift`, `alt`, and `super`. You can combine modifiers. Valid keys are:\n\n- **Letters:** `a-z`\n- **Digits:** `0-9`\n- **Special:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Function:** `f1`-`f12`\n- **Symbols:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nExamples: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, and `ctrl+1`.\n\n`super` bindings require a terminal that reports the modifier separately, typically through the Kitty keyboard protocol. They may not work in terminals without that support.\n\n## Actions\n\n### Terminal UI\n\n#### Cursor movement\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.cursorUp` | `up` | Move cursor up, browsing older history at the top |\n| `tui.editor.cursorDown` | `down` | Move cursor down, browsing newer history at the bottom |\n| `tui.editor.historyPrevious` | None | Select the previous prompt history entry |\n| `tui.editor.historyNext` | None | Select the next prompt history entry |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Move cursor left |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Move cursor right |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Move cursor word left |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Move cursor word right |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+a` | Move to line start |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+e` | Move to line end |\n| `tui.editor.jumpForward` | `ctrl+]` | Jump forward to character |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Jump backward to character |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Scroll up by page |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Scroll down by page |\n\nThe dedicated history actions browse prompt history regardless of cursor position and take precedence over application actions using the same key.\n\n#### Text editing\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.deleteCharBackward` | `backspace` | Delete character backward |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Delete character forward |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Delete word backward |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Delete word forward |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Delete to line start |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Delete to line end |\n| `tui.editor.yank` | `ctrl+y` | Paste most recently deleted text |\n| `tui.editor.yankPop` | `alt+y` | Cycle through deleted text after yank |\n| `tui.editor.undo` | `ctrl+-` (`ctrl+z` on Windows; `alt+z` on WSL) | Undo last edit |\n\n#### Input and selection\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insert new line |\n| `tui.input.submit` | `enter` | Submit input |\n| `tui.input.tab` | `tab` | Tab or autocomplete |\n| `tui.input.copy` | `ctrl+c` | Copy selection |\n| `tui.select.up` | `up` | Move selection up |\n| `tui.select.down` | `down` | Move selection down |\n| `tui.select.pageUp` | `pageUp` | Page up in list |\n| `tui.select.pageDown` | `pageDown` | Page down in list |\n| `tui.select.confirm` | `enter` | Confirm selection |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Cancel selection |\n\n#### Fullscreen\n\nIn fullscreen mode, these actions control the transcript and take precedence over editor actions using the same key.\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.altScreen.pageUp` | `pageUp` | Scroll the transcript up by one page |\n| `tui.altScreen.pageDown` | `pageDown` | Scroll the transcript down by one page |\n| `tui.altScreen.halfPageUp` | None | Scroll the transcript up by half a page |\n| `tui.altScreen.halfPageDown` | None | Scroll the transcript down by half a page |\n| `tui.altScreen.lineUp` | None | Scroll the transcript up by one line |\n| `tui.altScreen.lineDown` | None | Scroll the transcript down by one line |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up`, `ctrl+up` (`ctrl+up` only on Windows and WSL) | Jump to the previous marked message |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down`, `ctrl+down` (`ctrl+down` only on Windows and WSL) | Jump to the next marked message |\n| `tui.altScreen.search` | `ctrl+shift+f` (`ctrl+f` on Windows and WSL) | Search the rendered transcript |\n| `tui.altScreen.searchNext` | `enter`, `ctrl+g` | Select the next search match while searching |\n| `tui.altScreen.searchPrevious` | `shift+enter`, `ctrl+shift+g` | Select the previous search match while searching |\n| `tui.altScreen.searchClose` | `escape` | Close transcript search |\n| `tui.altScreen.top` | `ctrl+home` | Scroll to the beginning of the transcript |\n| `tui.altScreen.bottom` | `ctrl+end` | Scroll to the transcript end and follow new output |\n\n### Application\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Cancel / abort |\n| `app.clear` | `ctrl+c` | Clear editor (first) / exit (second) |\n| `app.exit` | `ctrl+d` | Exit (when editor empty) |\n| `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background |\n| `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard |\n\nOn native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior.\n\n### Sessions\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.session.new` | None | Start a new session (`/new`) |\n| `app.session.tree` | None | Open session tree navigator (`/tree`) |\n| `app.session.fork` | None | Fork current session (`/fork`) |\n| `app.session.resume` | None | Open session resume picker (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Toggle path display |\n| `app.session.toggleSort` | `ctrl+s` | Toggle sort mode |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Toggle named-only filter |\n| `app.session.rename` | `ctrl+r` | Rename session |\n| `app.session.delete` | `ctrl+d` | Delete session |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Delete session when query is empty |\n\n### Models and Thinking\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Open model selector |\n| `app.model.cycleForward` | `ctrl+p` | Cycle to next model |\n| `app.model.cycleBackward` | `shift+ctrl+p` (`alt+p` on Windows and WSL) | Cycle to previous model |\n| `app.models.save` | `ctrl+s` | Save the selected default model or scoped model configuration to settings |\n| `app.thinking.cycle` | `shift+tab` | Cycle thinking level |\n| `app.thinking.save` | `ctrl+s` | Save current thinking level to settings |\n| `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks |\n\n### Display and Message Queue\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Collapse or expand tool output |\n| `app.message.copy` | `ctrl+x` | Copy the selected message in `/tree`; in fullscreen mode, copy the active selection when `fullscreenCopyOnSelect` is `false`; otherwise copy the last assistant message. On OAuth sign-in screens, copy the sign-in URL |\n| `app.message.followUp` | `alt+enter` (`ctrl+q` on Windows and WSL) | Queue follow-up message |\n| `app.message.dequeue` | `alt+up` (`alt+q` on Windows and WSL) | Restore queued messages to editor |\n\n### Tree Navigation\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Fold current branch segment, or jump to the previous segment start |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Unfold current branch segment, or jump to the next segment start or branch end |\n| `app.tree.editLabel` | `shift+l` | Edit the label on the selected tree node |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Toggle label timestamps in the tree |\n| `app.tree.filter.default` | `ctrl+d` | Set tree filter to default view |\n| `app.tree.filter.noTools` | `ctrl+t` | Toggle tree filter that hides tool results |\n| `app.tree.filter.userOnly` | `ctrl+u` | Toggle tree filter that shows only user messages |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Toggle tree filter that shows only labeled entries |\n| `app.tree.filter.all` | `ctrl+a` | Toggle tree filter that shows all entries |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Cycle tree filter forward |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Cycle tree filter backward |\n\n### Scoped Models Selector\n\nUsed inside the scoped models selector (opened via `/scoped-models`).\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.models.enableAll` | `ctrl+a` | Enable all models (or all matching the current search) |\n| `app.models.clearAll` | `ctrl+x` | Clear all models (or all matching the current search) |\n| `app.models.toggleProvider` | `ctrl+p` | Toggle all models for the current provider |\n| `app.models.reorderUp` | `alt+up` | Move the selected model up in the cycle order |\n| `app.models.reorderDown` | `alt+down` | Move the selected model down in the cycle order |\n","isInternal":false,"tokens":2912,"sizeBytes":10658},{"name":"llama-cpp.md","path":"packages/coding-agent/docs/llama-cpp.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/llama-cpp.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Local Models with llama.cpp\n\nPi supports the [llama.cpp](https://github.com/ggml-org/llama.cpp) router server. The router discovers multiple GGUF models and loads or unloads them on demand.\n\nUse a current llama.cpp build with router support. Follow the [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) or install a [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) for your platform.\n\n## Start the router\n\nStart `llama-server` without `--model` or `-m`. Passing a model starts single-model mode instead of router mode.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\nImportant options:\n\n- `--models-dir ~/models` discovers local GGUF files.\n- `--no-models-autoload` keeps loading explicit through `/llama`.\n- `--jinja` enables compatible chat templates and tool calling.\n- `-ngl 999` offloads as many layers as possible to the GPU.\n- `-c 32768` sets the context window for each loaded model. Omit it to use the model's native context, which may require substantially more memory.\n\nA single-file model can sit directly in the model directory. Put multimodal and multi-shard models in separate subdirectories:\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\nRestart the router after manually adding files. For per-model context sizes and other options, use [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets). For settings tuned to a specific NVIDIA GPU and model, see [Run Pi with NVIDIA RTX and llama.cpp](nvidia-rtx-llama-cpp.md).\n\n## Configure Pi\n\nStart Pi and configure the provider:\n\n```text\n/login llama.cpp\n```\n\nEnter the router URL and optional API key. The default URL is `http://127.0.0.1:8080`.\n\nIf you start the router with `--no-models-autoload`, `/login llama.cpp` only stores the connection. Run `/llama` to load a model, then `/model` to select the loaded model for the current session.\n\nEnvironment variables can configure the same values without `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nIf the server uses an API key, start `llama-server` with the matching `--api-key` value. Keep `--host 127.0.0.1` for local-only access.\n\n## Manage models\n\nRun:\n\n```text\n/llama\n```\n\n- Select an unloaded model to load it.\n- Select a loaded model to unload it.\n- Select **Download model…**, search Hugging Face, then choose a repository and quantization. Exact `owner/repository[:quant]` values also work.\n- Press Escape during a load or download to confirm cancellation.\n\nHugging Face search uses `HF_TOKEN` when set, then checks `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token`, and `~/.cache/huggingface/token`. Search also works without authentication, subject to lower rate limits. Pi warns before downloading gated repositories and links to their access page. The llama.cpp server performs the download, so its process must also have `HF_TOKEN` when the selected repository requires access.\n\nIf other models are loaded, Pi asks whether to unload them first or keep them loaded. Pi does not silently unload models and never deletes model files. The router may be shared with other clients, so `/llama` always displays the router's current state.\n\nLoaded and sleeping models appear in `/model`. Sleeping models wake automatically when selected. With router autoload enabled, unloaded preset models also appear and load when selected. With `--no-models-autoload`, load a model through `/llama` before selecting it.\n\nIf the router disconnects, `/llama` shows **Retry** and **Close**. Retry reconnects and refreshes model state without replaying the interrupted operation.\n\n## Classification\n\nClassifier models answer typed `choice`, `bool`, and `score` questions about JSON state, like TypeSafe's Jev models. The model reaches them from [`codemode`](cli.md#enable-codemode) scripts, and extensions through `ctx.modelRegistry.classify()`; see [Classifier models](models.md#use-classifier-models). Pi lists llama.cpp models as classifiers in two ways:\n\n- **Decision models** such as [Julia-1, Laya, Kev, lev, and OpenJev](https://huggingface.co/collections/ggml-org/decision-models-6abf80cca3c83f127060a769) answer natively through llama.cpp's `/v1/systemone` endpoint. They appear only as classifiers, with the `typesafe-system-one` API, and not in `/model`.\n- **Chat models** are also listed as classifiers with the same ID and the `llama-cpp-classify` API, which reads answers from next-token probabilities as described below.\n\nllama.cpp 0.6.0 and later report decision models in the router's model list: their `architecture.output_modalities` contains `decisions`. The router reads this from the GGUF metadata without loading the model, so Pi recognizes unloaded and sleeping decision models too. Older llama.cpp builds do not report it, and Pi lists their decision models as chat models.\n\n### Chat models as classifiers\n\nThe model does not generate an answer. Each question becomes one chat prompt: the state, every question of the request, the state again, and then the question with its answers under single-token labels. Labels are letters for a choice (up to 62 options), `Yes`/`No` for a bool, and digits for a score (up to 10 levels). The second copy of the state is read with the questions in view, which improved accuracy on JevBench with small models. Pi reads the probabilities of the labels as the next token and normalizes them. A choice returns every option's probability and a confidence of `(n * peak - 1) / (n - 1)`; a score returns the expected level.\n\n- Raw label probabilities are usually overconfident. The per-request `temperature` option divides the label logits before normalizing; values above 1 soften the distribution. It changes no answer.\n- Questions run one after another. Everything before the final question is the same for all questions of a request, so the server's prompt cache evaluates it once. The state appears twice, so it needs twice its size in context.\n- Small models may follow instructions written inside the state. The prompt tells the model to judge the state as data, but that is not a guarantee.\n- Hybrid models such as Qwen3.5 cannot rewind a partially cached prompt without context checkpoints. If each question reprocesses the whole state, start the router with `--ctx-checkpoints 32 --checkpoint-min-step 0`.\n\n## Troubleshooting\n\nCheck that the router is reachable:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **No models in `/llama`:** Check `--models-dir`, the directory layout, and restart the router.\n- **Model missing from `/model` with `--no-models-autoload`:** Load it with `/llama` first.\n- **Load fails or uses too much memory:** Lower `-c` or unload another model.\n- **Server is not in router mode:** Start it without `--model`, `-m`, or `-hf`.\n\nTo remove the `llama.cpp` provider and `/llama`, disable `llama.cpp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:llama.cpp\"]` in [settings](settings.md#resources).\n","isInternal":false,"tokens":1845,"sizeBytes":7459},{"name":"mcp.md","path":"packages/coding-agent/docs/mcp.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/mcp.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# MCP Servers\n\nPi connects to [Model Context Protocol](https://modelcontextprotocol.io) servers over stdio or streamable HTTP and makes their tools and resources available to the model.\n\n## Quick setup\n\nAdd a local stdio server, check the connection, then start Pi:\n\n```bash\npi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .\npi mcp list\npi\n```\n\nFor a remote server:\n\n```bash\npi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN\npi mcp list\n```\n\nThese commands add user-level servers by default. Add `--local` or `-l` to write the project configuration instead:\n\n```bash\npi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp\n```\n\nUse `/mcp` inside an interactive session to inspect connections, sign in, reconnect, change exposure, or enable and disable servers. Run `/reload` after adding, removing, or changing a server outside the session.\n\n## Configure servers\n\nPi reads user-level servers from `~/.pi/agent/mcp.json` and project servers from `.pi/mcp.json`. Project configuration is read only after [project trust](security.md#understand-project-trust) is granted. A project entry replaces a user-level entry with the same name.\n\nA project entry without `command`, `url`, or `type` overrides only `enabled`, `exposure`, and `toolExposure` of the user-level server with the same name and keeps the rest, including `env`, `headers`, and `auth`. For example, this turns off a user-level server in one project:\n\n```json\n{\n  \"mcpServers\": {\n    \"internal-tools\": { \"enabled\": false }\n  }\n}\n```\n\nThe format matches other MCP clients:\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \".\"]\n    },\n    \"docs\": {\n      \"url\": \"https://example.com/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer ${DOCS_TOKEN}\" },\n      \"description\": \"Search and read the product documentation\"\n    }\n  }\n}\n```\n\nStdio servers use `command`, `args`, `env`, and `cwd`. Relative `cwd` values resolve against the session directory. A leading `~/` in `command`, an argument, or `cwd` names the home directory.\n\nHTTP servers use `url`, `headers`, and `oauth` (see [Authenticate with OAuth](#authenticate-with-oauth)). The legacy SSE transport is not supported.\n\nBoth server types support:\n\n- `timeout`: per-request timeout in seconds (default 60). Progress notifications reset it.\n- `enabled: false`: keep the entry without connecting to it.\n- `exposure` and `toolExposure`: control how tools reach the model (see [Control tool exposure](#control-tool-exposure)).\n- `description`: what the server offers, in a sentence. It lists the server in the system prompt (see [Control tool exposure](#control-tool-exposure)), tool search ranks the server's tools by it, and codemode's `describeNamespace()` returns it. Without it, the first line of the server instructions is used once the server connects.\n\nKeep personal servers and servers with credentials in the user-level file. Use the project file only for servers the project requires, and only in trusted projects.\n\n### Configuration rules\n\n- Server names may contain only letters, digits, `_`, and `-`. Tools are named `mcp__<server>__<tool>`, with every character other than letters, digits, and `_` replaced by `_`; tools of a server whose names then collide all get a hash suffix. Server names that differ only in `-` and `_` count as the same server: a second one is rejected, and a `mcp.json` server overrides a registered one.\n- `type` is optional. A `command` selects stdio and a `url` selects streamable HTTP. When present, `type` must be `stdio`, `http`, or `streamable-http`.\n- `sse` is rejected. Servers that document an SSE endpoint often also provide streamable HTTP, commonly at `/mcp` instead of `/sse`.\n- `command` is one executable and `args` contains its arguments. It is not a shell command string.\n- `env` and `headers` values can use environment variables such as `${GITHUB_TOKEN}`. They can also run a command with `!command`, but the command must make up the whole value, for example `\"Authorization\": \"!echo Bearer $(gh auth token)\"`.\n- Invalid entries are reported and skipped without preventing other servers from connecting.\n\n`pi mcp add` and `pi mcp remove` cover common changes from a shell. See [MCP commands](cli.md#mcp-commands) for their options.\n\n### Inspect or change a server\n\n`/mcp` lists configured servers with their state, tool count, exposure, and configuration source. Servers that need attention appear first. Select a server to inspect its tools and connection details, reconnect, sign in or out, change exposure, or enable and disable it.\n\nExposure and enabled-state changes are saved to the file that defines the server without replacing unrelated content. In a trusted project, \"Enable in this project\" and \"Disable in this project\" add a project override for a user-level server; later changes to that server are saved to the override. Disabled servers remain listed. Outside the interactive TUI, `/mcp` prints server status; `/mcp login <server>`, `/mcp logout <server>`, and `/mcp reconnect <server>` perform those actions directly.\n\nShell commands work without a session: `pi mcp add`, `pi mcp remove`, `pi mcp list`, `pi mcp login`, and `pi mcp logout`. Shell commands do not load extensions.\n\n### Diagnose connection problems\n\nRun `pi mcp list` to connect to every enabled server and print its state, tools, and errors. It exits with status 1 when an entry is invalid or an enabled server is not connected. `/mcp` shows the full connection error and the tail of stderr from a failed stdio server.\n\nPi reports configuration errors, failed connections, and required sign-ins once after startup. Server logging notifications are appended to `~/.pi/agent/mcp.log` as `<time> [<server>] <level> <logger>: <message>`. The file moves to `mcp.log.1` after it grows past 5 MB.\n\nPi connects every enabled server in the background when a session starts. A server's tools appear once it connects; the `codemode` description does not list them, so it does not change when servers connect. The first prompt waits up to 10 seconds only for servers with `direct` tools, which must be declared in its request. Other servers are waited for when they are needed: a codemode script waits for the servers it names (`mcp__<server>`) and, when it calls `searchTools()` or reads `ALL_TOOLS`, for all of them; `tool_search` and the resource tools also wait for all of them. HTTP network errors and transient statuses (408, 429, and 5xx) are retried twice. A dropped connection is shown as disconnected and reconnects on the next call. When a server announces a changed tool list, new tools are added and withdrawn tools become unreachable.\n\nStopping a stdio server closes its stdin, sends SIGTERM, then sends SIGKILL to its process group. This also stops servers launched through wrappers such as `npx` or `uvx`.\n\n## Migrate configuration from another client\n\nMove the converted entry under `mcpServers` in `mcp.json`, then run `pi mcp list` to validate it.\n\n| Client | Conversion |\n|---|---|\n| Claude Desktop, Claude Code, or Cursor | Copy the existing `mcpServers` entry. |\n| VS Code | Move an entry from the top-level `servers` object and replace `${input:...}` prompts with `${NAME}` environment variables. |\n| Codex | Convert `[mcp_servers.<name>]` TOML fields such as `command`, `args`, `env`, and `url` to JSON. |\n| OpenCode | Convert `\"type\": \"local\"` to a stdio entry, split its `command` array into `command` and `args`, rename `environment` to `env`, and replace `{env:NAME}` with `${NAME}`. Convert `\"type\": \"remote\"` to a URL entry. |\n\n## Authenticate with OAuth\n\nRemote servers that use OAuth, such as Sentry, need no credentials in `mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"sentry\": { \"url\": \"https://mcp.sentry.dev/mcp\" }\n  }\n}\n```\n\nWhen the server rejects an unauthenticated connection, `/mcp` shows that it needs sign-in. Select \"Sign in\", run `/mcp login sentry`, or run `pi mcp login sentry`. Pi opens the authorization page and waits for approval. If the browser runs on another machine, such as over SSH, paste its redirected URL into the sign-in screen. A running session uses the new credentials on its next turn.\n\nPi registers itself with the authorization server, stores tokens in `~/.pi/agent/mcp-auth.json`, and refreshes access tokens when they expire or the server rejects them. If a server later requests additional scope, Pi asks for sign-in again. Signing out deletes the stored credentials.\n\nCredentials belong to a server name and URL. Servers with the same URL under different names, such as one per account, sign in separately; servers with the same name and URL in different `mcp.json` files share one sign-in.\n\nOAuth applies to HTTP servers without an `Authorization` header. For a server that does not support dynamic client registration, configure a registered client:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"clientId\": \"my-client\", \"clientSecret\": \"${EXAMPLE_SECRET}\", \"callbackPort\": 8765 }\n    }\n  }\n}\n```\n\nThe redirect URI must match the registered URI. `callbackPort` uses `http://127.0.0.1:<port>/callback`. To use another URI, set `callbackUrl`; it must use HTTP on `localhost`, `127.0.0.1`, or `[::1]`. Pi sends it exactly as written. When `callbackUrl` omits a port, Pi uses `callbackPort` or a free port and adds it to the URI, as allowed for loopback redirects by RFC 8252. `clientSecret` is optional and can use an environment variable or command.\n\nSet `scope` to a space-separated list for servers that do not advertise their required scopes. Otherwise, Pi requests the advertised scopes. Later scope requests are added to the configured value.\n\nPi registers as `pi`. Some servers only accept registrations from known clients. Set `clientName` to send another name:\n\n```json\n{\n  \"mcpServers\": {\n    \"figma\": { \"url\": \"https://mcp.figma.com/mcp\", \"oauth\": { \"clientName\": \"Claude Code\" } }\n  }\n}\n```\n\nThe name is only sent when Pi registers a client. To register again under a new name, sign out first.\n\nSome authorization servers allow clients by their Client ID Metadata Document URL instead of registering them. Set `clientRegistration` to `cimd` to identify as Pi's document on pi.dev instead of registering:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": { \"url\": \"https://mcp.example.com/mcp\", \"oauth\": { \"clientRegistration\": \"cimd\" } }\n  }\n}\n```\n\nThe client ID is `https://pi.dev/oauth/client.json` with the redirect URI `http://127.0.0.1:<port>/callback`. If the authorization server does not send the `iss` parameter in authorization responses (RFC 9207), Pi uses a document and redirect path specific to the MCP server instead: `https://pi.dev/oauth/<id>/client.json` with `http://127.0.0.1:<port>/callback/<id>`. The authorization server must advertise Client ID Metadata Document support and public clients, or sign-in fails. `cimd` cannot be combined with `clientId` or `clientName`, and a `callbackUrl` must use `localhost` or `127.0.0.1` with the path `/callback`.\n\nPi finds the authorization server through the server's protected resource metadata (RFC 9728) and checks that the authorization server's metadata names the expected issuer (RFC 8414). Some servers advertise the wrong authorization server or none, so sign-in opens a page that does not exist. Set `authServerMetadataUrl` to the metadata document of the right authorization server:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"authServerMetadataUrl\": \"https://example.okta.com/.well-known/openid-configuration\" }\n    }\n  }\n}\n```\n\nPi uses that document instead of discovery and trusts it as configured, so only point it at a document you trust. The URL must use HTTPS, except on `localhost`, `127.0.0.1`, or `[::1]`.\n\n## Control tool exposure\n\nEach server tool is registered as `mcp__<server>__<tool>`. The server's `exposure` determines how the model reaches it:\n\n| Exposure | Behavior | Typical use |\n|---|---|---|\n| `codemode` (default) | Callable from [`codemode`](cli.md#tools) scripts, but neither declared to the model nor listed in the codemode description. Scripts find tools with `searchTools()`, `describeTool()`, or `ALL_TOOLS`. | General MCP servers, especially when scripts should combine or filter calls. |\n| `deferred` | Not declared until [`tool_search`](cli.md#tools) loads a match for the next model call. | Large servers whose tools should be called directly after discovery. |\n| `direct` | Declared to the model like a built-in tool and also callable from codemode. | Small, frequently used tool sets. |\n| `hidden` | Registered but unreachable. | Servers or tools that should remain unavailable. |\n\n`codemode-deferred` is accepted as an alias for `codemode`.\n\nServers with `codemode` or `deferred` tools are listed in the `mcp_servers` section of the system prompt, with how their tools are reached and one line from the configured `description` or, once connected, from the server instructions. Pi updates the section when a prompt starts, after waiting for servers with `direct` tools. When it changed, for example because a server connected and its summary became available, Pi appends the new section to the conversation instead of changing tool declarations, so earlier messages stay cached. `describeNamespace()` and the `namespace` option of `searchTools()` accept `mcp__dev-radius`, `mcp__dev_radius`, `dev-radius`, or `dev_radius`.\n\nPi activates `codemode` when a server with `codemode` exposure connects. It activates `tool_search` for a server with `deferred` exposure. To make the model see a tool without searching, give it `direct` exposure with `toolExposure`.\n\n`toolExposure` overrides the server exposure for individual tools. Keys are exact server tool names or patterns where `*` matches any characters. Exact names win over patterns; among patterns, the first match wins. A server with `hidden` exposure can expose only selected tools:\n\n```json\n{\n  \"mcpServers\": {\n    \"github\": {\n      \"url\": \"https://api.githubcopilot.com/mcp/\",\n      \"exposure\": \"deferred\",\n      \"toolExposure\": {\n        \"search_code\": \"direct\",\n        \"get_*\": \"codemode\",\n        \"delete_*\": \"hidden\"\n      }\n    }\n  }\n}\n```\n\n`pi mcp list` marks tools whose exposure differs from their server. The Tools view in `/mcp` also shows the effective exposure.\n\nTools with `codemode` or `deferred` exposure can be reached through either indirect mechanism: codemode scripts can call them, and `tool_search` can load them. Codemode calls do not depend on the active tool set, so they remain available after `/tree`, resume, and fork. Tools loaded by `tool_search` are recorded in the transcript and remain declared on that branch.\n\n`--tools` does not remove MCP tools unless one of its entries starts with `mcp__`; `pi --tools read,codemode,'mcp__radius__*'` keeps only the tools of `radius`. `--exclude-tools` accepts the same patterns, and `--no-mcp` disables MCP for one run (see [Tools](cli.md#mcp-tools)).\n\nTo keep `codemode` active without MCP servers, add `\"defaultTools\": [\"+codemode\"]` to [settings](settings.md#tools). To prevent automatic codemode activation, set `\"autoEnableCodemode\": false` beside `mcpServers`. A project value overrides the user-level value. Pi warns once when neither `codemode` nor `tool_search` is active and non-direct tools cannot be called.\n\nText results over 20 KB reach the model with their middle removed around a `…N chars truncated…` marker. The full text is saved to a temporary file named in the result. Codemode scripts receive the complete result and can reduce it before returning output to the model.\n\nCodemode scripts receive the complete MCP `CallToolResult`, including `content`, `structuredContent`, and `isError`. A result with `isError` resolves inside scripts but is reported as an error for direct calls. `image(result.content[0])` forwards an image block. Server instructions are not part of any tool description; scripts read them with `describeNamespace(\"mcp__<server>\")`, which also returns the server's tool names.\n\n## Use resources\n\nWhen a connected server offers [resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources), Pi adds the resource tools used by Codex and OpenCode:\n\n- `list_mcp_resources` lists resources as JSON: `{ server?, resources: [{ server, uri, name, ... }], nextCursor? }`. With `server`, it lists one page; `cursor` continues with the next page. Without `server`, it lists every resource from every server.\n- `list_mcp_resource_templates` lists URI templates for resources the servers do not list directly.\n- `read_mcp_resource` reads a resource by `server` and `uri`. Text reaches the model as text and images as images. Other binary resources are saved to temporary files, and the model receives the path. Scripts receive `{ server, uri, contents }`.\n\nThese tools reach every enabled, non-hidden server with resources. Their exposure is the widest exposure among those servers: `direct`, then `codemode` or `deferred`. Resource links in tool results identify `read_mcp_resource` and the server.\n\nResources for MCP Apps, identified by `ui://` URIs or `text/html;profile=mcp-app`, are omitted because Pi does not render them. Resource icons are also omitted.\n\nReading and listing resources is retried once after a transient HTTP error (408, 429, or 5xx). Tool calls are not retried because the server may already have performed them.\n\n## Permissions\n\nEvery MCP call passes through Pi's tool pipeline. Extension `tool_call` and `tool_result` handlers, including permission gates, therefore apply to MCP tools. Calls made from codemode scripts carry the codemode call ID as `parentToolCallId`.\n\n`pi.getAllTools()` reports the annotations declared by each server: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. Permission extensions can use these hints to decide which calls require confirmation (see [Tool exposure](extensions.md#tool-exposure)). Resource tools are marked read-only.\n\n## Extensions and SDK\n\n### Add servers from extensions\n\nExtensions can add servers for the current session with `pi.registerMcpServer(name, config)`, using the same shape as an `mcpServers` entry (see [MCP servers in extensions](extensions.md#mcp-servers)). Registered servers connect like configured servers and appear in `/mcp` with the extension as their source.\n\nChanges to enabled state or exposure apply only to the current session. A file-configured server with the same name takes precedence, and `/mcp` lists the overridden registration. `pi mcp` shell commands do not load extensions and only see file-configured servers.\n\n### Replace the built-in MCP support\n\nAn installed extension that registers `/mcp`, such as `pi-mcp-adapter`, replaces the built-in MCP support for sessions. Pi then does not read `mcp.json` or connect its servers in a session, and `/mcp` belongs to the extension. Remove the extension to restore the built-in behavior. To disable built-in MCP support without a replacement, disable `mcp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:mcp\"]` in [settings](settings.md#resources). `--no-mcp` disables it for one run.\n\nAn extension that registers `codemode` or `tool_search` similarly replaces the built-in tool with that name. Pi activates the replacement for MCP servers and waits for the servers its calls need only when it declares [`reachesTools`](extensions.md#tool-exposure). Shell-level `pi mcp` commands always use the built-in implementation.\n\n### Use MCP from the SDK\n\nSDK sessions do not load built-in extensions. Add the MCP extension, the codemode extension for `codemode` servers, and the tool-search extension for `deferred` servers to the resource loader. See [Codemode and MCP](sdk.md#codemode-mcp).\n","isInternal":false,"tokens":4680,"sizeBytes":19870},{"name":"message-types.md","path":"packages/coding-agent/docs/message-types.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/message-types.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Message Types\n\nPi uses `AgentMessage` values in SDK state, lifecycle events, RPC responses, and persisted session message entries. This page defines those shared messages and their content blocks.\n\nMessage timestamps are Unix timestamps in milliseconds. They are different from the ISO 8601 timestamps on [session entries](session-format.md#entry-base).\n\nSource definitions:\n\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/ai/src/types.ts) defines provider-facing messages and content blocks.\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts) defines the extensible `AgentMessage` union.\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/messages.ts) adds coding-agent message roles.\n\n## Content blocks\n\n### TextContent\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n  textSignature?: string;\n}\n```\n\n`textSignature` contains provider-specific message metadata. Treat it as opaque.\n\n### ImageContent\n\n```typescript\ninterface ImageContent {\n  type: \"image\";\n  data: string;\n  mimeType: string;\n}\n```\n\n`data` is base64-encoded image data. `mimeType` identifies its media type, such as `image/png` or `image/jpeg`.\n\n### ThinkingContent\n\n```typescript\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n  thinkingSignature?: string;\n  redacted?: boolean;\n}\n```\n\nThinking signatures contain provider-specific replay data. Treat them as opaque. A redacted block can have no visible thinking text while retaining an encrypted payload in `thinkingSignature`.\n\n### ToolCall\n\n```typescript\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n  thoughtSignature?: string;\n  namespace?: string;\n}\n```\n\n`thoughtSignature` is provider-specific. `namespace` identifies an OpenAI Responses namespace for dynamically loaded or namespaced tools.\n\n## Usage\n\nAssistant messages always contain usage. Tool results can contain usage when the tool performed nested model work.\n\n```typescript\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  cacheWrite1h?: number;\n  reasoning?: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\nWhen present, `reasoning` is already included in `output`; do not add it again. `cacheWrite1h` is the subset of `cacheWrite` written with one-hour retention.\n\n## Base messages\n\n### SystemMessage\n\n```typescript\ninterface SystemMessage {\n  role: \"system\";\n  content: string | TextContent[];\n  sections?: Record<string, string | null>;\n  toolsAdded?: Tool[];\n  toolsRemoved?: ToolReference[];\n  timestamp: number;\n}\n```\n\nThe leading system message declares the initial prompt and tools. Later system messages can append instructions, replace or remove named prompt sections, and add or remove tools. Replaying them in order yields the current state.\n\n### UserMessage\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;\n}\n```\n\n### AssistantMessage\n\n```typescript\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  responseModel?: string;\n  responseId?: string;\n  providerThinkingLevel?: string;\n  thinkingLevel?: ModelThinkingLevel;\n  diagnostics?: AssistantMessageDiagnostic[];\n  usage: Usage;\n  stopReason: \"pending\" | \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\" | \"deferred\";\n  deferred?: DeferredHandle;\n  errorMessage?: string;\n  rawStopReason?: string;\n  endTurn?: boolean;\n  timestamp: number;\n}\n```\n\n`responseModel` records a concrete provider response model when it differs from the requested model. `responseId`, `providerThinkingLevel`, `thinkingLevel`, `diagnostics`, and `rawStopReason` preserve provider or runtime details.\n\n`\"pending\"` is used for a partial assistant message while it streams. The completed message in `message_end` has a terminal stop reason, and Pi does not persist `\"pending\"` assistant messages in session JSONL.\n\nA `\"deferred\"` response has a `DeferredHandle` with the provider data needed to retrieve it:\n\n```typescript\ninterface DeferredHandle {\n  provider: string;\n  modelId: string;\n  api: string;\n  id: string;\n  expiresAt?: number;\n  pollAfterMs?: number;\n  data?: JsonValue;\n}\n```\n\n### ToolResultMessage\n\n```typescript\ninterface ToolResultMessage<TDetails = any> {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: TDetails;\n  usage?: Usage;\n  nestedCalls?: NestedToolCalls;\n  isError: boolean;\n  timestamp: number;\n}\n```\n\n`details` is tool-specific. Optional `usage` reports nested model work performed by the tool and contributes to full-session statistics, but it is not part of the main model-call usage. `nestedCalls` records bounded metadata about calls this tool made to other tools:\n\n```typescript\ninterface NestedToolCalls {\n  calls: NestedToolCallRecord[];\n  complete: boolean;\n}\n\ninterface NestedToolCallRecord {\n  id: string;\n  name: string;\n  arguments?: JsonObject;\n  argumentsBytes?: number;\n  status: \"ok\" | \"error\" | \"unfinished\";\n  durationMs?: number;\n  error?: string;\n}\n```\n\n## Coding-agent messages\n\nThe coding-agent package extends `AgentMessage` with four roles.\n\n### BashExecutionMessage\n\nCreated by direct shell commands, including the RPC [`bash`](rpc-commands.md#bash) command. It is not an LLM tool result.\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;\n  timestamp: number;\n}\n```\n\nUnless `excludeFromContext` is true, Pi converts this message to user-role text before the next model request.\n\n### CustomMessage\n\nCreated when an extension sends a context message.\n\n```typescript\ninterface CustomMessage<T = unknown> {\n  role: \"custom\";\n  customType: string;\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;\n  details?: T;\n  timestamp: number;\n}\n```\n\nPi converts its content to a user message for model requests. `display` controls terminal rendering; `details` is not sent to the model.\n\n### BranchSummaryMessage\n\n```typescript\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string | null;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `branch_summary` entry.\n\n### CompactionSummaryMessage\n\n```typescript\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `compaction` entry.\n\n## AgentMessage union\n\nIn the coding agent, the union is equivalent to:\n\n```typescript\ntype AgentMessage =\n  | SystemMessage\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\nAt the lower-level agent package, `AgentMessage` is `Message | CustomAgentMessages[keyof CustomAgentMessages]`. Applications can add roles through TypeScript declaration merging, so consumers should tolerate unknown custom roles when they accept messages from an augmented host.\n","isInternal":false,"tokens":1715,"sizeBytes":7536},{"name":"models-configuration.md","path":"packages/coding-agent/docs/models-configuration.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/models-configuration.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Models Configuration\n\n<!-- This file is generated from TypeScript source. Do not edit it manually; run npm run generate:references. -->\n\nThis reference lists the fields of `models.json`. See [Configure a compatible endpoint](models.md#configure-a-compatible-endpoint) for examples and [Configuration](configuration.md#agent-directory) for the file location.\n\nAdd `\"$schema\": \"https://pi.dev/schemas/models.schema.json\"` to `models.json` for editor validation and completion.\n\n## Providers\n\nEach key under `providers` is a provider ID. A built-in provider ID changes that provider. Any other ID adds a provider, which needs `baseUrl` and an `api` at provider or model level.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name. |\n| `baseUrl` | string |  | API base URL. Also applies to the provider's built-in models. |\n| `apiKey` | string |  | Literal key, \"$NAME\" environment interpolation, or \"!command\" output. |\n| `api` | string |  | API for the provider's models, such as \"openai-completions\", \"openai-responses\", \"anthropic-messages\", or \"google-generative-ai\". |\n| `oauth` | `\"radius\"` |  | OAuth login flow. Requires baseUrl. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n| `authHeader` | boolean | `false` | Whether to send the resolved API key as Authorization: Bearer. |\n| `models` | [`object[]`](#models) |  |  |\n| `modelOverrides` | [object](#model-overrides) |  |  |\n\n## Models\n\nEach `models` entry adds a model to the provider. An entry whose `id` matches an existing model replaces it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `id` | string |  | Required. Model ID sent to the API. |\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `api` | string |  | Overrides the provider api. |\n| `baseUrl` | string |  | Overrides the provider baseUrl. |\n| `reasoning` | boolean | `false` | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` | `[\"text\"]` |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number | `128000` | Context window in tokens. |\n| `maxTokens` | number | `16384` | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Model overrides\n\nEach `modelOverrides` key is the ID of a built-in or extension model. Unknown IDs are ignored. Object fields, such as `cost`, merge with the model's value field by field; other fields replace it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `reasoning` | boolean |  | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` |  |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Input cost in USD per million tokens. |\n| `cost.output` | number |  | Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number |  | Context window in tokens. |\n| `maxTokens` | number |  | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Input limits\n\nSee [Describe model input and caching](models.md#describe-model-input-and-caching).\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `maxRequestBytes` | integer |  | Maximum serialized provider request size in bytes. |\n| `images` | object |  |  |\n| `images.resize` | object |  | Cache-safe resize profile applied before a new image enters conversation history. |\n| `images.resize.maxWidth` | integer |  |  |\n| `images.resize.maxHeight` | integer |  |  |\n| `images.resize.maxBytes` | integer |  | Maximum base64-encoded payload size in bytes. |\n| `images.resize.jpegQuality` | integer |  |  |\n| `images.maxPerMessage` | integer |  | Maximum images accepted in one provider message. |\n| `images.maxPerRequest` | integer |  | Maximum images accepted across one provider request. |\n\n## Compatibility\n\n`compat` describes verified differences from an API's standard request and response behavior. Model values override provider values per field. The fields that apply depend on the model's `api`.\n\n### `openai-completions`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStore` | boolean |  | Whether the provider supports the store field. Default: auto-detected from URL. |\n| `supportsDeveloperRole` | boolean |  | Whether the provider supports the developer role instead of system. Default: auto-detected from URL. |\n| `supportsReasoningEffort` | boolean |  | Whether the provider supports reasoning_effort. Default: auto-detected from URL. |\n| `supportsUsageInStreaming` | boolean | `true` | Whether the provider supports stream_options.include_usage for token usage in streaming responses. |\n| `supportsFinishReason` | boolean | `true` | Whether streamed responses include finish_reason. When false, pi infers stop or toolUse when the stream ends. |\n| `maxTokensField` | `\"max_completion_tokens\" \\| \"max_tokens\"` |  | Which field to use for max tokens. Default: auto-detected from URL. |\n| `requiresToolResultName` | boolean |  | Whether tool results require the name field. Default: auto-detected from URL. |\n| `requiresAssistantAfterToolResult` | boolean |  | Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. |\n| `requiresThinkingAsText` | boolean |  | Whether thinking blocks must be converted to text blocks with &lt;thinking> delimiters. Default: auto-detected from URL. |\n| `requiresReasoningContentOnAssistantMessages` | boolean |  | Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. |\n| `thinkingFormat` | `\"openai\" \\| \"openrouter\" \\| \"deepseek\" \\| \"together\" \\| \"baseten\" \\| \"zai\" \\| \"qwen\" \\| \"chat-template\" \\| \"qwen-chat-template\" \\| \"string-thinking\" \\| \"ant-ling\"` |  | Format for reasoning or thinking parameters. When omitted, Pi auto-detects the format from the provider URL. \"openai\" uses reasoning_effort, \"openrouter\" uses reasoning.effort, \"deepseek\" uses thinking.type plus reasoning_effort when supported, \"together\" uses reasoning.enabled plus reasoning_effort when supported, \"baseten\" uses configurable chat_template_args plus reasoning_effort when supported, \"zai\" uses thinking.type, \"qwen\" uses top-level enable_thinking, \"qwen-chat-template\" uses chat_template_kwargs.enable_thinking and preserve_thinking, \"chat-template\" uses configurable chat_template_kwargs, \"string-thinking\" uses top-level thinking, and \"ant-ling\" uses reasoning.effort only when the mapped effort is non-null. |\n| `chatTemplateKwargs` | object |  | Kwargs sent as chat_template_kwargs when thinkingFormat is \"chat-template\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `chatTemplateArgs` | object |  | Arguments sent as chat_template_args when thinkingFormat is \"baseten\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `openRouterRouting` | object |  | OpenRouter provider routing preferences. Controls which upstream providers OpenRouter routes requests to. Sent as the provider field in the OpenRouter API request body. See https://openrouter.ai/docs/guides/routing/provider-selection. |\n| `vercelGatewayRouting` | object |  | Vercel AI Gateway routing preferences. Controls which upstream providers the gateway routes requests to. See https://vercel.com/docs/ai-gateway/models-and-providers/provider-options. |\n| `zaiToolStream` | boolean | `false` | Whether z.ai supports top-level tool_stream for streaming tool call deltas. |\n| `thinkingTokenBudgetField` | `\"thinking_token_budget\" \\| \"thinking_budget\" \\| \"thinking_budget_tokens\"` |  | Top-level request field used to cap reasoning tokens from thinkingBudgets. Reasoning and the answer share max_tokens on these endpoints. \"thinking_token_budget\" is vLLM, \"thinking_budget\" is Qwen, DashScope, or SGLang, and \"thinking_budget_tokens\" is llama.cpp. Off by default and not set on the generated catalog. |\n| `supportsThinkingTokenBudget` | boolean | `false` | Alias for thinkingTokenBudgetField: \"thinking_token_budget\" (vLLM). Prefer thinkingTokenBudgetField. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether the provider supports OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system or developer messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `supportsMidConvoToolAdditions` | boolean | `false` | Whether system messages can introduce additional tools mid-conversation. Requires supportsMidConvoSystemMessages. The generated catalog enables this for capable models. |\n| `supportsStrictMode` | boolean | `false` | Whether the provider supports the strict field in tool definitions. Generated capable models enable it explicitly. |\n| `cacheControlFormat` | `\"anthropic\"` |  | Cache control convention for prompt caching. Anthropic applies cache_control markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send session-affinity data from options.sessionId. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id, x-client-request-id, and x-session-affinity; openai-nosession sends x-client-request-id and x-session-affinity; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean |  | Whether the provider supports long prompt cache retention (prompt_cache_retention: \"24h\" or Anthropic-style cache_control.ttl: \"1h\", depending on format). Default: auto-detected from provider and URL. |\n| `vllmPriority` | number |  | vLLM scheduler priority sent as the top-level priority request field. Lower values are handled earlier and the server default is 0. Only meaningful with --scheduling-policy priority. Off by default and not set on the generated catalog. |\n\n### `openai-responses`\n\nAlso applies to `azure-openai-responses` and `openai-codex-responses`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsDeveloperRole` | boolean | `true` | Whether the provider supports the developer role instead of system. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts developer or system messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id and x-client-request-id; openai-nosession sends x-client-request-id; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports long prompt cache retention. This uses prompt_cache_options.ttl: \"30m\" on GPT-5.6+ and prompt_cache_retention: \"24h\" on earlier models. |\n| `supportsStrictMode` | boolean |  | Whether the provider supports strict JSON-schema function tools. Defaults are API-specific; generated OpenAI models enable it explicitly. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether to emit OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsAdditionalTools` | boolean | `false` | Whether the model supports message-anchored additional_tools input items. |\n| `supportsToolSearch` | boolean | `false` | Whether the model supports client-executed tool search for transcript-anchored additions. |\n| `supportsExplicitPromptCacheMode` | boolean | `false` | Whether the model accepts prompt_cache_options. Older OpenAI models reject the parameter. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether thinking level changes are sent as configuration_update input items instead of changing reasoning.effort, so a change keeps the prompt cache. Only used by the openai-responses API, and ignored with Sign in with ChatGPT on the openai provider. |\n| `supportsMaxOutputTokens` | boolean | `true` | Whether the provider accepts max_output_tokens. Some Codex-protocol gateways reject it. |\n\n### `anthropic-messages`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsEagerToolInputStreaming` | boolean | `true` | Whether the provider accepts per-tool eager_input_streaming. When false, the Anthropic provider omits tools[].eager_input_streaming and sends the legacy fine-grained-tool-streaming-2025-05-14 beta header for tool-enabled requests. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports Anthropic long cache retention through cache_control.ttl. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send x-session-affinity from options.sessionId when caching is enabled. Required for providers like Fireworks that use session affinity for prompt cache routing; requests to the same replica maximize cache hits. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openrouter\"` |  | Session-affinity format. openrouter sends x-session-id; when unset, sends x-session-affinity. |\n| `supportsCacheControlOnTools` | boolean | `true` | Whether the provider supports Anthropic-style cache_control markers on tool definitions. When false, cache_control is omitted from tool parameters. Some Anthropic-compatible providers, such as Fireworks, do not support this field on tools and may reject or ignore it. |\n| `supportsTemperature` | boolean | `true` | Whether the model accepts the Anthropic temperature request field. Claude Opus 4.7+ rejects non-default values. |\n| `forceAdaptiveThinking` | boolean | `false` | Whether to force adaptive thinking (thinking.type: adaptive plus output_config.effort) regardless of model ID. Built-in models that require adaptive thinking set this in generated metadata. Custom Anthropic-compatible providers can set this to true for any model whose upstream requires the adaptive format. Set false to opt out on overridden built-in models. |\n| `allowEmptySignature` | boolean | `false` | Whether to replay empty thinking signatures instead of converting thinking to text. |\n| `supportsStrictTools` | boolean | `false` | Whether the provider supports Anthropic strict tool schemas. Generated Anthropic models enable it explicitly. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether the exact model transport supports effort-only system messages and thinking binding controls. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system-role messages inside the conversation. When false, later system messages are folded into the top-level system prompt. |\n| `supportsMidConvoToolChanges` | boolean | `false` | Whether the exact model accepts mid-conversation tool_addition and tool_removal blocks. Requires supportsMidConvoSystemMessages. |\n| `allowedFallbackModels` | `object[]` |  | Models Anthropic accepts for server-side refusal fallback, with local pricing metadata for returned fallback responses. When absent or empty, callers must omit fallbacks; Anthropic rejects the field for models with no permitted fallback targets. |\n\n### `bedrock-converse-stream`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStrictMode` | boolean | `false` | Whether the model supports Bedrock strict tool schemas. |\n\n### `mistral-conversations`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system messages after the conversation has started. When false, later system messages are folded into the leading system message. |\n\n### `typesafe-system-one`\n\nAlso applies to `cloudflare-workers-ai-system-one`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `imageInput` | `\"assets\" \\| \"images\" \\| \"state-image-urls\"` |  | How the service takes classifier images. \"assets\" sends TypeSafe's assets map (PNG and JPEG) and references the images from the state, as TypeSafe and Jev on Workers AI expect. \"images\" sends Clef's images list of data URLs (PNG, JPEG, and WebP, at most four), as Clef on Workers AI expects. \"state-image-urls\" places image_url parts before the state, as OpenRouter expects; OpenRouter drops assets without an error. Auto-detected from the provider, baseUrl, and model ID. Classifier models read it from the provider-level compat; models and modelOverrides entries apply to chat models only. |\n| `sendImageOptInHeader` | boolean |  | Whether requests with images send the TypeSafe-Danger-Zone-Images: true header, which TypeSafe's own API requires for its experimental image input. Defaults to true for the typesafe-system-one API unless the model is detected as OpenRouter. |\n","isInternal":false,"tokens":4898,"sizeBytes":20716},{"name":"models.md","path":"packages/coding-agent/docs/models.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/models.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Choose a Model\n\nFor a built-in provider, start with `/login`, then choose a model with `/model`. Use custom model configuration only when Pi does not already include the provider or endpoint you need.\n\n## Choose a connection\n\n| What you have | Recommended setup |\n|---|---|\n| A supported subscription | Sign in through `/login` |\n| A provider API key | Store it through `/login` or set its environment variable |\n| A local GGUF model | Connect Pi to the llama.cpp router |\n| An OpenAI-, Anthropic-, or Google-compatible endpoint | Add it to `models.json` |\n| A provider with a custom protocol or authentication flow | Build or install a provider extension |\n\nBrowse the [model catalog](https://pi.dev/models) for current providers, model IDs, capabilities, context limits, and pricing. Pi starts with its bundled catalog and can overlay newer catalog data from pi.dev. Cached catalog data remains available offline; run `pi update --models` to force a refresh.\n\n## Authenticate\n\nRun `/login` and select a provider. Pi stores credentials in [`auth.json`](configuration.md#agent-directory). Run `/logout` to remove stored credentials for a provider.\n\nYou can instead provide an API key through the provider's environment variable. This is useful in CI and other environments where Pi should not write credentials. [Providers](providers.md) lists the variables and provider-specific setup.\n\nWhen several credential sources are configured, Pi uses a runtime `--api-key` first, then a stored `auth.json` credential, an `apiKey` from `models.json`, and finally the provider's environment variables or ambient cloud credentials. Provider extensions can define their own authentication behavior.\n\nKeep `auth.json` and any credential commands private. Project settings and extensions can execute inside the Pi process after you trust a project. Review [Security](security.md) before loading configuration from an untrusted directory.\n\n## Select a model\n\nRun `/model` to search available models. The picker shows models whose providers have usable authentication. Press `Ctrl+S` on a model to save it as the default for new sessions.\n\nRun `/thinking` to select the thinking level for the current model. Press `Ctrl+S` there to save the startup level. Pi limits the choices to levels supported by the selected model.\n\n`Ctrl+P` cycles through available models. Use `/scoped-models` to control that cycle and save the selection, or configure model patterns through [Settings](settings.md#model-cycling).\n\nA session records model and thinking-level changes. Resuming the session restores them without changing defaults for new sessions.\n\n## Connect local models\n\nPi integrates directly with the llama.cpp router. The router discovers GGUF files and loads models on demand. Pi's `/llama` command manages the router, while `/model` selects one of its loaded models.\n\nFollow [Local Models with llama.cpp](llama-cpp.md) for server startup, model layout, downloads, and connection troubleshooting.\n\nFor Ollama, LM Studio, vLLM, SGLang, and other compatible servers, [configure a compatible endpoint](#configure-a-compatible-endpoint) in `models.json`.\n\n## Configure a compatible endpoint\n\nUse [`models.json`](configuration.md#agent-directory) when an endpoint speaks an API Pi already supports. This includes most Ollama, LM Studio, vLLM, SGLang, and proxy deployments.\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/models.schema.json\",\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\nThe dummy key makes the model available to Pi; Ollama ignores it. For an authenticated endpoint, `apiKey` and header values can use `$NAME` or `${NAME}` environment interpolation, a literal value, or a leading `!command`. `$$` and `$!` produce a literal `$` and `!`. Commands in `models.json` run at request time and are not cached by Pi.\n\nOpening `/model` reloads the file. A `models` entry adds or replaces a model with the same ID on that provider. Use `modelOverrides` to change metadata for an existing built-in or extension-provided model without replacing the provider's model list. Unknown override IDs are ignored.\n\nTo route a built-in provider through a proxy, set only its `baseUrl`. Its models and authentication stay unchanged:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": { \"baseUrl\": \"https://proxy.example.com/v1\" }\n  }\n}\n```\n\n[Models Configuration](models-configuration.md) lists every field.\n\n### Describe model input and caching\n\nUse `inputLimits.images.resize` to control how Pi encodes new image attachments, `read` results, and tool-result images before storing them in conversation history:\n\n```json\n{\n  \"id\": \"vision-model\",\n  \"input\": [\"text\", \"image\"],\n  \"inputLimits\": {\n    \"images\": {\n      \"resize\": {\n        \"maxWidth\": 1568,\n        \"maxHeight\": 1568,\n        \"maxBytes\": 524288,\n        \"jpegQuality\": 75\n      }\n    }\n  }\n}\n```\n\n`maxBytes` limits the base64-encoded payload. Omitted resize fields use conservative defaults of 2000 by 2000 pixels, 4.5 MiB encoded, and JPEG quality 80. Images are encoded once; changing models does not rewrite historical images. The catalog can also describe hard request limits with `inputLimits.maxRequestBytes`, `images.maxPerMessage`, and `images.maxPerRequest`, but Pi does not yet rewrite or reject history based on them.\n\n<a id=\"prompt-cache-lifetimes\"></a>\n\nUse `promptCache` to declare the provider's best-effort cache lifetime in seconds for the `short` or `long` retention tier:\n\n```json\n{ \"id\": \"claude-sonnet-5\", \"promptCache\": { \"short\": 300, \"long\": 3600 } }\n```\n\nChoose the conservative end of any published range. A model without a lifetime for the active tier is not eligible for cache warming. A `modelOverrides` entry can set `inputLimits` or `promptCache` for a built-in or extension model, including a model accessed through a validated proxy. See [`cacheWarming`](settings.md#model-and-thinking).\n\n### Configure sampling by thinking level\n\nOpenAI-compatible APIs support free-form `samplingParams` model defaults and `samplingParamsByThinkingLevel` overrides. The latter uses Pi thinking-level keys (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`), not provider values from `thinkingLevelMap`:\n\n```json\n{\n  \"id\": \"qwen-thinking-model\",\n  \"reasoning\": true,\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95\n  },\n  \"samplingParamsByThinkingLevel\": {\n    \"off\": {\n      \"temperature\": 0.7,\n      \"top_p\": 0.8\n    },\n    \"high\": {\n      \"top_k\": 20\n    }\n  }\n}\n```\n\nPi first clamps unsupported thinking levels, then merges model `samplingParams`, the effective level's override, and request-level `samplingParams` in that order. Later values win per key. Missing levels inherit the model defaults. `modelOverrides` merges per-level entries per key with the base model. These fields apply only to `openai-completions`, `openai-responses`, and `azure-openai-responses`; other APIs ignore them.\n\nCompatibility settings should describe verified differences in the endpoint's request or response behavior. Do not enable them based only on an endpoint advertising OpenAI or Anthropic compatibility.\n\n## Use classifier models\n\nClassifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef, Clef Flash, and Clef Omni models from Workers AI and OpenRouter, and OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions):\n\n| Provider | Model IDs | Authentication |\n|---|---|---|\n| `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |\n| `openrouter` | `typesafe/jev-1.13`, `typesafe/jev-1.14`, `~typesafe/jev-latest`, `cloudflare/clef`, `cloudflare/clef-flash`, `cloudflare/clef-omni` | `OPENROUTER_API_KEY` or `/login` |\n| `cloudflare-workers-ai` | `typesafe/jev`, `typesafe/jev-1.14.0`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`, `@cf/cloudflare/clef-omni` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |\n| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |\n| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |\n| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |\n\nChat models on a [llama.cpp router](llama-cpp.md#classification) are also listed as classifier models.\n\nOpenAI's Decisions API needs an API key. Sign in with ChatGPT credentials do not work with it, so `gpt-6-luna` is not listed as available while `openai` is logged in through `/login`, even when `OPENAI_API_KEY` is set; log out of `openai` to use the key. The API rejects inputs above 922K tokens, but requests that run longer than about five seconds, currently above roughly 600K input tokens, fail with a gateway timeout.\n\nClassifier models do not appear in `/model`. The model reaches them through the [`codemode`](cli.md#enable-codemode) tool, which is off unless an MCP server turned it on. Enable it with `\"defaultTools\": [\"+codemode\"]` in [settings](settings.md#tools). Scripts then list classifier models with `models.getAvailableOfType(\"classifier\")` and call `models.classify(model, { state, questions })`:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst result = await models.classify(jev, {\n  state: { message: \"The change works, thanks.\" },\n  questions: {\n    approved: {\n      type: \"bool\",\n      instructions: \"Does the user approve of the result?\",\n      criteria: { true: \"Approval\", false: \"No approval\" },\n    },\n  },\n});\nreturn result.answers;\n```\n\n[Codemode](codemode.md#classify) describes the question and answer types.\n\nClassifier models whose `input` includes `\"image\"` also judge images passed in `images`; other classifier models return an error for them. Image input works with GPT-6 Luna, with Jev from `typesafe` and `cloudflare-workers-ai`, with Clef from `cloudflare-workers-ai`, and with the OpenRouter models whose catalog lists image input. Jev takes PNG and JPEG images, and image input is experimental on TypeSafe's own API. Clef takes PNG, JPEG, and WebP images, at most four per request. Jev from `vercel-ai-gateway` and `opencode` takes text only.\n\nEach System One service takes images in its own request format, set by the model's `compat.imageInput`. Built-in models set it, so overriding a provider's `baseUrl` keeps it. A custom provider that proxies another service's classifiers sets it in its provider-level `compat` in `models.json` (see [System One compatibility](models-configuration.md#typesafe-system-one)). OpenRouter in particular answers requests in TypeSafe's format without an error but drops the images.\n\nWhen the service reports token counts, as all System One services do, `result.usage` carries them with their cost. Pi adds the usage of a script's classifier calls to the `codemode` tool result, so it counts toward the session cost in the footer and `/session`. The cost uses the model's catalog price; models without one, such as TypeSafe's direct `jev-latest`, report tokens at no cost.\n\nExtensions call classifiers through `ctx.modelRegistry.classify()`, without codemode. [Virtual models](virtual-models.md#route-requests) can use them to route requests; see the `jev-router.ts` example.\n\n## Use image models\n\nImage models generate images from a prompt and optional input images. Pi lists OpenRouter's image models, such as `google/gemini-2.5-flash-image` and `black-forest-labs/flux.2-pro`, under the `openrouter` provider; they use the same `OPENROUTER_API_KEY` or `/login` credential as its chat models.\n\nLike classifier models, image models do not appear in `/model`; the model reaches them through the [`codemode`](cli.md#enable-codemode) tool. Scripts list them with `models.getAvailableOfType(\"image\")` and call `models.generateImages(model, { input })`. The result's `output` holds base64 image blocks, which `image()` attaches to the `codemode` result so the model sees them:\n\n```js\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) if (block.type === \"image\") image(block);\n```\n\n`input` can also contain `{ type: \"image\", data, mimeType }` blocks to edit or use as references. Pi adds the usage of a script's image calls to the `codemode` tool result, like classifier calls. Generated images are not saved to disk. [Codemode](codemode.md#generate-images) describes the full API.\n\nExtensions generate images through `ctx.modelRegistry.generateImages()`, without codemode.\n\n## Add a custom provider\n\nUse an extension when the provider needs custom streaming, model discovery, or authentication behavior. See [Custom Providers](custom-provider.md) for the extension workflow.\n\n## Troubleshooting\n\n### A model does not appear\n\nConfirm that its provider has usable authentication. Custom models can load from `models.json` but remain unavailable in `/model` until Pi can resolve credentials. For llama.cpp, only models currently loaded by the router appear.\n\n### Authentication works in one shell only\n\nCheck whether the key came from an environment variable rather than `auth.json`. Environment variables must be present in the process that starts Pi.\n\n### Sign-in opens a browser on a remote machine\n\nComplete the provider's headless authentication flow when available. Some providers let you paste the final redirect URL or authorization code back into Pi. See [Authenticate interactively](providers.md#authenticate-interactively).\n\n### A compatible endpoint rejects requests\n\nCheck its API type and compatibility settings in `models.json`. The upstream server must support the corresponding request fields and behavior.\n","isInternal":false,"tokens":3306,"sizeBytes":13992},{"name":"nvidia-rtx-llama-cpp.md","path":"packages/coding-agent/docs/nvidia-rtx-llama-cpp.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/nvidia-rtx-llama-cpp.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Run Pi with NVIDIA RTX and llama.cpp\n\n[NVIDIA Local AI Inference Recipes](https://nvidia.github.io/local-ai-inference-recipes/index.html) provides llama.cpp settings tuned for specific NVIDIA hardware and models. Each recipe gives a single-model `llama-server` command. Pi works with the llama.cpp router, which can serve and manage several models, so this guide converts the recipe command into a router preset.\n\nThe examples below use Windows PowerShell. See [Local Models with llama.cpp](llama-cpp.md) for Pi's complete router and model-management reference.\n\n## Before you begin\n\n1. Open the [recipe catalog](https://nvidia.github.io/local-ai-inference-recipes/index.html), select your hardware, operating system, and model, then choose **llama.cpp**. Check the prerequisites and memory requirements. Keep the recipe's launch command for the conversion below.\n2. Install llama.cpp with the recipe's setup instructions. The recipe's settings depend on its build, for example CUDA support or speculative decoding options.\n3. Run `llama-server --help` and check that it lists `--models-preset`. Without it, the build cannot load the recipe as a router preset. Use a newer build that still meets the recipe's requirements.\n4. Install Pi with the [Quickstart](quickstart.md). On native Windows, also see [Run Pi on Windows](windows.md).\n\n## Convert the recipe to a preset\n\nA preset lists the settings for one or more models. Each section name becomes the model ID shown in Pi. Save the preset next to `llama-server`, or pass its full path when starting the router.\n\nConvert each option from the recipe command into one preset line:\n\n- Remove the leading dashes: `--ctx-size 65536` becomes `ctx-size = 65536`.\n- Write options without a value as `true`: `--jinja` becomes `jinja = true`.\n- Remove quotes and PowerShell line-continuation backticks around values.\n- Leave out `--host` and `--port`. They belong on the router command line.\n- Add `load-on-startup = true` so the router loads the model when it starts.\n- Replace short model-file options with their long preset keys:\n\n| Recipe option | Preset key |\n|---|---|\n| `-hf` | `hf-repo` |\n| `-hff` | `hf-file` |\n| `-hfrd` | `hf-repo-draft` |\n| `--spec-draft-model` | `spec-draft-model` |\n\nFor example, this recipe command:\n\n```powershell\nllama-server.exe -hf \"unsloth/Qwen3.8-27B-GGUF\" `\n  -hff \"Qwen3.8-27B-UD-Q4_K_M.gguf\" `\n  -hfrd \"z-lab/Qwen3.8-27B-DFlash2-GGUF\" `\n  --spec-draft-model \"Qwen3.8-27B-DFlash2-Q4_K_M.gguf\" `\n  --no-mmproj `\n  --host 127.0.0.1 `\n  --port 9931 `\n  --batch-size 4096 `\n  --ubatch-size 1024 `\n  --ctx-size 65536 `\n  --parallel 1 `\n  -ngl 999 `\n  --jinja `\n  --chat-template-kwargs '{\"preserve_thinking\":true}' `\n  --spec-type draft-dflash `\n  --spec-draft-n-max 7 `\n  --spec-draft-backend-sampling\n```\n\nbecomes `qwen3.8-27b.ini`:\n\n```ini\nversion = 1\n\n[qwen3.8-27b]\nload-on-startup = true\nhf-repo = unsloth/Qwen3.8-27B-GGUF\nhf-file = Qwen3.8-27B-UD-Q4_K_M.gguf\nhf-repo-draft = z-lab/Qwen3.8-27B-DFlash2-GGUF\nspec-draft-model = Qwen3.8-27B-DFlash2-Q4_K_M.gguf\nno-mmproj = true\nbatch-size = 4096\nubatch-size = 1024\nctx-size = 65536\nparallel = 1\nngl = 999\njinja = true\nchat-template-kwargs = {\"preserve_thinking\":true}\nspec-type = draft-dflash\nspec-draft-n-max = 7\nspec-draft-backend-sampling = true\n```\n\n## Start the router\n\nOpen PowerShell in the llama.cpp folder and run:\n\n```powershell\n.\\llama-server.exe --models-preset qwen3.8-27b.ini --host 127.0.0.1 --port 8080\n```\n\nIf the recipe requires a Hugging Face token, set `$env:HF_TOKEN` in the same PowerShell window before starting the router. On its first start, the router downloads the model files.\n\nUse the [router health checks](llama-cpp.md#troubleshooting) to confirm that the server is ready. In Windows PowerShell, use `curl.exe` rather than `curl`, which PowerShell maps to a different command. The model is ready when `/models` lists `qwen3.8-27b` with the status `loaded`.\n\n## Use the model in Pi\n\nFollow [Configure Pi](llama-cpp.md#configure-pi) and connect to `http://127.0.0.1:8080`. The preset section name is the model ID, so select `qwen3.8-27b` in `/model` for this example.\n\nNVIDIA recipe memory settings assume that only the selected model uses the GPU. If `/llama` shows other loaded models, unload them before using the recipe model.\n","isInternal":false,"tokens":1220,"sizeBytes":4290},{"name":"packages.md","path":"packages/coding-agent/docs/packages.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/packages.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Pi Packages\n\nPi packages install and distribute extensions, skills, prompt templates, and themes as one unit. Use a package when a customization should be shared through npm or git, or when several resources belong together.\n\nA package is an ordinary directory or npm package. It can expose conventional resource directories, declare explicit paths under the `pi` key in `package.json`, and carry its own runtime dependencies.\n\n## Install and manage packages\n\nInstall from npm, git, or a local path:\n\n```bash\npi install npm:@example/pi-tools@1.0.0\npi install git:github.com/example/pi-tools@v1\npi install ./local-package\n```\n\n`pi list` shows configured packages. Use `pi remove <source>` to remove one and `pi update --extensions` to reconcile package installations. See [Command Line](cli.md#package-commands) for every package command and option.\n\nPersonal installs are written to `~/.pi/agent/settings.json`. Add `--local` or `-l` to write the package declaration to `.pi/settings.json`. Pi reads declarations from that file only after project trust is granted.\n\nProject packages are installed and loaded only after project trust is resolved. Packages can execute extension code and can include skills that instruct the model to run programs. Review third-party package source before installing it. Review project package declarations before granting project trust.\n\nUse `--extension` or `-e` to try a package for one invocation without adding it to settings:\n\n```bash\npi -e npm:@example/pi-tools\n```\n\n## Choose a source\n\n| Source | Example | Behavior |\n|---|---|---|\n| npm | `npm:@example/pi-tools@1.0.0` | Installed under the Pi npm directory |\n| git | `git:github.com/example/pi-tools@v1` | Cloned and reconciled to the selected ref |\n| URL | `https://github.com/example/pi-tools` | Treated as a git source |\n| Local | `./pi-tools` | Loaded from the resolved path without copying |\n\nVersioned npm specifications are pinned. Git tags and commits are also pinned; package updates reconcile the checkout but do not move a configured ref.\n\nRelative local paths resolve from the settings file that contains them. A file path loads one extension. A directory follows normal package discovery rules.\n\n## Create a package\n\nThe simplest package uses conventional directories:\n\n```text\nmy-pi-package/\n├── package.json\n├── extensions/\n├── skills/\n├── prompts/\n└── themes/\n```\n\nWithout a `pi` manifest, Pi discovers TypeScript and JavaScript extensions, skill directories, Markdown prompts, and JSON themes from those directories.\n\nUse an explicit manifest when resources live elsewhere or need filtering:\n\n```json\n{\n  \"name\": \"my-pi-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./src/extension.ts\"],\n    \"skills\": [\"./resources/skills\"],\n    \"prompts\": [\"./resources/prompts/*.md\"],\n    \"themes\": [\"./resources/themes/*.json\"]\n  }\n}\n```\n\nPaths are relative to the package root. Arrays accept glob patterns and exclusions. List dot-prefixed or symlinked resource roots directly when traversal through a glob would not discover them.\n\nThe `pi-package` keyword makes an npm package eligible for discovery in the [Pi package gallery](https://pi.dev/packages). Optional `pi.image` and `pi.video` fields add gallery previews.\n\n## Declare dependencies\n\nPut runtime packages imported by extensions in `dependencies`. Pi installs package dependencies when it installs an npm or git source.\n\nPi supplies these packages to extensions and skills:\n\n- `@earendil-works/pi-ai`\n- `@earendil-works/pi-agent-core`\n- `@earendil-works/pi-coding-agent`\n- `@earendil-works/pi-tui`\n- `typebox`\n\nDeclare the host-provided packages listed above in `peerDependencies` with a `\"*\"` range and do not bundle them. Pi suppresses automatic peer installation for managed npm packages and git packages installed with npm, pnpm, or Bun. Local packages are not installed or modified, so their dependency tree remains the package author's responsibility.\n\nDo not list host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping in compiled ESM and create duplicate classes, registries, and initialization work. Pi reports an extension warning when it detects this manifest configuration. Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths.\n\nInstalled packages load with separate module roots. Do not rely on two packages sharing one dependency instance or one package resolving another package’s undeclared dependency.\n\n## Select package resources\n\nThe object form in settings narrows which resources load from a package:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"npm:@example/pi-tools\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"]\n    }\n  ]\n}\n```\n\nFor each resource type:\n\n- Omit the property to load everything allowed by the package.\n- Use `[]` to load none of that type.\n- Use `!pattern` to exclude glob matches.\n- Use `+path` to include one exact allowed path.\n- Use `-path` to exclude one exact path.\n\nFilters narrow the package manifest. They do not expose resources that the package itself did not declare.\n\nRun `pi config` to enable or disable discovered resources and pi's built-in extensions. It starts with personal configuration; press Tab to switch scope, or run `pi config --local` to start with project overrides.\n\n## Understand scope and identity\n\nThe same package can appear in personal and project settings. A project entry normally replaces the personal entry. With `autoload: false`, the project entry instead acts as a filtering delta over the personal package.\n\nPi identifies npm packages by package name, git packages by repository URL without the ref, and local packages by resolved absolute path. This prevents the same package from loading twice through equivalent declarations.\n\nUse [Extensions](extensions.md), [Skills](skills.md), [Prompt Templates](prompt-templates.md), and [Themes](themes.md) to design each resource before packaging it.\n","isInternal":false,"tokens":1307,"sizeBytes":6106},{"name":"prompt-templates.md","path":"packages/coding-agent/docs/prompt-templates.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/prompt-templates.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Prompt Templates\n\nPrompt templates turn Markdown files into reusable `/` commands. Use one when you want to reuse the same prompt without adding executable behavior or a larger set of supporting instructions.\n\nA template can accept arguments and appear in command completion. Pi can load templates from personal configuration, project configuration, an explicit path, or a Pi package. Project configuration loads only after project trust is granted.\n\n## Create a template\n\nCreate `~/.pi/agent/prompts/review.md`:\n\n```markdown\n---\ndescription: Review staged git changes\nargument-hint: \"[focus]\"\n---\nReview the staged changes. Focus on ${1:-correctness, security, and error handling}.\n```\n\nThe filename becomes the command name, so this template is available as `/review`. The `description` appears in command completion. If it is omitted, Pi uses the first non-empty line.\n\n`argument-hint` is optional. Use `<angle brackets>` for required arguments and `[square brackets]` for optional arguments.\n\nRun `/reload` after adding or changing a template in an active session.\n\n<a id=\"invoke-a-template\"></a>\n\n## Use a template\n\nType the template command in the editor:\n\n```text\n/review\n/review concurrency\n```\n\nPi expands the template before the resulting text enters the agent. Extensions receive the raw input first through the `input` event unless an extension command with the same name handles it.\n\nTemplates support these substitutions:\n\n| Syntax | Result |\n|---|---|\n| `$1`, `$2`, … | One positional argument |\n| `$@` or `$ARGUMENTS` | All arguments joined with spaces |\n| `${1:-default}` | First argument, or a default value |\n| `${@:-default}` | All arguments, or a default value |\n| `${@:N}` | Arguments starting at position `N` |\n| `${@:N:L}` | `L` arguments starting at position `N` |\n\nArguments follow shell-like quoting, so `/review \"API compatibility\"` supplies one argument containing a space.\n\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the template in your user or project prompt directory. Conventional prompt directories load direct `.md` children only.\n\nSettings and packages can select nested Markdown files; a package manifest can narrow discovery with explicit paths and globs. See [Settings](settings.md#resources) and [Pi Packages](packages.md) for these options.\n\nProject templates become commands in the editor after trust is granted. Review their content before trusting an unfamiliar project. See [Security](security.md#understand-project-trust).\n","isInternal":false,"tokens":532,"sizeBytes":2490},{"name":"providers.md","path":"packages/coding-agent/docs/providers.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/providers.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Providers\n\nMost hosted providers support one or both of these authentication methods:\n\n- Sign in through a browser or device flow backed by OAuth.\n- Provide an API key.\n\nUse `/login [provider]` to see the methods supported by a provider. Amazon Bedrock and Google Vertex AI can also use ambient cloud credentials.\n\n## Authenticate interactively\n\nRun `/login` and select a provider. Pi guides you through its OAuth or API-key flow and saves the resulting credential in [`auth.json`](configuration.md#agent-directory).\n\nOn a remote or headless machine, an OAuth callback may not reach the local process. When prompted, paste the final redirect URL or authorization code back into Pi.\n\nRun `/logout` and select a provider to remove its stored credential. This does not unset environment variables, remove authentication from `models.json`, or revoke the credential at the provider.\n\n`auth.json` can contain API keys and OAuth tokens. Keep it private and do not commit it.\n\n## Use an API key from the environment\n\nEnvironment variables are useful in CI and anywhere Pi should not store the key. Set the variable before starting Pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nThis table covers providers with a single primary API-key variable. Providers that need additional configuration or support ambient credentials are covered under [Provider Specific Config](#provider-specific-config).\n\n| Provider | Environment variable |\n|---|---|\n| Anthropic | `ANTHROPIC_API_KEY` |\n| Ant Ling | `ANT_LING_API_KEY` |\n| OpenAI | `OPENAI_API_KEY` |\n| DeepSeek | `DEEPSEEK_API_KEY` |\n| NVIDIA NIM | `NVIDIA_API_KEY` |\n| Google Gemini | `GEMINI_API_KEY` |\n| GitHub Copilot | `COPILOT_GITHUB_TOKEN` |\n| Mistral | `MISTRAL_API_KEY` |\n| Groq | `GROQ_API_KEY` |\n| Cerebras | `CEREBRAS_API_KEY` |\n| xAI | `XAI_API_KEY` |\n| OpenRouter | `OPENROUTER_API_KEY` |\n| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |\n| ZAI Coding Plan (Global) | `ZAI_API_KEY` |\n| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` |\n| OpenCode Zen and Go | `OPENCODE_API_KEY` |\n| Radius | `RADIUS_API_KEY` |\n| TypeSafe ([classifier models](models.md#use-classifier-models)) | `TYPESAFE_API_KEY` |\n| Hugging Face | `HF_TOKEN` |\n| Fireworks | `FIREWORKS_API_KEY` |\n| Together AI | `TOGETHER_API_KEY` |\n| Baseten | `BASETEN_API_KEY` |\n| Kimi For Coding | `KIMI_API_KEY` |\n| Meta | `META_API_KEY` |\n| MiniMax | `MINIMAX_API_KEY` |\n| MiniMax (China) | `MINIMAX_CN_API_KEY` |\n| Moonshot AI (Global and China) | `MOONSHOT_API_KEY` |\n| Qwen Token Plan and Individual | `QWEN_TOKEN_PLAN_API_KEY` |\n| Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |\n| Xiaomi MiMo | `XIAOMI_API_KEY` |\n| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |\n| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |\n| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` |\n\nAnthropic also recognizes `ANTHROPIC_OAUTH_TOKEN` as an API credential and `ANTHROPIC_AUTH_TOKEN` as bearer authentication.\n\nWith no key or token set, Anthropic uses workload identity federation when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` and `ANTHROPIC_IDENTITY_TOKEN_FILE` are set: the Anthropic SDK exchanges the identity token for a short-lived access token and refreshes it itself (re-reading the identity token file, so keep that file fresh for long sessions). `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID` are passed through when set.\n\n## Load an API key from a command\n\nTo use a secret manager without writing the resolved key to disk, set a provider's `key` in `auth.json` to a command prefixed with `!`:\n\n```json\n{\n  \"anthropic\": {\n    \"type\": \"api_key\",\n    \"key\": \"!security find-generic-password -ws 'anthropic'\"\n  }\n}\n```\n\nPi runs the command when the key is first needed and caches its standard output for the process lifetime. Empty output, a timeout, or a nonzero exit leaves the key unresolved until Pi restarts.\n\n## Provider Specific Config\n\nThe providers below have additional setup, need additional settings, or can use credentials supplied by their platform.\n\nA stored API-key credential can include an `env` object. Its values take priority over the process environment for that provider:\n\n```json\n{\n  \"cloudflare-workers-ai\": {\n    \"type\": \"api_key\",\n    \"key\": \"...\",\n    \"env\": {\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\"\n    }\n  }\n}\n```\n\n### Radius\n\nRadius is a service crafted for Pi by the builders of Pi, Earendil Works. It provides a customizable AI gateway with organization-level controls and analytics built in, and artifacts for sharing what you create with Pi.\n\nTo get started, run `/login radius` in Pi. This adds Radius as a provider, and its models appear in `/model` like any other provider's.\n\nRadius also has an MCP server, so Pi can manage Radius for you.\n\nRadius is currently in early alpha and evolving quickly. See [radius.earendil.com](https://radius.earendil.com) for more.\n\nRadius authentication uses its gateway catalog and caches refreshed model metadata for later offline startup. A custom Radius gateway configured in `models.json` uses its own catalog rather than inheriting the public `radius.pi.dev` catalog.\n\n### OpenRouter\n\nAuthenticate with `/login openrouter` or `OPENROUTER_API_KEY`.\n\nWith a key, Pi lists only the OpenRouter chat models that key may use, with the key's context size, max output, and prices. These come from OpenRouter's `/models/user` endpoint and reflect guardrails and provider restrictions set in OpenRouter. Image and classifier models are not filtered.\n\nPi refreshes the list in the background at startup in interactive mode, when `/model` opens, after `/login`, and on `pi update --models`. The last list is saved and used offline and in `pi -p` and RPC mode. After switching keys outside `/login`, the previous key's list stays until the next refresh.\n\nRegional endpoints filter models to their region. To use one, set the base URL in [`models.json`](models.md#configure-a-compatible-endpoint):\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": { \"baseUrl\": \"https://us.openrouter.ai/api/v1\" }\n  }\n}\n```\n\nIf the base URL rejects the key at `/models/user` or does not have that endpoint, for example a proxy configured as the OpenRouter base URL, Pi lists every OpenRouter model.\n\n### Azure OpenAI\n\nThe provider ID is `azure` (formerly `azure-openai-responses`). Use it as the key in `auth.json`, `models.json`, and `settings.json`, and in model references such as `--model azure/gpt-5.4`.\n\nThe `azure` provider serves OpenAI models through the Responses API and Microsoft Foundry models through Chat Completions, such as `azure/deepseek-v4-pro`.\n\nSet an API key plus either a base URL or resource name:\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# Or:\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n```\n\nResource root URLs under `ai.azure.com`, `cognitiveservices.azure.com`, and `openai.azure.com` are normalized to the OpenAI API path.\n\nPi sends the model ID as the deployment name. If a deployment has a different name, map it with `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`:\n\n```bash\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-5.4=my-gpt-deployment,deepseek-v4-pro=my-deepseek\n```\n\n`AZURE_OPENAI_API_VERSION` overrides the API version for OpenAI models (default `v1`).\n\nTo use a Foundry model that Pi does not include, add it under `azure` in [`models.json`](models.md#configure-a-compatible-endpoint) with `api: \"openai-completions\"`. Custom models require a `baseUrl`; `AZURE_OPENAI_BASE_URL` and `AZURE_OPENAI_RESOURCE_NAME` take priority over it when set:\n\n```json\n{\n  \"providers\": {\n    \"azure\": {\n      \"baseUrl\": \"https://your-resource.services.ai.azure.com\",\n      \"models\": [\n        { \"id\": \"your-deployment\", \"api\": \"openai-completions\" }\n      ]\n    }\n  }\n}\n```\n\n### Amazon Bedrock\n\nBedrock can use a bearer token or an ambient AWS credential source:\n\n```bash\n# Named profile\nexport AWS_PROFILE=your-profile\n\n# IAM keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n# Required for temporary credentials\nexport AWS_SESSION_TOKEN=...\n\n# Bedrock bearer token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Region, when not supplied by the profile or AWS SDK configuration\nexport AWS_REGION=us-west-2\n# AWS_DEFAULT_REGION is also supported\n```\n\nPi also supports ECS task credentials and IRSA through the standard `AWS_CONTAINER_CREDENTIALS_*` and `AWS_WEB_IDENTITY_TOKEN_FILE` variables.\n\n### Cloudflare AI Gateway\n\nThe gateway requires a token, account ID, and gateway ID:\n\n```bash\nexport CLOUDFLARE_API_KEY=...\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...\n```\n\nThe account and gateway IDs can come from the process environment or the credential's `env` object in `auth.json`.\n\n`CLOUDFLARE_API_KEY` authenticates Pi to the gateway. Upstream access can use Cloudflare unified billing, credentials stored in the gateway, or an `Authorization` header configured for the provider in `models.json`.\n\n### Cloudflare Workers AI\n\nWorkers AI requires a token and account ID:\n\n```bash\nexport CLOUDFLARE_API_KEY=...\nexport CLOUDFLARE_ACCOUNT_ID=...\n```\n\nThe account ID can also be stored in the credential's `env` object.\n\n### Google Vertex AI\n\nUse a Google Cloud API key:\n\n```bash\nexport GOOGLE_CLOUD_API_KEY=...\n```\n\nTo use Application Default Credentials, configure a project and location:\n\n```bash\nexport GOOGLE_CLOUD_PROJECT=your-project\n# GCLOUD_PROJECT is also supported\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nThen authenticate:\n\n```bash\ngcloud auth application-default login\n```\n\nTo use a service-account key file instead, set `GOOGLE_APPLICATION_CREDENTIALS` along with the project and location.\n","isInternal":false,"tokens":2401,"sizeBytes":9681},{"name":"quickstart.md","path":"packages/coding-agent/docs/quickstart.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/quickstart.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Quickstart\n\nPi runs in your terminal and works with files on your machine. To use it, you need access to a model through a supported provider. This can be a subscription, an API key, or a local model.\n\nFor native Windows setup, read [Windows Setup](windows.md). For Android, read [Termux Setup](termux.md).\n\n## 1. Install Pi\n\nOn macOS or Linux, you can use the installer:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nThe installer pins all dependencies and updates Pi with `pi update`. Alternatively, install Pi from npm, which does not pin transitive dependencies. This requires Node.js 22.19 or newer:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\nPi does not require dependency lifecycle scripts for a normal npm installation.\n\nWith Nix on macOS or Linux, install the latest release from Pi's flake. Nix builds Pi from source:\n\n```bash\nnix profile add github:earendil-works/pi/stable\n```\n\nOlder Nix versions use `nix profile install` instead. Update with `nix profile upgrade pi`; `pi update` cannot update a Nix installation. To pin a release, use a tag such as `github:earendil-works/pi/v1.0.0`.\n\nVerify the installation:\n\n```bash\npi --version\n```\n\n## 2. Start Pi\n\nChange to the folder you want Pi to work with, then start it:\n\n```bash\ncd /path/to/folder\npi\n```\n\nThe working folder helps Pi discover relevant files, instructions, and configuration. Pi also uses it to group saved sessions.\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Pi running in a terminal with a conversation, input editor, and status footer\" width=\"750\"></p>\n\nThe interface shows your conversation, an editor for prompts and commands, and a footer with the current folder, model, and session status. See [Use Pi in the terminal](usage.md) to learn how to add files, run commands, direct ongoing work, and manage results.\n\n## 3. Choose a model\n\nA **model** generates Pi's responses. A **provider** is the service or account Pi uses to access that model.\n\nIn Pi, run:\n\n```text\n/login\n```\n\nChoose a provider, then follow the prompts to use a subscription or store an API key. Run `/model` afterward if you want to select a different available model.\n\nSee [Choose a model and provider](models.md) for supported providers, environment-variable authentication, local models, and custom endpoints.\n\n## 4. Give Pi a task\n\nPi shows each file read, search, command, and edit it performs. It does not ask before every tool call.\n\nEnter a task that matches your work, for example:\n\n```text\nSummarize @meeting-notes.md and save the action items to action-items.md.\n```\n\n```text\nExplain how this repository is structured and how to run its checks.\n```\n\n```text\nCompare @previous.csv with @current.csv and summarize the important changes.\n```\n\nType `@` in the editor to search for a file instead of entering its full path. When Pi finishes, review its response and any changed files. Use version control or backups for important work. For untrusted or unattended work, use a container or another sandbox. See [Security](security.md).\n\n## Continue later\n\nPi saves sessions automatically. Exit Pi, then resume the most recent session for the same working folder with:\n\n```bash\npi --continue\n```\n\nUse `/resume` to choose another saved session. See [Continue or branch a session](sessions.md) for session naming, branching, compaction, export, and sharing.\n\n## Next steps\n\n- [Use Pi interactively](usage.md) to learn input, commands, shortcuts, and queued messages.\n- [Add instructions](configuration.md#context-files) that Pi should follow whenever it works in a folder.\n- [Choose a model and provider](models.md).\n\n### Choose how to customize Pi\n\nStart with the least powerful mechanism that meets your need:\n\n| Need | Start with |\n|---|---|\n| Give Pi persistent instructions for a folder | [`AGENTS.md`](configuration.md#context-files) |\n| Reuse a prompt from the `/` menu | [Prompt template](prompt-templates.md) |\n| Add task-specific instructions and supporting files | [Skill](skills.md) |\n| Add executable tools, commands, or event handlers | [Extension](extensions.md) |\n| Build a custom terminal component | [Terminal UI](tui.md) |\n| Connect an unsupported model service | [Custom provider](custom-provider.md) |\n| Install or distribute several resources | [Pi package](packages.md) |\n\n## Uninstall Pi\n\nIf you installed Pi with npm, run:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nIf you used the installer, run it again and choose **Uninstall Pi**:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nIf you installed Pi with Nix, run:\n\n```bash\nnix profile remove pi\n```\n\nNone of these methods removes configuration, credentials, sessions, or installed Pi packages from `~/.pi/agent/`.\n","isInternal":false,"tokens":1117,"sizeBytes":4741},{"name":"rpc-commands.md","path":"packages/coding-agent/docs/rpc-commands.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/rpc-commands.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# RPC Commands\n\nThis reference lists commands accepted on stdin in [RPC mode](rpc.md). Each command and response is one JSON object. Shared message values use the [message types](message-types.md).\n\n## Prompting\n\n### prompt\n\nSend a user prompt to the agent. The command response is emitted after the prompt is accepted, queued, or handled. Events continue streaming asynchronously after acceptance.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nWith images:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**During streaming**: If the agent is already streaming, you must specify `streamingBehavior` to queue the message:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: Queue the message while the agent is running. It is delivered after the current assistant turn finishes executing its tool calls, before the next LLM call.\n- `\"followUp\"`: Wait until the agent finishes. Message is delivered only when agent stops.\n\nIf the agent is streaming and no `streamingBehavior` is specified, the command returns an error.\n\n**Extension commands**: If the message is an extension command (e.g., `/mycommand`), it executes immediately even during streaming. Extension commands manage their own LLM interaction via `pi.sendMessage()`.\n\n**Input expansion**: Skill commands (`/skill:name`) and prompt templates (`/template`) are expanded before sending/queueing.\n\nResponse:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true, \"data\": {\"disposition\": \"started\"}}\n```\n\n`data.disposition` is `\"handled\"` if an extension command or input handler consumed the prompt, `\"queued\"` if Pi queued it during a run, or `\"started\"` if Pi accepted it to start a run. This describes the submitted prompt, not independent work started by an extension or a guarantee of completion.\n\n`success: true` means the prompt was accepted, queued, or handled immediately. `success: false` means the prompt was rejected before acceptance. Failures after acceptance are reported through the normal event and message stream, not as a second `response` for the same request id.\n\nThe `images` field is optional. Each image uses `ImageContent` format: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n### steer\n\nQueue a steering message while the agent is running. It is delivered after the current assistant turn finishes executing its tool calls, before the next LLM call. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nWith images:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nThe `images` field is optional. Each image uses `ImageContent` format (same as `prompt`).\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true, \"data\": {\"disposition\": \"queued\"}}\n```\n\n`data.disposition` is `\"handled\"` if an input handler consumed this steer, or `\"queued\"` if Pi queued it (including after a handler transformed it). It does not guarantee this message remains queued.\n\nSee [set_steering_mode](#set_steering_mode) for controlling how steering messages are processed.\n\n### follow_up\n\nQueue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nWith images:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nThe `images` field is optional. Each image uses `ImageContent` format (same as `prompt`).\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true, \"data\": {\"disposition\": \"queued\"}}\n```\n\n`data.disposition` has the same `\"handled\"` or `\"queued\"` meaning as for `steer`, applied to this follow-up.\n\nSee [set_follow_up_mode](#set_follow_up_mode) for controlling how follow-up messages are processed.\n\n### abort\n\nAbort the current operation and wait for the session to become idle before responding.\n\n```json\n{\"type\": \"abort\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n### clear_queue\n\nRemove queued steering and follow-up messages and return their text.\n\n```json\n{\"type\": \"clear_queue\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clear_queue\",\n  \"success\": true,\n  \"data\": {\n    \"steering\": [\"Change direction\"],\n    \"followUp\": [\"Summarize when finished\"]\n  }\n}\n```\n\nTo implement interactive Esc behavior, send `clear_queue` before `abort`, then restore the returned text in the client editor. `abort` continues queued messages when they remain in the session.\n\n### new_session\n\nStart a fresh session. Can be canceled by a `session_before_switch` extension event handler.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nWith optional parent session tracking:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nIf an extension canceled:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n## State\n\n### get_state\n\nGet current session state.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\nThe `model` field is a full [Model](#model-object) object, or omitted when no model is selected. The `sessionName` field is the display name set via `set_session_name`, or omitted if not set.\n\n### get_messages\n\nGet all messages in the conversation.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nMessages are `AgentMessage` objects (see [Message Types](message-types.md)).\n\n## Model\n\n### set_model\n\nSwitch to a specific model.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nResponse contains the full [Model](#model-object) object:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n### cycle_model\n\nCycle to the next available model. Returns `null` data if only one model available.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\nThe `model` field is a full [Model](#model-object) object.\n\n### get_available_models\n\nList all configured models.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nResponse contains an array of full [Model](#model-object) objects:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n## Thinking\n\n### set_thinking_level\n\nSet the reasoning/thinking level for models that support it.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nLevels: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` and `\"max\"` are exposed only when supported by the selected model. Some models, including GPT-5.6, expose both.\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n### cycle_thinking_level\n\nCycle through available thinking levels. Returns `null` data if model doesn't support thinking.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n### get_available_thinking_levels\n\nList the thinking levels supported by the current model. Returns `[\"off\"]` for a model without reasoning support.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n## Queue modes\n\n### set_steering_mode\n\nControl how steering messages (from `steer`) are delivered.\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: Deliver all steering messages after the current assistant turn finishes executing its tool calls\n- `\"one-at-a-time\"`: Deliver one steering message per completed assistant turn (default)\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n### set_follow_up_mode\n\nControl how follow-up messages (from `follow_up`) are delivered.\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: Deliver all follow-up messages when agent finishes\n- `\"one-at-a-time\"`: Deliver one follow-up message per agent completion (default)\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n## Compaction\n\n### compact\n\nManually compact conversation context to reduce token usage.\n\n```json\n{\"type\": \"compact\"}\n```\n\nWith custom instructions:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter` is a heuristic estimate over the rebuilt message context immediately after compaction, not a provider-exact token count. `usage` reports the LLM call or calls that generated the summary and may be omitted by custom compaction handlers.\n\n### set_auto_compaction\n\nEnable or disable automatic compaction when context is nearly full.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n## Retry\n\n### set_auto_retry\n\nEnable or disable automatic retry on transient errors (overloaded, rate limit, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n### abort_retry\n\nAbort an in-progress retry (cancel the delay and stop retrying).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n## Bash\n\n### bash\n\nExecute a shell command and add output to conversation context. Output streams as `bash_execution_update` events while the command runs; the response contains the final result.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nSet `excludeFromContext` to `true` when the command output should be stored in the session but omitted from the model context on the next prompt.\n\nInclude an `id` to associate streamed `bash_execution_update` events with this command.\n\nResponse:\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\nIf output was truncated, includes `fullOutputPath`:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**How bash results reach the LLM:**\n\nThe `bash` command executes immediately and returns a `BashResult`. Internally, a `BashExecutionMessage` is created and stored in the agent's message state.\n\nWhen the next `prompt` command is sent, Pi transforms context messages before sending them to the model. Unless `excludeFromContext` is true, the `BashExecutionMessage` becomes a `UserMessage` with this format:\n\n````\nRan `ls -la`\n```\ntotal 48\ndrwxr-xr-x ...\n```\n````\n\nThis means:\n1. Included bash output reaches the model on the **next prompt**, not immediately.\n2. Multiple bash commands can run before a prompt; Pi includes each output that does not set `excludeFromContext`.\n\n### abort_bash\n\nAbort a running bash command.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n## Session\n\n### get_session_stats\n\nGet token usage, cost statistics, and current context window usage.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens` and `cost` include assistant messages, usage reported by tools, and compaction/branch-summary generation across the full session. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.\n\n`contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.\n\n### export_html\n\nExport session to an HTML file.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nWith custom path:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n### switch_session\n\nLoad a different session file. Can be canceled by a `session_before_switch` extension event handler.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nResponse:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nIf an extension canceled the switch:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### fork\n\nCreate a new fork from a previous user message on the active branch. Can be canceled by a `session_before_fork` extension event handler. Returns the text of the message being forked from.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nIf an extension canceled the fork:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n### clone\n\nDuplicate the current active branch into a new session at the current position. Can be canceled by a `session_before_fork` extension event handler.\n\n```json\n{\"type\": \"clone\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nIf an extension canceled the clone:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n### get_fork_messages\n\nGet user messages available for forking.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n### get_entries\n\nGet all session entries in append order (excluding the session header). The session is an append-only tree of entries with stable ids, so an entry id works as a durable cursor: pass the last entry id you have seen as `since` to get only entries strictly after it, even across client restarts. Unlike `get_messages`, this includes pre-compaction history and abandoned branches.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nWith a cursor:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId` is the id of the current leaf entry (`null` for an empty session), so a client can tell in one round trip whether the active branch moved. If `since` does not match any entry id, the response is `success: false`.\n\n### get_tree\n\nGet the session as a tree of entries. Each node is `{entry, children, label?, labelTimestamp?}`. The result is an array because navigation APIs can create multiple roots; orphaned entries with broken parent chains also appear as roots.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n### get_last_assistant_text\n\nGet the text content of the last assistant message.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\nThe `text` value is `null` if no assistant text exists.\n\n### set_session_name\n\nSet a display name for the current session. The name appears in session listings and helps identify sessions.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nThe current session name is available via `get_state` in the `sessionName` field. To set the initial name when starting RPC mode, pass `--name <name>` or `-n <name>` to the `pi --mode rpc` process.\n\n## Discoverable commands\n\n### get_commands\n\nGet available commands (extension commands, prompt templates, and skills). Run one through the `prompt` command by prefixing its name with `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nResponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\n        \"name\": \"fix-tests\",\n        \"description\": \"Fix failing tests\",\n        \"source\": \"prompt\",\n        \"sourceInfo\": {\n          \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\",\n          \"source\": \"local\",\n          \"scope\": \"project\",\n          \"origin\": \"top-level\"\n        }\n      }\n    ]\n  }\n}\n```\n\nEach command has:\n- `name`: Command name (use `/name`)\n- `description`: Human-readable description (optional for extension commands)\n- `source`: What kind of command:\n  - `\"extension\"`: Registered via `pi.registerCommand()` in an extension\n  - `\"prompt\"`: Loaded from a prompt template `.md` file\n  - `\"skill\"`: Loaded from a skill directory (name is prefixed with `skill:`)\n- `sourceInfo`: Metadata for the resource that registered the command:\n  - `path`: Absolute path to the resource\n  - `source`: How Pi discovered it, such as `\"local\"`, `\"auto\"`, or `\"cli\"`\n  - `scope`: `\"user\"`, `\"project\"`, or `\"temporary\"`\n  - `origin`: `\"top-level\"` for a directly loaded resource or `\"package\"` for a package resource\n  - `baseDir`: Package base directory, when applicable\n\n**Note**: Built-in TUI commands (`/settings`, `/hotkeys`, etc.) are not included. They are handled only in interactive mode and would not execute if sent via `prompt`.\n\n## Model object\n\nModel commands return the complete configured model definition. Costs are in US dollars per million tokens.\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\nFor model configuration, see [Configure a compatible endpoint](models.md#configure-a-compatible-endpoint). For TypeScript, use the exported `Model` type from `@earendil-works/pi-ai`.\n","isInternal":false,"tokens":5630,"sizeBytes":21482},{"name":"rpc-extension-ui.md","path":"packages/coding-agent/docs/rpc-extension-ui.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/rpc-extension-ui.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# RPC Extension UI\n\nExtensions can request user interaction through `ctx.ui`. In RPC mode, supported calls become a request/response subprotocol alongside normal [RPC commands](rpc-commands.md) and [session events](json.md).\n\nThere are two categories of extension UI methods:\n\n- **Dialog methods** (`select`, `confirm`, `input`, `editor`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.\n- **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): emit an `extension_ui_request` on stdout but do not expect a response. The client can display the information or ignore it.\n\nIf a dialog method includes a `timeout` field, the agent-side will auto-resolve with a default value when the timeout expires. The client does not need to track timeouts.\n\n## Limitations\n\nSome `ExtensionUIContext` methods are not supported or degraded in RPC mode because they require direct terminal UI access:\n\n- `custom()` returns `undefined`.\n- `onTerminalInput()` returns a no-op unsubscribe function.\n- `setWorkingMessage()`, `setWorkingVisible()`, `setWorkingIndicator()`, `setHiddenThinkingLabel()`, `setFooter()`, `setHeader()`, `addAutocompleteProvider()`, `setEditorComponent()`, and `setToolsExpanded()` are no-ops.\n- `getEditorText()` returns `\"\"` and `getEditorComponent()` returns `undefined`.\n- `getToolsExpanded()` returns `false`.\n- `pasteToEditor()` delegates to `setEditorText()` without terminal paste handling.\n- `getAllThemes()` returns `[]`, and `getTheme()` returns `undefined`.\n- `setTheme()` returns `{ success: false, error: \"Theme switching not supported in RPC mode\" }`.\n\nNote: `ctx.mode` is `\"rpc\"` and `ctx.hasUI` is `true` in RPC mode because the dialog and fire-and-forget methods are functional via the extension UI sub-protocol. Use `ctx.mode === \"tui\"` to guard TUI-specific features like `custom()` that require a real terminal.\n\n## Requests from Pi\n\nAll requests have `type: \"extension_ui_request\"`, a unique `id`, and a `method` field.\n\n### select\n\nPrompt the user to choose from a list. Dialog methods with a `timeout` field include the timeout in milliseconds; the agent auto-resolves with `undefined` if the client doesn't respond in time.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the selected option string) or `cancelled: true`.\n\n### confirm\n\nPrompt the user for yes/no confirmation.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\nExpected response: `extension_ui_response` with `confirmed: true/false` or `cancelled: true`.\n\n### input\n\nPrompt the user for free-form text.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the entered text) or `cancelled: true`.\n\n### editor\n\nOpen a multi-line text editor with optional prefilled content.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\nExpected response: `extension_ui_response` with `value` (the edited text) or `cancelled: true`.\n\n### notify\n\nDisplay a notification. Fire-and-forget, no response expected.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\nThe `notifyType` field is `\"info\"`, `\"warning\"`, or `\"error\"`. Defaults to `\"info\"` if omitted.\n\n### setStatus\n\nSet or clear a status entry in the footer/status bar. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\nSend `statusText: undefined` (or omit it) to clear the status entry for that key.\n\n### setWidget\n\nSet or clear a widget (block of text lines) displayed above or below the editor. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\nSend `widgetLines: undefined` (or omit it) to clear the widget. The `widgetPlacement` field is `\"aboveEditor\"` (default) or `\"belowEditor\"`. Only string arrays are supported in RPC mode; component factories are ignored.\n\n### setTitle\n\nSet the terminal window/tab title. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n### set_editor_text\n\nSet the text in the input editor. Fire-and-forget.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n## Responses to Pi\n\nResponses are sent for dialog methods only (`select`, `confirm`, `input`, `editor`). The `id` must match the request.\n\n### Value response (select, input, editor)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n### Confirmation response (confirm)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n### Cancellation response (any dialog)\n\nDismiss any dialog method. The extension receives `undefined` (for select/input/editor) or `false` (for confirm).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Example\n\nSee the checked [RPC extension UI client](../examples/rpc-extension-ui.ts) and its [demo extension](../examples/extensions/rpc-demo.ts).\n\nThe exported request and response unions are defined in [`rpc-types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-types.ts). See [Extensions](extensions.md#ui-and-modes) for mode-independent extension guidance.\n","isInternal":false,"tokens":1595,"sizeBytes":6174},{"name":"rpc.md","path":"packages/coding-agent/docs/rpc.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/rpc.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# RPC Mode\n\nRPC mode runs Pi as a long-lived subprocess controlled through JSON records on stdin and stdout. Use it for language-independent integrations, process isolation, IDEs, and custom user interfaces.\n\nFor an in-process Node.js or Bun integration, prefer the [SDK](sdk.md). For a subprocess-based TypeScript integration, prefer the exported `RpcClient`, which starts Pi, correlates responses, exposes typed command methods, and delivers events to listeners.\n\n| Interface | Process boundary | Control model | Best fit |\n|---|---|---|---|\n| [SDK](sdk.md) | In process | Direct TypeScript methods and events | Node.js or Bun hosts that want complete API access |\n| RPC | Child process | JSONL commands, responses, and events | Other languages, isolated processes, IDEs, or custom clients |\n\n## Start RPC mode\n\n```bash\npi --mode rpc --no-session\n```\n\nNormal CLI options still select the working folder, model, tools, resources, and session behavior. Common choices include `--provider`, `--model`, `--name`, `--no-session`, and `--session-dir`. See [Command Line](cli.md) for the complete, version-specific interface; `pi --help` is authoritative for the installed version.\n\nRPC mode rejects `@file` prompt arguments. Send prompts through the [`prompt`](rpc-commands.md#prompt) command instead.\n\n## Protocol records\n\nThe protocol has four record families:\n\n| Direction | Record | Purpose |\n|---|---|---|\n| stdin | Command | Ask Pi to prompt, inspect state, change configuration, or manage the session |\n| stdout | `response` | Report whether one command succeeded and return any command data |\n| stdout | Session event | Stream run, message, tool, queue, compaction, and retry activity |\n| Both | Extension UI record | Forward supported extension interactions between Pi and the client |\n\nSee [RPC Commands](rpc-commands.md), [JSON Event Stream](json.md), and [RPC Extension UI](rpc-extension-ui.md) for the canonical record definitions.\n\n### Correlate commands and responses\n\nEvery command accepts an optional string `id`. A matching response repeats it:\n\n```json\n{\"id\":\"req-1\",\"type\":\"get_state\"}\n{\"id\":\"req-1\",\"type\":\"response\",\"command\":\"get_state\",\"success\":true,\"data\":{\"...\":\"...\"}}\n```\n\nUse unique IDs whenever more than one command can be outstanding. Command handling is asynchronous, so clients should correlate by ID rather than response order.\n\nSession events generally have no command ID because they describe session activity. `bash_execution_update` is the exception: when the originating [`bash`](rpc-commands.md#bash) command has an ID, its output events repeat that ID.\n\nAn `extension_ui_response` uses the ID supplied by its `extension_ui_request`. It does not produce a normal command response.\n\n## Framing\n\nRPC uses strict JSONL framing. Write one complete JSON object per record and terminate it with LF (`\\n`). Read stdout as a byte or UTF-8 stream and split records only on LF. Strip an optional preceding carriage return to accept CRLF input.\n\nDo not use a generic line reader that treats Unicode line or paragraph separators as record boundaries. In particular, Node.js `readline` also splits on `U+2028` and `U+2029`, which are valid inside JSON strings.\n\nRead stdout continuously. Pi honors stdout backpressure, but a client that stops reading can stall the process. Honor stdin backpressure when writing commands. Stdout is reserved for protocol records; diagnostics and application logging go to stderr.\n\n## Run lifecycle\n\nA successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean model work completed:\n\n```json\n{\"id\":\"req-2\",\"type\":\"prompt\",\"message\":\"Review this repository\"}\n{\"id\":\"req-2\",\"type\":\"response\",\"command\":\"prompt\",\"success\":true,\"data\":{\"disposition\":\"started\"}}\n```\n\n`data.disposition` reports what happened to the prompt. If it is `\"handled\"`, no run started for this prompt, so don't wait for `agent_settled`. See [RPC Commands](rpc-commands.md#prompt) for all values.\n\nContinue consuming [events](json.md) after that response. `agent_end` marks the end of one low-level agent run, but retries, overflow recovery, compaction, steering, or follow-up work can still follow. Wait for `agent_settled` when the client needs to know Pi will not continue automatically.\n\nSubscribe before sending a prompt to avoid missing a fast completion. `RpcClient.promptAndWait()` does this internally. If using separate `RpcClient` calls, install the event listener before `prompt()` and call `waitForIdle()` only while a run is active.\n\n## Errors\n\nA failed command returns one response with `success: false`:\n\n```json\n{\"id\":\"req-3\",\"type\":\"response\",\"command\":\"set_model\",\"success\":false,\"error\":\"Model not found: invalid/model\"}\n```\n\nMalformed JSON produces a parse response without a request ID:\n\n```json\n{\"type\":\"response\",\"command\":\"parse\",\"success\":false,\"error\":\"Failed to parse command: Unexpected token...\"}\n```\n\nA success response only covers command handling. Provider failures and aborts after a prompt is accepted appear in the message and event stream.\n\nClients must also handle child-process startup failures, unexpected exits, stderr diagnostics, cancellation, and their own deadlines. Do not parse stderr as protocol data.\n\n## Shutdown\n\nClose the child's stdin to request an orderly shutdown. Pi disposes the active runtime before exiting. Clients should still handle process signals and unexpected exits.\n\nAn extension can also request shutdown through its extension context. Pi completes shutdown after the current command or after the active run emits `agent_settled`.\n\n## Minimal client\n\nThis Python example uses a binary pipe reader, which splits on LF without treating Unicode separators as protocol boundaries:\n\n```python\nimport json\nimport subprocess\n\nprocess = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n)\n\nassert process.stdin is not None\nassert process.stdout is not None\n\ncommand = {\"id\": \"prompt-1\", \"type\": \"prompt\", \"message\": \"Hello\"}\nprocess.stdin.write(json.dumps(command).encode(\"utf-8\") + b\"\\n\")\nprocess.stdin.flush()\n\nwhile line := process.stdout.readline():\n    record = json.loads(line)\n    if record.get(\"type\") == \"message_update\":\n        update = record[\"assistantMessageEvent\"]\n        if update[\"type\"] == \"text_delta\":\n            print(update[\"delta\"], end=\"\", flush=True)\n    elif record.get(\"type\") == \"agent_settled\":\n        print()\n        break\n\nprocess.stdin.close()\nprocess.wait()\n```\n\nFor maintained TypeScript clients, use the checked [RPC client example](../examples/rpc-client.ts). It requires a built Pi CLI because the repository example points to `dist/cli.js`.\n\n## Reference\n\n- [RPC Commands](rpc-commands.md): every stdin command and response\n- [JSON Event Stream](json.md): shared stdout session events and streaming reconstruction\n- [RPC Extension UI](rpc-extension-ui.md): dialogs, notifications, responses, and limitations\n- [Message Types](message-types.md): messages and content blocks used by responses and events\n- [Session File Format](session-format.md): entries returned by session commands\n- [`rpc-types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-types.ts): exported TypeScript protocol definitions\n- [`RpcClient`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-client.ts): subprocess client implementation\n\n## Moved reference anchors\n\nThe detailed references formerly on this page now have dedicated pages. These anchors preserve existing links.\n\n<a id=\"prompt\"></a>\n<a id=\"steer\"></a>\n<a id=\"follow_up\"></a>\n<a id=\"abort\"></a>\n<a id=\"clear_queue\"></a>\n<a id=\"new_session\"></a>\n<a id=\"get_state\"></a>\n<a id=\"get_messages\"></a>\n<a id=\"set_model\"></a>\n<a id=\"cycle_model\"></a>\n<a id=\"get_available_models\"></a>\n<a id=\"set_thinking_level\"></a>\n<a id=\"cycle_thinking_level\"></a>\n<a id=\"get_available_thinking_levels\"></a>\n<a id=\"set_steering_mode\"></a>\n<a id=\"set_follow_up_mode\"></a>\n<a id=\"compact\"></a>\n<a id=\"set_auto_compaction\"></a>\n<a id=\"set_auto_retry\"></a>\n<a id=\"abort_retry\"></a>\n<a id=\"bash\"></a>\n<a id=\"abort_bash\"></a>\n<a id=\"get_session_stats\"></a>\n<a id=\"export_html\"></a>\n<a id=\"switch_session\"></a>\n<a id=\"fork\"></a>\n<a id=\"clone\"></a>\n<a id=\"get_fork_messages\"></a>\n<a id=\"get_entries\"></a>\n<a id=\"get_tree\"></a>\n<a id=\"get_last_assistant_text\"></a>\n<a id=\"set_session_name\"></a>\n<a id=\"get_commands\"></a>\n\nCommand details moved to [RPC Commands](rpc-commands.md).\n\n<a id=\"message_update-streaming\"></a>\n<a id=\"bash_execution_update\"></a>\n<a id=\"compaction_start--compaction_end\"></a>\n<a id=\"summarization_retry_scheduled--summarization_retry_attempt_start--summarization_retry_finished\"></a>\n\nEvent details moved to [JSON Event Stream](json.md).\n\n<a id=\"extension-ui-protocol\"></a>\n\nExtension interaction details moved to [RPC Extension UI](rpc-extension-ui.md).\n","isInternal":false,"tokens":2018,"sizeBytes":8897},{"name":"sdk.md","path":"packages/coding-agent/docs/sdk.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/sdk.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# SDK\n\n`@earendil-works/pi-coding-agent` embeds Pi in a Node.js or Bun process. It provides direct TypeScript access to the agent, sessions, tools, models, and resources used by the command-line application.\n\nUse the SDK for in-process TypeScript integration. For a language-independent or isolated subprocess, see [CLI Integration](cli-integration.md).\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\nconst { session } = await createAgentSession();\n\ntry {\n  await session.prompt(\"What files are in the current directory?\");\n  console.log(session.getLastAssistantText());\n} finally {\n  session.dispose();\n}\n```\n\nThis uses the working directory, discovered resources, stored settings, and configured credentials. `prompt()` resolves when the run finishes.\n\nThe [complete minimal example](../examples/sdk/01-minimal.ts) also streams text events. All [SDK examples](../examples/sdk/) are typechecked with the repository.\n\n<a id=\"session-management\"></a>\n\n## Session lifecycle\n\n`createAgentSession()` creates an `AgentSession`. The session owns one conversation, its model and tools, queued messages, compaction state, and extension runtime.\n\nRead current state through `session.messages`, `session.model`, `session.thinkingLevel`, `session.systemPrompt`, and `session.getActiveToolNames()`.\n\n`session.systemPrompt` is read-only and returns the current effective system prompt, including changes that have not yet been sent to the model. Tool changes are declared to the model before the next request.\n\n<a id=\"sessionmanager-api\"></a>\n\n### Session storage\n\nSessions are persistent by default. `SessionManager` owns the persisted or in-memory entry tree and tracks its active leaf. Branching changes that leaf without deleting abandoned branches. When Pi reconstructs model context, the manager selects the active branch and applies compaction.\n\n`SessionManager` is authoritative for finalized model context. Restore external history by constructing the session with a manager containing those entries. Assigning `session.agent.state.messages` does not replace persisted context.\n\nUse an in-memory manager when the host does not want session files:\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\nSee the checked [sessions example](../examples/sdk/11-sessions.ts) for creating, opening, continuing, listing, and forking sessions. [Session File Format](session-format.md) defines the persisted JSONL contract, and [Message Types](message-types.md) defines transcript values. For exact methods and signatures, use the exported TypeScript declarations or [`session-manager.ts`](../src/core/session-manager.ts).\n\n`cwd` selects the workspace used for project resource discovery, context files, session grouping, and built-in tool paths. Pass it explicitly when the target differs from `process.cwd()`.\n\n`session.dispose()` aborts active work, invalidates extension contexts, disconnects from the agent, and removes event listeners. Call it when the session is no longer needed.\n\n`AgentSessionRuntime` adds `newSession()`, `switchSession()`, `fork()`, and `importFromJsonl()`. Each operation replaces the active `AgentSession` and recreates services for the target working directory.\n\nAfter a runtime replacement, subscriptions belong to the old `AgentSession` and must be rebound. See the [session runtime example](../examples/sdk/13-session-runtime.ts).\n\n## Prompting\n\n`prompt()` handles extension commands and expands file-based prompt templates before ordinary user messages enter the agent. For an accepted agent run, it resolves after the run finishes, including automatic retries.\n\nA prompt sent while the session is already streaming must specify whether it should steer the current run or follow it. Calling `prompt()` without that choice rejects rather than guessing.\n\nA steering message enters after the current assistant turn and its tool calls. A follow-up enters after the current run finishes its pending work. `steer()` and `followUp()` expose those behaviors directly and return `\"queued\"` if the input was queued (including after an extension transformed it), or `\"handled\"` if an extension consumed it.\n\n`abort()` stops the active operation and waits for the session to become idle. `waitForIdle()` waits without aborting it.\n\n## Subscribing to events\n\nSubscribe before prompting when the host needs streamed output:\n\n```typescript\nconst unsubscribe = session.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\ntry {\n  await session.prompt(\"Explain this repository\");\n} finally {\n  unsubscribe();\n}\n```\n\nSession events report message updates, tool execution, queues, compaction, retries, and run lifecycle changes.\n\n`message_end` contains the authoritative completed message. `agent_end` marks the end of one low-level agent run, but automatic recovery or queued work can still follow.\n\nUse `agent_settled` when the host needs to know that Pi will not continue automatically.\n\n## Configuring a session\n\nWithout overrides, the factory creates a `ModelRuntime`, file-backed `SettingsManager`, persistent `SessionManager`, `DefaultResourceLoader`, and the configured default tools.\n\nEach boundary can be supplied explicitly:\n\n- `modelRuntime`, `model`, `thinkingLevel`, and `scopedModels` control model access and selection.\n- `settingsManager` supplies merged settings or an in-memory configuration.\n- `sessionManager` supplies persistent or in-memory conversation history.\n- `resourceLoader` supplies extensions, skills, prompt templates, themes, and context files.\n- `tools`, `noTools`, `excludeTools`, and `customTools` control the active tool set.\n\nUse `DefaultResourceLoader` when you want standard discovery with selected overrides. Supply a custom `ResourceLoader` when the host owns resource storage and discovery completely.\n\n<a id=\"inlineextension\"></a>\n\nInline extension factories can be supplied through `DefaultResourceLoader`. Give one an `InlineExtension` name only when it needs a stable name in diagnostics and startup output. A named inline extension with `replaceable: true` is left out when another extension registers a tool, command, or flag with a name it registers during loading, instead of both loading with a conflict. The CLI's built-in codemode, tool search, and MCP extensions are replaceable. A named entry with `builtin: true` is not an inline extension: it supplies the code of the `builtin:<name>` extension, which loads like a configured extension file. It loads by default, is listed in `pi config`, and is disabled by `-builtin:<name>` in the `extensions` setting or by `noExtensions`; `additionalExtensionPaths: [\"builtin:<name>\"]` loads it explicitly. It loads after project trust is resolved, so it cannot handle `project_trust`. The CLI's built-in extensions use it.\n\n<a id=\"codemode-mcp\"></a>\n\nThe CLI loads `codemode`, `tool_search`, and MCP as built-in extensions. SDK sessions do not; add `createCodemodeExtension()`, `createToolSearchExtension()`, and `createMcpExtension()` to the `extensionFactories` of `DefaultResourceLoader`. `codemode` and `tool_search` are registered inactive: enable them through the `defaultTools` setting (`[\"+codemode\", \"+tool_search\"]` keeps the other default tools), or let the MCP extension activate them: `codemode` for servers with `codemode` exposure, `tool_search` for servers with `deferred` exposure. The MCP extension connects its servers on `session_start`, so call `session.bindExtensions()`. See [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts).\n\nSee the focused examples for [models](../examples/sdk/02-custom-model.ts), [tools](../examples/sdk/05-tools.ts), [extensions](../examples/sdk/06-extensions.ts), and [full control](../examples/sdk/12-full-control.ts).\n\n## Examples\n\n| Example | Purpose |\n|---|---|\n| [Minimal](../examples/sdk/01-minimal.ts) | Create, prompt, observe, and dispose a session |\n| [Custom model](../examples/sdk/02-custom-model.ts) | Select a model and thinking level |\n| [System prompt](../examples/sdk/03-custom-prompt.ts) | Replace or append to the system prompt |\n| [Skills](../examples/sdk/04-skills.ts) | Discover, filter, and add skills |\n| [Tools](../examples/sdk/05-tools.ts) | Select built-in tools and their working directory |\n| [Extensions](../examples/sdk/06-extensions.ts) | Load file-based and inline extensions |\n| [Context files](../examples/sdk/07-context-files.ts) | Add or replace project instructions |\n| [Prompt templates](../examples/sdk/08-prompt-templates.ts) | Add file-style prompt templates |\n| [Credentials](../examples/sdk/09-api-keys-and-oauth.ts) | Configure credential and model storage |\n| [Settings](../examples/sdk/10-settings.ts) | Supply file-backed or in-memory settings |\n| [Sessions](../examples/sdk/11-sessions.ts) | Control session persistence and restoration |\n| [Full control](../examples/sdk/12-full-control.ts) | Replace default discovery and state services |\n| [Session runtime](../examples/sdk/13-session-runtime.ts) | Replace the active session safely |\n| [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts) | Add the `codemode`, `tool_search`, and MCP extensions |\n\n<a id=\"exports\"></a>\n\n## Resources\n\n- [Choose a Model](models.md) covers model selection and compatible endpoints; [Providers](providers.md) covers credentials and provider-specific setup.\n- [Configuration](configuration.md) explains normal discovery and settings; [Settings](settings.md) lists every setting.\n- [Sessions and Context](sessions.md) explains session behavior; [Session Format](session-format.md) defines persisted entries; [Message Types](message-types.md) defines shared transcript values.\n- [Extensions](extensions.md), [Skills](skills.md), and [Prompt Templates](prompt-templates.md) document resources supplied through a `ResourceLoader`.\n- [CLI Integration](cli-integration.md) covers print, JSON, and RPC alternatives to an in-process SDK integration.\n","isInternal":false,"tokens":2155,"sizeBytes":10147},{"name":"security.md","path":"packages/coding-agent/docs/security.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/security.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Run Pi safely\n\nTreat model-generated commands and code as untrusted. Pi can read, change, and execute files with the permissions of the account that started it, and it does not ask for approval before every tool call. Extensions, package installers, language servers, and other child processes run with those same permissions unless an operating-system or virtualization boundary restricts them.\n\nFiles, comments, instructions, command output, and model responses can steer the model through prompt injection. Project trust controls which project resources load at startup, but it does not make that content or the resulting actions safe.\n\nSafety comes from limiting the files, credentials, processes, and network services Pi can access and affect if a generated action is wrong or hostile. Watching the transcript, using project trust, and reviewing changes do not create a security boundary.\n\n## Choose how to run Pi\n\nDifferent ways of running Pi place different limits on what generated commands can access:\n\n| How Pi runs | What remains protected |\n|---|---|\n| Directly, with the permissions of its operating-system user | Anything that user cannot access. A dedicated user account can narrow those permissions, but Pi still shares the operating system and network with other users. |\n| Entirely inside a container, virtual machine, or sandbox | Host files and processes that you do not expose to the environment. Credentials and network services remain accessible if you make them available inside it. This is usually the strongest practical option. |\n| Outside the isolated environment, with only its built-in tools running inside | Host resources are protected from actions performed through those tools. Pi itself and other extensions remain outside the boundary, so this is a narrower form of isolation. |\n\nThe working folder controls resource discovery and the default location for tools, but it does not prevent commands from accessing other paths available to the Pi process.\n\nWhichever option you choose, only provide the files and services required for the task. Keep credentials outside the environment where possible, or use narrowly scoped, short-lived credentials. Restrict network access when commands do not need it.\n\nFor setup instructions and the limitations of each isolation method, see [Run Pi in an isolated environment](containerization.md).\n\n<a id=\"project-trust\"></a>\n\n## Understand project trust\n\nProject trust controls whether Pi loads most settings and resources supplied by a working folder. It prevents a folder from silently loading executable extensions before you approve it.\n\nProject trust is not a complete startup boundary. Pi reads the project `sessionDir` setting while selecting or creating a session, before it resolves project trust. Declining trust prevents the remaining project settings and protected resources from loading, but it cannot undo that initial session-directory lookup.\n\nProject trust does not limit what tool calls can access or affect. After Pi starts, enabled tools still use the operating-system permissions of the Pi process. Instructions and other content in the folder can also influence the model.\n\n### Resources protected by project trust\n\nPi requires a project-trust decision when it finds any of these resources from the current working directory:\n\n- `.pi/settings.json`\n- `.pi/mcp.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts`, or `.pi/themes`\n- `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md`\n- project `.agents/skills` in the current directory or an ancestor directory\n\nA bare `.pi` directory does not require project trust.\n\nGranting project trust allows Pi to load:\n\n- project settings\n- project MCP servers from `.pi/mcp.json`\n- extensions, skills, prompt templates, themes, and system-prompt files under `.pi`\n- missing packages configured through project settings\n- project-local and project-package extensions\n\nDeclining project trust skips those protected resources, except for the initial `sessionDir` lookup described above.\n\nContext files such as `AGENTS.override.md`, `AGENTS.md`, and `CLAUDE.md` load regardless of project trust unless you disable context loading. Treat instructions in a folder as untrusted input even when you decline project trust.\n\n### How Pi chooses a trust decision\n\nA command-line `--approve` or `--no-approve` override applies first. When protected resources exist and there is no command-line override:\n\n1. User-level and command-line extensions can handle the `project_trust` event. The first extension that returns yes or no owns the decision.\n2. If no extension decides, Pi looks for a saved decision for the current directory or one of its parents. The closest decision applies.\n3. If no saved decision applies, Pi follows the global `defaultProjectTrust` setting, whose default is `\"ask\"`.\n\nSaved decisions use canonical directory paths and live in:\n\n```text\n~/.pi/agent/trust.json\n```\n\nUse `/trust` to save a decision for future Pi processes.\n\n### Project trust without an interactive prompt\n\nPrint, JSON, and RPC modes cannot show the built-in trust prompt. If no command-line override, extension, or saved decision applies:\n\n- `defaultProjectTrust: \"always\"` loads protected project resources.\n- `defaultProjectTrust: \"ask\"` or `\"never\"` skips them.\n\nUse `--approve` or `--no-approve` when an automated run needs an explicit one-time decision.\n\n## Reduce impact and improve recovery\n\nThese practices do not replace isolation, but they reduce exposure or make recovery easier:\n\n- Give Pi access only to files and services required for the task.\n- Use snapshots, backups, or version control before substantial changes.\n- Review extensions and packages before loading them. Extensions execute inside the Pi process.\n- Prefer narrowly scoped, short-lived credentials.\n- Review diffs and generated output before applying results to another system.\n- Review sessions before exporting or sharing them. They can contain prompts, tool arguments, command output, file contents, and credentials exposed during the conversation.\n\n## Report a security issue\n\nFollow the repository [Security Policy](https://github.com/earendil-works/pi/blob/main/SECURITY.md). Do not open a public issue for a security-sensitive report.\n\nExpected local-agent behavior, prompt injection from untrusted content, lack of a built-in sandbox, and behavior from user-installed extensions or skills are generally outside the security boundary unless the report demonstrates a privilege-boundary bypass or access that the local user did not already have.\n","isInternal":false,"tokens":1274,"sizeBytes":6542},{"name":"session-format.md","path":"packages/coding-agent/docs/session-format.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/session-format.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Session File Format\n\nSessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field. Session entries form a tree structure via `id`/`parentId` fields, enabling in-place branching without creating new files.\n\nFor programmatic creation, persistence, and tree navigation, see the [`SessionManager` API](sdk.md#sessionmanager-api).\n\n\n## File Location\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<session-id>.jsonl\n```\n\nBy default, `<session-id>` is a UUID. Callers can supply a custom ID through the SDK or `--session-id`. For `<path>`, Pi removes the leading path separator and replaces `/`, `\\\\`, and `:` with `-`.\n\n## Deleting Sessions\n\nSessions can be removed by deleting their `.jsonl` files under `~/.pi/agent/sessions/`.\n\nPi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.\n\n## Session Version\n\nSessions have a version field in the header:\n\n- **Version 1**: Linear entry sequence (legacy, auto-migrated on load)\n- **Version 2**: Tree structure with `id`/`parentId` linking\n- **Version 3**: Renamed `hookMessage` role to `custom` (extensions unification)\n\nExisting sessions are automatically migrated to the current version (v3) when loaded.\n\n## Source Files\n\nSource on GitHub ([pi](https://github.com/earendil-works/pi)):\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts) - Session entry types and SessionManager\n- [Message Types](message-types.md) - Shared message and content-block reference\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/messages.ts) - Extended message types\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/ai/src/types.ts) - Base message and content-block types\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts) - Extensible `AgentMessage` union\n\nFor TypeScript definitions in your project, inspect `node_modules/@earendil-works/pi-coding-agent/dist/` and `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Messages\n\nA `message` entry stores an [`AgentMessage`](message-types.md). Message content blocks, roles, usage, and message timestamps are defined in [Message Types](message-types.md).\n\nSession entry timestamps are ISO 8601 strings. The nested message timestamp is a Unix timestamp in milliseconds.\n\n## Entry Base\n\nAll entries (except `SessionHeader`) extend `SessionEntryBase`:\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // Usually an 8-char hex ID; may fall back to a full UUID\n  parentId: string | null;  // Parent entry ID (null for a root entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## Entry Types\n\n### SessionHeader\n\nFirst line of the file. Metadata only, not part of the tree (no `id`/`parentId`).\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\nFor sessions with a parent (created via `/fork`, `/clone`, or `newSession({ parentSession })`):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### SessionMessageEntry\n\nA message in the conversation. The `message` field contains an `AgentMessage`. System messages carry the prompt and tool loadout: the first request of a session persists one with every prompt section and tool declaration, and later changes persist as system messages that patch `sections` by name (`null` removes one) and list `toolsAdded`/`toolsRemoved`. Replaying them in order yields the current prompt and tools; there is no separate prompt state entry.\n\n```json\n{\"type\":\"message\",\"id\":\"a0b1c2d3\",\"parentId\":null,\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"message\":{\"role\":\"system\",\"content\":\"\",\"sections\":{\"preamble\":\"You are an expert coding assistant...\",\"tools\":\"<tools>\\n- read: ...\\n</tools>\",\"cwd\":\"/project\"},\"toolsAdded\":[{\"name\":\"read\",\"description\":\"...\",\"parameters\":{}}],\"timestamp\":1733234400000}}\n{\"type\":\"message\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:04:00.000Z\",\"message\":{\"role\":\"system\",\"content\":\"\",\"sections\":{\"skills\":\"<skills>...</skills>\"},\"toolsRemoved\":[{\"name\":\"write\"}],\"timestamp\":1733234640000}}\n```\n\nSessions created before system messages existed have no leading system message; the first request declares the current prompt as a later system message, which replays the same way.\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\",\"timestamp\":1733234401000}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"api\":\"anthropic-messages\",\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\",\"timestamp\":1733234402000}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false,\"timestamp\":1733234403000}}\n```\n\nAssistant messages name the model that produced them. Newer messages also record `thinkingLevel`, the Pi thinking level requested for that response.\n\n### ModelChangeEntry\n\nEmitted when the user switches models mid-session. The latest entry is the selected model, which may be a [virtual model](virtual-models.md); assistant messages then name the physical model that answered.\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### ThinkingLevelChangeEntry\n\nEmitted when the user changes the thinking/reasoning level.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### UsageEntry\n\nRecords model-attributed usage that is not an assistant message and does not participate in LLM context. `kind` is an arbitrary string identifying the operation; for example, cache warming uses `\"cache_warm\"`.\n\n```json\n{\"type\":\"usage\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:08:00.000Z\",\"kind\":\"cache_warm\",\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{\"input\":0,\"output\":0,\"cacheRead\":50000,\"cacheWrite\":0,\"totalTokens\":50000,\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0.015,\"cacheWrite\":0,\"total\":0.015}}}\n```\n\nUsage entries contribute to session token and cost totals. Pi hides them from the conversation tree. Consumers should treat unknown `kind` values as normal usage rather than rejecting them.\n\n### CompactionEntry\n\nCreated when context is compacted. Stores a summary of earlier messages and a complete system prompt/tool checkpoint.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000,\"systemMessage\":{\"role\":\"system\",\"content\":\"You are a coding assistant.\",\"toolsAdded\":[],\"timestamp\":1733235000000}}\n```\n\n`firstKeptEntryId` is required. It identifies the first entry retained from before the compaction entry. When rebuilding context, Pi replaces older summarized entries with the compaction summary and keeps the range beginning at this entry. A retain-none compaction stores its own ID in this field, so no preceding entries are retained.\n\nOptional fields:\n- `systemMessage`: The replayed prompt sections and tool declarations at the compaction boundary; it becomes the leading system message of the compacted context, and system messages among the kept entries are dropped in its favor. It is absent on older session entries.\n- `usage`: LLM usage from generating the summary; included in session token and cost totals\n- `details`: Implementation-specific data (e.g., `{ readFiles: string[], modifiedFiles: string[] }` for default, or custom data for extensions)\n- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)\n\n### ContextEditEntry\n\nAppend-only edit of one earlier context-producing entry. It changes only future model context; the target entry and its metadata remain unchanged in raw history, UI, exports, and session accounting.\n\n```json\n{\"type\":\"context_edit\",\"id\":\"g6h7i8j9\",\"parentId\":\"f6g7h8i9\",\"timestamp\":\"2024-12-03T14:11:00.000Z\",\"targetId\":\"c3d4e5f6\",\"replacement\":null}\n```\n\nTargets may be user, assistant, tool-result, or custom-message entries. `replacement: null` omits the target from model context. A non-null `replacement` replaces only the target message content. String replacements for assistant and tool-result entries are normalized to one text block because those roles require content arrays. If several edits target the same entry, the latest edit on the active branch wins. Edits are branch-relative: navigating to a point before the edit reveals the target's original contribution again.\n\n### BranchSummaryEntry\n\nCreated when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\n`parentId` is the entry from which the new branch continues. `fromId` is the previous leaf whose abandoned path was summarized.\n\nOptional fields:\n- `usage`: LLM usage from generating the summary; included in session token and cost totals\n- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default, or custom data for extensions\n- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)\n\n### CustomEntry\n\nExtension state persistence. Does NOT participate in LLM context.\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\nUse `customType` to identify your extension's entries on reload. Interactive mode can render custom entries via `pi.registerEntryRenderer(customType, renderer)`, but they still do not participate in LLM context.\n\nPi stores [virtual model](virtual-models.md) router state as custom entries with `customType` `pi.virtual-model-state` and `data` `{ provider, modelId, state }`.\n\n### CustomMessageEntry\n\nExtension-injected messages that DO participate in LLM context.\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\nFields:\n- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)\n- `display`: `true` = show in TUI with distinct styling, `false` = hidden\n- `details`: Optional extension-specific metadata (not sent to LLM)\n\n### LabelEntry\n\nUser-defined bookmark/marker on an entry.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\nSet `label` to `undefined` to clear a label.\n\n### SessionInfoEntry\n\nSession metadata (e.g., user-defined display name). Set via `/name`, `--name` / `-n`, or `pi.setSessionName()` in extensions.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nThe session name is displayed in the session selector (`/resume`) instead of the first message when set.\n\n## Tree Structure\n\nEntries normally form one tree, but navigation APIs can create multiple roots:\n- A root entry has `parentId: null`; the first entry is initially the root\n- Each non-root entry points to its parent via `parentId`\n- Branching creates new children from an earlier entry\n- The \"leaf\" is the current position in the tree\n- Calling `resetLeaf()` or `branchWithSummary(null, ...)` allows a later entry to become another root\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Context Building\n\n`buildContextEntries()` walks from the current leaf to the root, producing the active entry list while honoring compaction:\n\n1. Collects all entries on the path\n2. If one or more `CompactionEntry` values are on the path, uses the latest one:\n   - Includes the compaction entry first\n   - Includes non-system entries from `firstKeptEntryId` up to, but not including, the compaction entry\n   - Includes entries after the compaction entry\n3. Preserves non-message entries in the selected range so interactive mode can render them\n\n`buildSessionProjection()` then applies the latest `context_edit` for each selected target. It returns the model-visible messages together with their source entries. Omitted targets produce no message; replacements retain the source entry's role and metadata while changing only content. The raw selected entries are not modified.\n\n`buildSessionContext()` builds on that projection to produce the message list for the LLM:\n\n1. Extracts current model and thinking level settings from the full path\n2. Converts selected entries to messages:\n   - `message` -> stored `AgentMessage`\n   - `compaction` -> complete system checkpoint followed by `compactionSummary`\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `context_edit` -> no context message of its own\n   - `usage` and `custom` -> no context message\n\nThe compaction summary replaces entries before `firstKeptEntryId`. Pre-compaction system messages are folded into the complete checkpoint rather than replayed from the retained range. Retained non-system entries and all entries after the compaction remain available to the LLM.\n\n## Parsing Example\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"usage\":\n      console.log(`[${entry.id}] Usage (${entry.kind}): ${entry.usage.totalTokens} tokens`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n","isInternal":false,"tokens":3973,"sizeBytes":15766},{"name":"sessions.md","path":"packages/coding-agent/docs/sessions.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/sessions.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Sessions and Context\n\nPi saves a conversation as a session. The active branch of that session supplies conversation history for the next model request. Use session commands to continue work, explore another branch, or reduce the amount of history sent to the model.\n\n## Continue or switch sessions\n\nPi saves sessions automatically unless you start it with `--no-session`.\n\n```bash\npi --continue\npi --resume\n```\n\n`--continue` opens the most recent session for the current working directory. `--resume` opens the session picker. In interactive mode, `/resume` opens the same picker and `/new` starts a new session.\n\nUse `/name` or `--name` to assign a recognizable session name. Run `/session` to verify the current session file, ID, message count, token usage, and cost.\n\nThe session picker lets you search, rename, and delete sessions. It can also show paths, change sorting, and limit results to named sessions. See [Keybindings](keybindings.md#sessions) for its shortcuts.\n\n## Choose how to branch\n\nPi stores entries as a tree, so returning to an earlier point does not erase the branch you leave.\n\n| Action | Result | Use it when |\n|---|---|---|\n| `/tree` | Moves within the current session file | Related alternatives should stay together |\n| `/fork` | Creates a new session from an earlier user message | The alternative should become separate work |\n| `/clone` | Copies the active branch into a new session | You want a separate copy of the current state |\n\nIn `/tree`, select a user message to put its text back in the editor. Edit and submit it to create another branch. Selecting an assistant response or another entry continues after that entry with an empty editor.\n\nWhen you leave a branch, Pi can summarize it and attach that summary to the branch you enter. This preserves relevant work from the abandoned path without including every message from it.\n\nFor the persisted tree and entry types, see [Session Format](session-format.md).\n\n## Manage conversation context\n\nThe model receives the active branch, not every branch in the session file. Pi combines that history with the system prompt, discovered context files, available tools, and loaded skill descriptions. [How Pi Works](how-pi-works.md#context) describes how those inputs are assembled.\n\nThe footer shows current context usage. When the active context approaches the model's limit, Pi normally compacts older history automatically. Compaction adds a summary and keeps recent messages. It does not delete the original session entries.\n\nRun `/compact` to compact manually. You can add instructions when the summary should preserve a particular topic or decision. Configure automatic compaction and retained history through [Settings](settings.md#compaction).\n\nCompaction can fail if the provider is unavailable or cannot accept the summarization request. Correct the provider problem and run `/compact` again. Disabling automatic compaction does not disable the manual command.\n\nSee [Compaction Reference](compaction.md) for thresholds, retained boundaries, branch-summary behavior, and extension hooks.\n\n## Control session storage\n\nBy default, Pi stores sessions under `~/.pi/agent/sessions/`, grouped by working directory. Use `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, or the `sessionDir` setting to choose another location. The CLI option has highest precedence.\n\nUse `--no-session` for an ephemeral run. An ephemeral session cannot be resumed after Pi exits.\n\nUse `--session` when you already know the session path or ID. Use `--fork` to create a new session from an existing session before interactive mode starts.\n\n## Export or share a session\n\nUse `/export` to write the current session as HTML or JSONL. Use `/share` to upload it and get a viewer link. By default, Pi uses a Radius artifact when Radius authentication is configured; otherwise, it uses a private GitHub gist. Set [`sessionShareDestination`](settings.md#sessions-and-context) to `gist` or `radius` to force one destination.\n\nReview exported or shared sessions first. They can contain prompts, model responses, tool arguments, command output, file contents, and extension messages.\n\n## Report a bug\n\nRun `/bug [description]` to prepare a private report for the Pi developers. You can include the session transcript, omit it, or ask the current model to summarize the problem. Review any transcript or generated summary because it can contain sensitive conversation data.\n\nThe report includes environment and provider configuration without credential values, plus recorded error diagnostics. Upload it through `radius.pi.dev` or export the same report as a zip to inspect and share yourself. Uploads do not require a login; Radius authentication attributes the report to your account so the developers can follow up. If an upload fails, Pi offers to export the zip.\n","isInternal":false,"tokens":979,"sizeBytes":4818},{"name":"settings.md","path":"packages/coding-agent/docs/settings.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/settings.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Settings Reference\n\nThis reference lists user-configurable settings, their types, defaults, and purposes. Project settings override agent-directory settings. Resource lists are combined. See [Configuration](configuration.md) for file locations and trust behavior.\n\nAdd `\"$schema\": \"https://pi.dev/schemas/settings.schema.json\"` to `settings.json` for editor validation and completion.\n\n## Model and thinking\n\n<a id=\"model-cycling\"></a>\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `defaultProvider` | string | Automatic | Startup AI provider. |\n| `defaultModel` | string | Automatic | Startup model ID. |\n| `defaultThinkingLevel` | `\"off\" \\| \"minimal\" \\| \"low\" \\| \"medium\" \\| \"high\" \\| \"xhigh\" \\| \"max\"` | `\"medium\"` | Startup thinking level. |\n| `modelThinkingLevels` | object | None | Per-model startup thinking levels keyed by exact `provider/modelId`. |\n| `thinkingBudgets` | object | Built-in budgets | Token budgets for `minimal`, `low`, `medium`, and `high` thinking levels. |\n| `enabledModels` | `string[]` | All available models | Model patterns used for startup selection and model cycling. Uses the same format as `--models`. |\n| `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in the transcript. |\n| `showCacheMissNotices` | boolean | `false` | Show notices for significant cache misses, successful cache warming, compaction usage, and provider recovery. |\n| `cacheWarming` | `\"off\" \\| \"streaming\" \\| \"idle\"` | `\"streaming\"` | Keep eligible provider prompt caches warm during active runs or, with `\"idle\"`, between runs. Global setting only. |\n\nCache warming runs only when the model declares a cache lifetime and Pi estimates at least $0.05 in avoided cache-miss cost. Refresh usage counts toward session totals but does not enter model context. `/session` shows the next decision; extensions can override it with `cache_warming_decision`. See [Prompt Cache Lifetimes](models.md#prompt-cache-lifetimes).\n\nSee [Choose a Model](models.md) for model selection and thinking controls.\n\n## Interaction\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `steeringMode` | `\"all\" \\| \"one-at-a-time\"` | `\"one-at-a-time\"` | How queued steering messages are delivered. |\n| `followUpMode` | `\"all\" \\| \"one-at-a-time\"` | `\"one-at-a-time\"` | How queued follow-up messages are delivered. |\n| `externalEditor` | string | `$VISUAL`, `$EDITOR`, then platform default | Command opened by the external-editor keybinding. |\n| `doubleEscapeAction` | `\"tree\" \\| \"fork\" \\| \"none\"` | `\"tree\"` | Action for double Escape with an empty editor. |\n| `treeFilterMode` | `\"default\" \\| \"no-tools\" \\| \"user-only\" \\| \"labeled-only\" \\| \"all\"` | `\"default\"` | Initial filter used by `/tree`. |\n| `defaultProjectTrust` | `\"ask\" \\| \"always\" \\| \"never\"` | `\"ask\"` | Fallback project-trust behavior. **Can only be set in agent-directory settings.** |\n\n## Tools\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `defaultTools` | `string[]` | `read`, `bash`, `edit`, `write` | Tools enabled at startup. Plain names replace the defaults; `+name` adds a tool and `-name` removes one. An empty array disables all built-in tools but not extension or SDK tools. |\n| `codemode.mode` | `\"on\"` \\| `\"only\"` | `\"on\"` | How the `codemode` tool presents tools while it is active. `on`: declared tools get a note on calling them from scripts appended to their description, and `codemode` lists only tools that are not declared. `only`: `codemode` lists every tool scripts can call, and active built-in and extension tools are hidden from the model, so it reaches them through `codemode`. |\n| `codemode.inlineBudget` | number | `3000` | Estimated tokens (characters / 4) the `codemode` tool's description may spend on tool declarations. Tools that do not fit are left out and found with `searchTools()`. `0` lists only namespaces. |\n\nAvailable built-in tools are `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`. `defaultTools` can also name `codemode` and `tool_search`, which built-in extensions register inactive, and other extension tools registered inactive.\n\nA list of only `+name` and `-name` entries changes the inherited selection instead of replacing it. For example, this enables `codemode` next to the default tools:\n\n```json\n{\n  \"defaultTools\": [\"+codemode\"]\n}\n```\n\nThis replaces `bash` with `powershell` and enables `grep`: `[\"-bash\", \"+powershell\", \"+grep\"]`. Project settings apply on top of user settings: a project list with only `+name` and `-name` entries changes the user's selection, and a project list with a plain name replaces it. In one list, plain names form the selection, and `+name` and `-name` then apply in order.\n\n`/reload` enables tools newly added to `defaultTools`. It does not disable tools removed from it or re-enable unchanged tools you turned off. `--tools` with plain names, `--no-tools`, and `--no-builtin-tools` override `defaultTools`, also on reload.\n\nCLI tool options override this setting for one invocation. `--tools` with only `+name` and `-name` entries changes the resolved `defaultTools` selection instead, for example `pi --tools +codemode`. On `/reload`, these entries apply to the reloaded setting too, so a tool removed with `-name` stays removed. See [Command Line](cli.md#tools).\n\n## Sessions and context\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `sessionDir` | string | Agent session directory | Session storage directory. Relative paths resolve from the working directory. `PI_CODING_AGENT_SESSION_DIR` and `--session-dir` override this setting. |\n| `sessionShareDestination` | `\"auto\" \\| \"gist\" \\| \"radius\"` | `\"auto\"` | Destination used by `/share`. `auto` uses Radius when authenticated and otherwise creates a GitHub gist. `gist` always uses a gist. `radius` requires Radius authentication and never falls back to a gist. |\n\n### Compaction\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `compaction.enabled` | boolean | `true` | Enable automatic compaction. |\n| `compaction.reserveTokens` | number | `16384` | Free context tokens below which automatic compaction starts. |\n| `compaction.keepRecentTokens` | number | `20000` | Recent tokens retained without summarization. |\n| `compaction.modelOverrides` | object | None | Per-model token settings keyed by exact `provider/modelId`. |\n\n<a id=\"per-model-compaction-overrides\"></a>\n\nCompaction token values must be non-negative safe integers. Each value resolves independently from the matching model override, then the ordinary compaction setting, then the built-in default. Project and user objects merge before model lookup.\n\nSee [Compaction Reference](compaction.md) for trigger, summarization, and validation behavior.\n\n### Branch summaries\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `branchSummary.reserveTokens` | number | `16384` | Tokens reserved when summarizing branch history. |\n| `branchSummary.skipPrompt` | boolean | `false` | Skip the branch-summary prompt and default to no summary. |\n\n## Terminal and display\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `theme` | string | `\"system\"` | Built-in or custom theme name. `system` derives colors from the terminal theme. |\n| `quietStartup` | boolean \\| `\"header\"` | `false` | `true` hides the startup header and loaded-resource listing. `\"header\"` keeps the header (version and key hints) but hides the model scope line and loaded-resource listing. |\n| `tuiMode` | `\"regular\" \\| \"fullscreen\"` | `\"fullscreen\"` | Interactive terminal UI mode. |\n| `fullscreenExitOutput` | `\"transcript\" \\| \"resume-hint\"` | `\"transcript\"` | Output printed when fullscreen mode exits. |\n| `fullscreenScrollbar` | `\"auto\" \\| \"always\" \\| \"hidden\"` | `\"auto\"` | Fullscreen transcript scrollbar behavior. |\n| `fullscreenCopyOnSelect` | boolean | `true` | Copy selected text automatically in fullscreen mode. |\n| `fullscreenWheelScrollLines` | `\"auto\"` \\| number | `\"auto\"` | Lines per mouse-wheel event in fullscreen mode, from 1 to 100. `\"auto\"` moves one line per event in local macOS terminals, which already accelerate wheel and trackpad input; elsewhere, and over SSH, it speeds up fast wheel spins to at most 6 lines per event. Alt+wheel moves five times as far. |\n| `editorPaddingX` | number | `0` | Horizontal editor padding from 0 to 3 cells. |\n| `outputPad` | `0 \\| 1` | `1` | Horizontal transcript padding for messages, tool output, `!` command output, and summary blocks. |\n| `autocompleteMaxVisible` | number | `5` | Visible autocomplete entries, from 3 to 20. |\n| `showHardwareCursor` | boolean | `false` | Use the terminal cursor instead of Pi's drawn cursor. Pi still positions it for input methods. |\n| `terminal.showImages` | boolean | `true` | Display inline images when supported. |\n| `terminal.imageWidthCells` | number | `60` | Preferred inline image width in terminal cells. |\n| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when rendered content shrinks. |\n| `terminal.showTerminalProgress` | boolean | `false` | Show OSC 9;4 progress in the terminal tab. |\n| `terminal.hyperlinks` | `boolean \\| \"auto\"` | `\"auto\"` | Override OSC 8 hyperlink detection. |\n| `terminal.images` | `\"kitty\" \\| \"iterm2\" \\| \"auto\" \\| false` | `\"auto\"` | Override inline-image protocol detection. |\n| `terminal.trueColor` | `boolean \\| \"auto\"` | `\"auto\"` | Override true-color detection. |\n| `images.autoResize` | boolean | `true` | Resize images to at most 2000 by 2000 pixels before sending them to a model. |\n| `images.blockImages` | boolean | `false` | Prevent images from being sent to models. |\n| `markdown.codeBlockIndent` | string | `\"  \"` | Prefix used to indent rendered code blocks. |\n| `markdown.mermaid` | `\"off\" \\| \"final\" \\| \"streaming\"` | `\"streaming\"` | Mermaid rendering mode. |\n\nSee [Themes](themes.md) and [Terminal Setup](terminal-setup.md) for format and platform details.\n\n## Network and retries\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `transport` | `\"auto\" \\| \"sse\" \\| \"websocket\" \\| \"websocket-cached\"` | `\"auto\"` | Preferred transport for AI providers that support multiple transports. |\n| `httpProxy` | string | None | Proxy URL applied as `HTTP_PROXY` and `HTTPS_PROXY` for Pi-managed HTTP clients. **Can only be set in agent-directory settings.** |\n| `httpIdleTimeoutMs` | number | `300000` | HTTP header and body idle timeout in milliseconds. Set to `0` to disable. |\n| `websocketConnectTimeoutMs` | number | `15000` | WebSocket connection timeout in milliseconds. Set to `0` to disable. |\n| `retry.enabled` | boolean | `true` | Enable automatic agent-level retry for transient failures. |\n| `retry.maxRetries` | number | `3` | Maximum agent-level retry attempts. |\n| `retry.baseDelayMs` | number | `2000` | Initial exponential-backoff delay in milliseconds. |\n| `retry.maxAgentDelayMs` | number | `60000` | Maximum agent-level retry delay in milliseconds. |\n| `retry.provider.timeoutMs` | number | `httpIdleTimeoutMs` | Provider request timeout in milliseconds. |\n| `retry.provider.maxRetries` | number | `0` | Provider-level retry attempts. |\n| `retry.provider.maxRetryDelayMs` | number | `60000` | Maximum server-requested delay in milliseconds. Set to `0` to disable the limit. |\n\nKeep `retry.provider.maxRetries` at `0` unless provider-level retries are required. Provider retries can delay Pi from handling quota and usage-limit errors itself.\n\n## Shell\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `shellPath` | string | Platform default | Custom shell executable path. Supports a leading `~`. |\n| `shellCommandPrefix` | string | None | Prefix prepended to every shell command. |\n| `npmCommand` | `string[]` | `npm` | Command and arguments used for npm package lookup and installation. |\n\nSee [Shell aliases](shell-aliases.md) for shell setup and [Pi Packages](packages.md) for package-manager behavior.\n\n## Resources\n\nResource paths in user settings resolve from the agent directory. Paths in project settings resolve from the project `.pi` directory. Absolute paths and `~` are supported.\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `packages` | array | `[]` | npm, git, or local Pi package sources. See [Pi Packages](packages.md). |\n| `extensions` | `string[]` | `[]` | Extension files or directories. |\n| `skills` | `string[]` | `[]` | Skill files or directories. |\n| `prompts` | `string[]` | `[]` | Prompt-template files or directories. |\n| `themes` | `string[]` | `[]` | Theme files or directories. |\n| `enableSkillCommands` | boolean | `true` | Register skills as `/skill:name` commands. |\n\nResource arrays support glob exclusions with `!pattern`, exact inclusion with `+path`, and exact exclusion with `-path`. Pi loads resources listed in both user-level and project settings.\n\nThe built-in extensions are named `builtin:mcp`, `builtin:llama.cpp`, `builtin:codemode`, and `builtin:tool-search` in `extensions`. They load by default; `-builtin:mcp` disables one. A `+builtin:<name>` or `-builtin:<name>` entry in project settings overrides the user setting. `pi config` lists them under Built-in. `--no-extensions` disables them too, and `-e builtin:<name>` loads one explicitly.\n\n## Updates, telemetry, and warnings\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `collapseChangelog` | boolean | `false` | Show a condensed changelog after an update. |\n| `enableInstallTelemetry` | boolean | `true` | Enable anonymous install/update reporting and selected provider attribution headers. Does not control update checks. |\n| `enableAnalytics` | boolean | `false` | Opt in to analytics data sharing. Currently used only by the experimental first-run setup. |\n| `warnings.anthropicExtraUsage` | boolean | `true` | Warn when Anthropic subscription authentication may use paid extra usage. |\n","isInternal":false,"tokens":3383,"sizeBytes":13799},{"name":"shell-aliases.md","path":"packages/coding-agent/docs/shell-aliases.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/shell-aliases.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Configure shell commands\n\nPi starts a separate non-interactive shell process for each Bash command. Non-interactive Bash does not expand aliases by default and usually does not load the same startup files as an interactive terminal.\n\nUse `shellPath` to choose the Bash executable and `shellCommandPrefix` to run setup before each command.\n\n## Understand which shell Pi uses\n\n| Command source | Shell |\n|---|---|\n| Model calls the built-in `bash` tool | Pi's resolved Bash executable |\n| You enter `!command` or `!!command` | The same resolved Bash executable |\n| Model calls the optional `powershell` tool | PowerShell 7 (`pwsh.exe`) or Windows PowerShell |\n| An extension provides or replaces a shell tool | The operations implemented by that extension |\n\nPi normally invokes Bash with `bash -c`. On Unix systems, it uses `/bin/bash`, then `bash` on `PATH`, and finally `sh` when Bash is unavailable. Native Windows first checks the configured path, then Git Bash, then `bash.exe` on `PATH`.\n\n## Choose a Bash executable\n\nSet `shellPath` in `~/.pi/agent/settings.json` when Pi should use a specific executable:\n\n```json\n{\n  \"shellPath\": \"~/.local/bin/bash\"\n}\n```\n\nOn Windows, use forward slashes or escape backslashes:\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```\n\nRun `/reload` after changing the setting. See [Run Pi on Windows](windows.md) for the native Windows defaults.\n\n## Run setup before every Bash command\n\nSet `shellCommandPrefix` to prepend shell setup to both the built-in `bash` tool and user-entered `!` or `!!` commands:\n\n```json\n{\n  \"shellCommandPrefix\": \"export CI=1\"\n}\n```\n\nPi joins the prefix and requested command with a newline. The prefix runs again for every command, so keep it fast and free of interactive prompts.\n\n## Enable Bash aliases\n\nStore aliases needed by Pi in a Bash-compatible file instead of parsing an entire interactive shell configuration.\n\nCreate `~/.bash_aliases`:\n\n```bash\nalias ll='ls -la'\nalias gs='git status --short'\n```\n\nThen configure Pi to enable alias expansion and load the file:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\nsource ~/.bash_aliases\"\n}\n```\n\nRun `/reload`, then verify the alias through Pi:\n\n```text\n!ll\n```\n\nThe command should produce the same listing as `ls -la`.\n\nAliases must use Bash-compatible syntax. Do not source an arbitrary `.zshrc` into Bash because zsh options, functions, and plugins may not parse or behave correctly there.\n\n## Troubleshooting\n\n### The prefix works for `!` but not for an extension tool\n\n`shellCommandPrefix` configures Pi's built-in Bash execution. An extension that replaces the `bash` tool or provides its own shell operations controls its own setup. Check that extension's documentation.\n\n### `shopt` is not found\n\nPi has fallen back to `sh` or `shellPath` points to a non-Bash shell. Install Bash or set `shellPath` to a Bash executable before using Bash-specific setup such as `shopt`.\n\n### A setup command waits for input\n\nRemove interactive commands from `shellCommandPrefix`. The prefix runs in a non-interactive process before every Bash command.\n\nFor the complete setting definitions, see [Shell settings](settings.md#shell).\n","isInternal":false,"tokens":733,"sizeBytes":3172},{"name":"skills.md","path":"packages/coding-agent/docs/skills.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/skills.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Skills\n\nSkills give Pi specialized instructions and supporting files for a particular kind of work. Pi advertises each available skill by name and description, then loads its full instructions only when the task calls for them.\n\nUse a skill when a workflow needs more context than a prompt template but does not need a new executable integration point. Skills can bundle scripts, references, and assets alongside their instructions.\n\nPi implements the [Agent Skills specification](https://agentskills.io/specification). Most invalid fields produce warnings rather than stopping startup.\n\n## Create a skill\n\nA skill is a directory containing `SKILL.md`:\n\n```text\npdf-tools/\n├── SKILL.md\n├── scripts/\n│   └── extract.sh\n├── references/\n│   └── formats.md\n└── assets/\n    └── template.json\n```\n\nStart `SKILL.md` with frontmatter followed by direct instructions:\n\n```markdown\n---\nname: pdf-tools\ndescription: Extract text and tables from PDF files. Use when reading, converting, or inspecting PDFs.\n---\n\n# PDF tools\n\nRead `references/formats.md` before converting a document. Run scripts relative to this skill directory.\n```\n\nThe description determines when the model considers loading the skill. State both what the skill does and when it applies. Avoid descriptions such as “Helps with PDFs,” which do not provide enough routing information.\n\nUse relative paths from the skill directory when referring to bundled files. Pi tells the model where the skill lives so it can resolve those paths.\n\n## Understand how skills load\n\nAt startup, Pi scans configured skill locations and adds each skill’s name, description, and path to the system prompt. It does not add the full instructions.\n\nWhen a task matches, the model reads `SKILL.md` and follows its instructions. This keeps detailed guidance out of context until it is needed. A model might fail to load a relevant skill, so use `/skill:name` when you need to force it.\n\nArguments after `/skill:name` are appended to the loaded instructions as a user request:\n\n```text\n/skill:pdf-tools extract report.pdf\n```\n\nSet `disable-model-invocation: true` in frontmatter when a skill should be available only through its explicit command. The `enableSkillCommands` [setting](settings.md) controls whether skill commands appear in interactive command discovery; manually entered `/skill:name` commands still work.\n\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the skill in your user or project skills directory. Directories containing `SKILL.md` are discovered recursively.\n\nPi also supports the Agent Skills locations `~/.agents/skills/` and `.agents/skills/`. Project `.agents/skills/` directories are discovered from the working directory through its ancestors, stopping at the repository root when one exists.\n\nPi accepts some standalone Markdown skills, but a directory containing `SKILL.md` is the portable form and should be preferred. See [Settings](settings.md#resources) and [Pi Packages](packages.md) for additional locations.\n\nProject skills can instruct the model to run scripts or modify files. Review unfamiliar skills and their supporting files before granting project trust.\n\n## Write portable frontmatter\n\nThe Agent Skills specification defines these fields:\n\n| Field | Purpose |\n|---|---|\n| `name` | Command and display name |\n| `description` | Routing description shown to the model |\n| `license` | License name or bundled license file |\n| `compatibility` | Environment requirements |\n| `metadata` | Additional key-value metadata |\n| `allowed-tools` | Experimental pre-approved tool list |\n| `disable-model-invocation` | Hide the skill from automatic model selection |\n\nNames use lowercase letters, numbers, and hyphens, with no leading, trailing, or consecutive hyphens. They can contain at most 64 characters; descriptions can contain at most 1024.\n\nPi neither requires nor warns when the declared name differs from the parent directory. Other Agent Skills implementations may enforce that requirement, so matching names remain the portable choice.\n\nMalformed `SKILL.md` files and declared skills without descriptions are not loaded. Name collisions keep the first discovered skill and produce a warning.\n\n## Validate and share a skill\n\nRun Pi from a location where the skill is discoverable, then inspect the startup diagnostics and `/skill:name` command. Run `/reload` after editing a skill during an active session.\n\nUse a [Pi package](packages.md) to distribute one or more skills through npm or git. Keep environment setup inside the skill and declare any required runtime dependencies in the package.\n\nFor examples, see the [Anthropic skills collection](https://github.com/anthropics/skills) and [Pi skills collection](https://github.com/badlogic/pi-skills).\n","isInternal":false,"tokens":981,"sizeBytes":4793},{"name":"slash-commands.md","path":"packages/coding-agent/docs/slash-commands.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/slash-commands.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Slash commands\n\nType `/` in Pi's terminal editor to search the commands available in the current session. This page lists the built-in commands in the current Pi release.\n\nExtensions, prompt templates, and skills can add commands. The command menu in Pi is therefore the exact reference for the resources loaded in your session.\n\n## Models and settings\n\n| Command | Description |\n|---|---|\n| `/settings` | Open settings |\n| `/model [provider/model]` | Select a model |\n| `/thinking [level]` | Set the thinking level |\n| `/scoped-models` | Configure the models used by interactive cycling |\n| `/login [provider]` | Add provider authentication |\n| `/logout` | Remove provider authentication |\n| `/llama` | Manage models on the configured llama.cpp router |\n\n## Sessions and context\n\n| Command | Description |\n|---|---|\n| `/new` | Start a new session |\n| `/resume` | Switch to another saved session |\n| `/name [name]` | Set the session display name, or show the current name when omitted |\n| `/session` | Show current session information and statistics |\n| `/tree` | Navigate the session tree |\n| `/fork` | Create a new session from an earlier user message |\n| `/clone` | Duplicate the current session at its current position |\n| `/compact [instructions]` | Compact the current context, optionally with custom instructions |\n| `/import <path>` | Import and resume a JSONL session |\n\n## Export and share\n\n| Command | Description |\n|---|---|\n| `/copy` | Copy the last assistant message |\n| `/export [path]` | Export the session as HTML or JSONL |\n| `/share` | Upload the session and return a viewer link |\n| `/bug [description]` | Prepare a private bug report for the Pi developers |\n\nReview a session before exporting or sharing it. Sessions can contain prompts, tool arguments, command output, file contents, and credentials exposed during the conversation.\n\n## Runtime and project\n\n| Command | Description |\n|---|---|\n| `/trust` | Save a project trust decision for future Pi processes |\n| `/reload` | Reload keybindings, extensions, skills, templates, themes, and context files |\n| `/hotkeys` | Show active keyboard shortcuts |\n| `/changelog` | Show changelog entries |\n| `/quit` | Quit Pi |\n\n## Commands added by resources\n\n- Extensions can register commands with their own arguments and completion behavior.\n- Each prompt template is available under its template name.\n- Skills are available as `/skill:name` when skill commands are enabled.\n\nUse `/reload` after adding or changing a discovered command resource. See [Extensions](extensions.md), [Prompt Templates](prompt-templates.md), and [Skills](skills.md) for their loading and naming rules.\n","isInternal":false,"tokens":580,"sizeBytes":2649},{"name":"terminal-setup.md","path":"packages/coding-agent/docs/terminal-setup.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/terminal-setup.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Configure your terminal\n\nMost modern terminals work with Pi without additional setup. Use this page when modified keys, scrolling, links, images, colors, or input-method editor (IME) positioning do not behave as expected.\n\nPi uses extended-key protocols so terminals can distinguish combinations such as `Shift+Enter` and `Alt+Enter` from plain `Enter`. Terminal proxies, multiplexers, and built-in IDE terminals can change or discard that information.\n\n## Troubleshooting\n\n| Symptom | Start here |\n|---|---|\n| `Shift+Enter` submits instead of inserting a line | Your terminal's section below; for tmux, see [Run Pi in tmux](tmux.md) |\n| `Alt+Enter` does not queue a follow-up | [WezTerm](#wezterm), [Alacritty](#alacritty), or [Windows Terminal](#windows-terminal) |\n| Fullscreen scrolling is unusually slow | [iTerm2](#iterm2) |\n| Links work but show no hover preview | [Ghostty](#ghostty) |\n| Inline images or colors are not detected | [Override detected capabilities](#override-detected-capabilities) |\n| An IME candidate window appears in the wrong place | [WezTerm](#wezterm) or [IntelliJ IDEA](#intellij-idea-integrated-terminal) |\n| Modified keys fail only inside tmux | [Run Pi in tmux](tmux.md) |\n\nUse `/hotkeys` to inspect Pi's active shortcuts. See [Keybindings](keybindings.md) to change them.\n\n## Kitty\n\nKitty supports the required keyboard protocol without additional configuration.\n\n## iTerm2\n\nRegular terminal mode works without additional configuration.\n\n### Fix slow fullscreen scrolling\n\nIn fullscreen mode, Pi owns the viewport, so iTerm2 sends mouse-wheel reports instead of scrolling native terminal history. Fast trackpad gestures can then move only about one line at a time.\n\nTo change this behavior:\n\n1. Open **iTerm2 > Settings > Advanced**.\n2. Search for **Trackpad scrolls fast?**.\n3. Set it to **No**.\n\nThis is an iTerm2-wide setting and can also change native trackpad scrolling. The underlying behavior is tracked in [iTerm2 issue 9619](https://gitlab.com/gnachman/iterm2/-/work_items/9619).\n\n## Apple Terminal\n\nPi enables enhanced key reporting when available. If Terminal.app still sends plain Return for `Shift+Enter`, Pi uses a local macOS modifier fallback and treats it as `Shift+Enter`.\n\nThe fallback works only when Pi runs on the same Mac as Terminal.app. It cannot inspect the local modifier state when Pi runs on another machine over SSH.\n\n## Ghostty\n\nAdd this mapping to Ghostty's configuration if `Alt+Backspace` does not work:\n\n```text\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nThe configuration file is `~/Library/Application Support/com.mitchellh.ghostty/config` on macOS and `~/.config/ghostty/config` on Linux.\n\nOlder Claude Code configurations may contain:\n\n```text\nkeybind = shift+enter=text:\\n\n```\n\nThis sends a raw linefeed, which Pi cannot distinguish from `Ctrl+J`. Remove the mapping if an older Claude Code installation is the only reason you added it. Pi already binds `Ctrl+J` as a newline alternative, so the mapping may appear to work while still preventing Pi and tmux from receiving a real `Shift+Enter` event.\n\n### Open links in fullscreen mode\n\nLinks remain clickable in fullscreen mode, but Ghostty does not show its normal hover underline or URL preview while Pi captures mouse input. Hold `Shift+Command` on macOS or `Shift+Ctrl` on Linux to use Ghostty's native link handling.\n\n## WezTerm\n\nWezTerm normally reports `Shift+Enter` through xterm extended keys. To enable the Kitty keyboard protocol explicitly, create `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\n### Forward Alt+Enter on macOS\n\nWezTerm binds `Option+Enter` to fullscreen by default on macOS. To use it for Pi's follow-up queue, add this entry to your `config.keys` table:\n\n```lua\n{\n  key = 'Enter',\n  mods = 'ALT',\n  action = wezterm.action.SendString('\\x1b[13;3u'),\n}\n```\n\nA complete minimal configuration is:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\n### Position an IME candidate window in WSL\n\nIf CJK IME candidates do not follow Pi's text cursor in WSL, show the hardware cursor:\n\n```bash\nexport PI_HARDWARE_CURSOR=1\npi\n```\n\nYou can instead set `showHardwareCursor` to `true` in Pi settings.\n\n## Alacritty\n\nAlacritty normally reports `Shift+Enter`. On macOS, `Option+Enter` can arrive as plain `Enter`. Add this to `~/.config/alacritty/alacritty.toml` to forward it to Pi:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nRestart Alacritty after changing the file.\n\n## VS Code integrated terminal\n\nVS Code 1.109.5 and newer enable the Kitty keyboard protocol in the integrated terminal by default.\n\nFor an older version, add a `Shift+Enter` terminal binding to `keybindings.json`:\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\nThe user `keybindings.json` file is normally located at:\n\n- macOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Linux: `~/.config/Code/User/keybindings.json`\n- Windows: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\n## Zed integrated terminal\n\nAdd these bindings to Zed's `keymap.json`:\n\n```json\n{\n  \"context\": \"Terminal\",\n  \"bindings\": {\n    \"shift-enter\": [\"terminal::SendText\", \"\\u001b[13;2u\"],\n    \"ctrl--\": [\"terminal::SendText\", \"\\u001b[45;5u\"],\n    \"ctrl-alt-]\": [\"terminal::SendText\", \"\\u001b[93;7u\"]\n  }\n}\n```\n\n## Windows Terminal\n\nWindows Terminal uses Pi's Windows and WSL shortcut defaults. See [Keybindings](keybindings.md) for the complete list.\n\n### Forward Shift+Enter\n\nOpen Windows Terminal's `settings.json` with `Ctrl+Shift+,` or **Settings > Open JSON file**. Add this object to its `actions` array:\n\n```json\n{\n  \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n  \"keys\": \"shift+enter\"\n}\n```\n\nFully close and reopen Windows Terminal, then verify that `Shift+Enter` inserts a new line in Pi.\n\n### Use Alt+Enter for follow-ups\n\nWindows Terminal binds `Alt+Enter` to fullscreen by default. Pi therefore uses `Ctrl+Q` for follow-ups on Windows and WSL.\n\nTo use `Alt+Enter` instead, configure Windows Terminal to forward the key and bind `app.message.followUp` to `alt+enter` in Pi's `keybindings.json`. See [Keybindings](keybindings.md#assign-keybindings).\n\n## xfce4-terminal and Terminator\n\nThese terminals cannot reliably distinguish modified Enter keys from plain `Enter`. Custom bindings such as `Ctrl+Enter` or `Shift+Enter` therefore may not work.\n\nUse a terminal with modern extended-key support when you need those shortcuts, such as Kitty, Ghostty, WezTerm, iTerm2, Windows Terminal, or a compatible Alacritty build.\n\n## IntelliJ IDEA integrated terminal\n\nIntelliJ IDEA's built-in terminal cannot reliably distinguish `Shift+Enter` from plain `Enter`. Use `Ctrl+J` for a newline or run Pi in a terminal with modern extended-key support.\n\nIf an IME candidate window does not follow the text cursor, show the hardware cursor:\n\n```bash\nexport PI_HARDWARE_CURSOR=1\npi\n```\n\n## Override detected capabilities\n\nPi automatically detects OSC 8 hyperlinks, inline image protocols, and truecolor support. A terminal proxy or multiplexer can make that detection inaccurate.\n\n| Capability | Environment variable | Setting |\n|---|---|---|\n| Hyperlinks | `PI_HYPERLINKS=1\\|0\\|auto` | `terminal.hyperlinks: true\\|false\\|\"auto\"` |\n| Inline images | `PI_IMAGE_PROTOCOL=kitty\\|iterm2\\|none\\|auto` | `terminal.images: \"kitty\"\\|\"iterm2\"\\|false\\|\"auto\"` |\n| Truecolor | `PI_TRUE_COLOR=1\\|0\\|auto` | `terminal.trueColor: true\\|false\\|\"auto\"` |\n\nSettings take precedence over environment variables. An unset value or `auto` preserves automatic detection.\n\nOnly force a capability supported by the complete terminal path. Unsupported escape sequences can corrupt rendering. See [Environment Variables](environment-variables.md#pi-process-configuration) and [Settings](settings.md) for the canonical value definitions.\n\n## Program status\n\nPi reports its state with the [Program Status Protocol (OSC 7501)](https://www.superlogical.com/rex/docs/build/program-status), so terminals and agent dashboards can show whether it is working, waiting for you, done, or failed:\n\n| State | When |\n|---|---|\n| `working` | An agent run or compaction is in progress. The message is the session name. |\n| `blocked` | An extension dialog or login waits for you. The message is the dialog title. |\n| `done` | A run finished. The message is the session name. |\n| `error` | A run ended with an error that is not retried. The message is the first line of the error. |\n| `idle` | Pi started, or you cancelled the run. |\n\nReports never contain prompts or model output. Pi sends them only after the terminal answers the protocol's support query; tmux and screen do not forward them. Set `PI_PROGRAM_STATUS=1` to send reports without asking, or `PI_PROGRAM_STATUS=0` to turn them off.\n","isInternal":false,"tokens":2306,"sizeBytes":9093},{"name":"termux.md","path":"packages/coding-agent/docs/termux.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/termux.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Run Pi on Android with Termux\n\nPi runs on Android through [Termux](https://termux.dev/), a terminal emulator and Linux environment. Text input, file tools, and shell commands are supported. Pi can copy and paste text through the Android clipboard with Termux:API. Clipboard image paste is not supported.\n\n## Before you begin\n\nInstall Termux from [GitHub or F-Droid](https://github.com/termux/termux-app#installation). Do not use the deprecated Google Play build.\n\n[Termux:API](https://github.com/termux/termux-api#installation) is optional. Install it only when you want Pi to copy or paste Android clipboard text, or when shell commands need Android device APIs.\n\n## Install Pi\n\n1. Update Termux packages:\n\n   ```bash\n   pkg update && pkg upgrade\n   ```\n\n2. Install Node.js and Git:\n\n   ```bash\n   pkg install nodejs git\n   ```\n\n3. Install Pi:\n\n   ```bash\n   npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n   ```\n\n4. Verify the installation:\n\n   ```bash\n   pi --version\n   ```\n\n5. Open the folder you want to work in and start Pi:\n\n   ```bash\n   cd /path/to/working-folder\n   pi\n   ```\n\nContinue with the main [Quickstart](quickstart.md#3-choose-a-model) to connect a model and run your first task.\n\n## Access Android shared storage\n\nTermux cannot access shared Android storage until you grant permission. Run this once:\n\n```bash\ntermux-setup-storage\n```\n\nAfter approval, Android shared storage is available under `/storage/emulated/0` and through the links Termux creates under `~/storage/`.\n\nOnly grant this permission when Pi should be able to access those files. Commands and tools running in Termux use the same storage permissions as the Termux process.\n\n## Use clipboard commands\n\nPi uses `termux-clipboard-set` to copy text and `termux-clipboard-get` for its clipboard-paste shortcut. Shell commands can use both commands directly. Install the Termux:API app and its command-line package:\n\n```bash\npkg install termux-api\n```\n\nVerify the integration:\n\n```bash\nprintf 'Pi clipboard test' | termux-clipboard-set\ntermux-clipboard-get\n```\n\nThe second command should print `Pi clipboard test`.\n\nThe Termux clipboard API supports text only. Pi's clipboard-paste shortcut inserts that text into the editor but cannot attach clipboard images.\n\n## Add Termux-specific instructions\n\nPi detects that it is running in Termux, but it cannot infer how you want it to interact with Android. Add only the environment details relevant to your work to `~/.pi/agent/AGENTS.md`:\n\n````markdown\n# Termux environment\n\n- Pi runs in Termux on Android.\n- Shared Android storage is under `/storage/emulated/0`.\n- Open URLs with `termux-open-url \"https://example.com\"`.\n- Open files with `termux-open <path>`.\n- Do not access shared storage unless the task requires it.\n````\n\nRun `/reload` after changing the file during an active session.\n\n## Troubleshooting\n\n### Clipboard integration fails\n\nConfirm that you installed both components:\n\n1. The Termux:API Android app from the same source as Termux\n2. The `termux-api` command-line package\n\nThen run the clipboard verification commands above outside Pi. If they fail there, fix the Termux:API installation before retrying Pi's copy command.\n\n### Shared storage reports permission denied\n\nRun `termux-setup-storage`, approve the Android permission request, and retry the path under `~/storage/` or `/storage/emulated/0`.\n\n### Pi is not found after installation\n\nOpen a new Termux shell and run:\n\n```bash\nnpm prefix -g\ncommand -v pi\n```\n\nConfirm that the global npm binary directory is on `PATH`, then reinstall Pi if the package is missing.\n","isInternal":false,"tokens":838,"sizeBytes":3590},{"name":"themes.md","path":"packages/coding-agent/docs/themes.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/themes.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Customize Pi with themes\n\nThemes control the colors Pi uses in interactive mode and HTML exports. Pi includes the `system`, `dark`, and `light` themes. You can select one theme, follow your terminal's light or dark appearance, or create your own palette.\n\n## Use your terminal's colors\n\nThe `system` theme is the default. It builds Pi's colors from your terminal's theme, so Pi matches the terminal instead of bringing its own palette:\n\n- Pi queries the terminal's default foreground and background colors and its 16 ANSI colors.\n- Each Pi color takes its hue from one ANSI color, for example errors from red and links from blue.\n- Pi sets each color's lightness so that it stands out from the background by a minimum contrast. Body text keeps at least a 4.5:1 WCAG contrast ratio on the background and every panel.\n- When the terminal switches between light and dark, Pi queries the colors again and rebuilds the theme.\n\nThe theme adapts to what the terminal reports:\n\n| Terminal reports | Result |\n|---|---|\n| Background and ANSI colors | Colors from the terminal palette, placed for the actual background. |\n| Background only | Pi's own hues, placed for the actual background. |\n| Nothing | ANSI color indices and the terminal's default colors, which the terminal renders itself. Secondary text is faint, and panels have no background color. |\n\nPi asks the terminal for its colors when it starts. Terminals usually answer within a few milliseconds, and Pi waits at most 100 ms before showing the startup header. If the terminal does not answer in time, Pi uses the ANSI color fallback, and it still applies the colors if they arrive later, for example over a slow SSH connection. `system` is a reserved name: a custom theme with that name is ignored.\n\n<a id=\"selecting-a-theme\"></a>\n\n## Choose a theme\n\nOpen `/settings` and select **Theme**. You can use one theme for every terminal appearance or choose separate themes for light and dark terminals.\n\nThe selection is saved as the `theme` [setting](settings.md#terminal-and-display):\n\n```json\n{\n  \"theme\": \"dark\"\n}\n```\n\nWithout a `theme` setting, Pi uses `system`.\n\nAutomatic mode stores the light theme first and the dark theme second:\n\n```json\n{\n  \"theme\": \"light/dark\"\n}\n```\n\nPi decides whether the terminal is light or dark from its reported background and foreground colors. If the terminal does not report its background, Pi uses the terminal's light/dark notification, then the `COLORFGBG` environment variable, then dark. The same decision picks the theme of a light/dark pair and the appearance of `system`. When automatic mode is active, Pi changes themes when the terminal reports an appearance change. Theme names cannot contain `/` because Pi reserves it for this setting format.\n\nUse `--use-theme` to choose the initial theme for one invocation without changing the saved setting:\n\n```bash\npi --use-theme light\npi --use-theme light/dark\n```\n\nSee [CLI resources](cli.md#resources) for the command-line option.\n\n## Create a custom theme\n\nCopy one of the [built-in themes](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/src/modes/interactive/theme) or create a new JSON file conforming to the [schema](https://pi.dev/schemas/theme.schema.json). The built-in themes use OKHSL colors, with variables for colors that several roles share, so you can adjust a hue, saturation, or lightness directly.\n\n1. Save the file as `<agent-dir>/themes/my-theme.json`. The agent directory defaults to `~/.pi/agent`.\n2. Set its `name` to `my-theme`.\n3. Change values in `vars` and `colors`.\n4. Select `my-theme` through `/settings`.\n\nUse the theme name as the filename. Pi hot-reloads the active user theme only from `<agent-dir>/themes/<name>.json`. Run `/reload` after adding or changing a theme from any other source.\n\n## Understand the theme file\n\n| Property | Required | Responsibility |\n|---|---|---|\n| `$schema` | No | Enables editor validation and completion against Pi's published schema. |\n| `name` | Yes | Identifies the theme in selectors and settings. It must be unique, cannot contain `/`, and cannot be `system`. |\n| `appearance` | No | `\"dark\"` or `\"light\"`: the background the theme is designed for. Pi detects it from the theme colors when omitted. |\n| `vars` | No | Defines reusable color values. Variables can reference other variables. |\n| `colors` | Yes | Assigns colors to terminal UI roles. The schema identifies required and optional roles. |\n| `export` | No | Overrides page and panel backgrounds in HTML exports. |\n\nTheme objects are strict: only documented top-level fields and color tokens are accepted. Define reusable custom colors under `vars`; custom keys under `colors` or `export` and additional top-level metadata are rejected.\n\nA color can be written in six forms:\n\n| Form | Example | Meaning |\n|---|---|---|\n| RGB hexadecimal | `\"#0af\"` or `\"#00aaff\"` | A three- or six-digit sRGB color. |\n| OKLCH | `\"oklch(62% 0.1 200)\"` | Perceptual lightness, chroma, and hue. |\n| OKHSL | `\"okhsl(250 60% 55%)\"` | Hue, saturation, and lightness. Saturation is relative to the most the sRGB gamut allows at that hue and lightness, so every value is in gamut and equal saturation looks equally colorful. |\n| 256-color index | `39` | An ANSI palette index from `0` through `255`. |\n| Variable reference | `\"primary\"` | The value of an entry in `vars`. |\n| Terminal default | `\"\"` | The terminal's default foreground or background color. |\n\nTerminal default colors render as the terminal's own colors. Where Pi needs a concrete value, such as HTML export or extension color math, it uses the default colors the terminal reports, or a black or white guess based on the theme's appearance.\n\nPi resolves chained variable references. A missing variable or circular reference makes the theme invalid. Pi uses truecolor when available, gamut-maps OKLCH to sRGB, and approximates colors for 256-color terminals. HTML exports convert OKHSL values to hexadecimal because CSS does not support them. If colors differ from their source values, check your terminal's truecolor detection and contrast settings. See [Configure Your Terminal](terminal-setup.md#override-detected-capabilities).\n\nUse the [theme JSON schema](https://pi.dev/schemas/theme.schema.json) for the exact properties, required colors, and accepted value types.\n\nPi reports invalid theme files during startup and `/reload`.\n\n## Find the color to change\n\nTheme colors describe interface roles rather than individual components. Use these groups to find the relevant part of the schema:\n\n| Area | Color names |\n|---|---|\n| General interface | `accent`, `border*`, `text`, `muted`, `dim`, `success`, `error`, `warning` |\n| Selection and fullscreen | `selectedBg`, `searchMatch*`, `scrollbar*` |\n| Messages | `userMessage*`, `customMessage*`, `thinkingText` |\n| Tool execution | `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`, `toolTitle`, `toolOutput` |\n| Markdown | `md*` |\n| Tool diffs | `toolDiff*` |\n| Syntax highlighting | `syntax*` |\n| Editor modes | `thinking*`, `bashMode` |\n| HTML export | `export.pageBg`, `export.cardBg`, `export.infoBg` |\n\nThe schema is the format reference. The built-in themes provide complete values that you can copy and adjust.\n\nFive colors are optional and inherit another color when omitted:\n\n| Optional color | Fallback |\n|---|---|\n| `scrollbarTrack` | `muted` |\n| `scrollbarThumb` | `text` |\n| `searchMatchBg` | `selectedBg` |\n| `searchMatchText` | `text` |\n| `thinkingMax` | `thinkingXhigh` |\n\nIf `export` colors are omitted, Pi derives HTML page and panel backgrounds from `userMessageBg`.\n\n## Load a theme from a project or package\n\nPlace a project theme in `.pi/themes/`. Project themes load only after [project trust](security.md#understand-project-trust) is granted.\n\nYou can also load theme files and directories through the `themes` setting or distribute them in a Pi package. See [Configuration](configuration.md), [Settings](settings.md#resources), and [Pi Packages](packages.md).\n\nEach loaded theme must have a unique name. Pi reports duplicate names as resource collisions.\n","isInternal":false,"tokens":1869,"sizeBytes":8069},{"name":"tmux.md","path":"packages/coding-agent/docs/tmux.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/tmux.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Run Pi in tmux\n\nPi works inside tmux, but tmux can report `Shift+Enter`, `Ctrl+Enter`, and plain `Enter` as the same key. Enable extended keys so Pi can distinguish them.\n\n## Check your tmux version\n\n```bash\ntmux -V\n```\n\nFor tmux 3.5 or newer, use the recommended CSI-u configuration below. For tmux 3.2 through 3.4, use the older-version configuration.\n\n## Enable extended keys in tmux 3.5 or newer\n\nAdd these lines to `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nPi requests extended-key reporting when the terminal does not provide the Kitty keyboard protocol directly. CSI-u is the most reliable format for forwarding modified keys through tmux.\n\n## Restart tmux\n\nThe configuration applies to the tmux server. To guarantee that it is active, close your tmux sessions and start a new server.\n\nIf you choose to stop the server from the command line, save your work first. This command terminates every session managed by that server:\n\n```bash\ntmux kill-server\ntmux\n```\n\n## Verify modified keys\n\nStart Pi inside the new tmux session and check that:\n\n1. `Shift+Enter` inserts a new line in the editor.\n2. `Enter` submits the prompt.\n3. `Alt+Enter` queues a follow-up on macOS and Linux. Windows and WSL use `Ctrl+Q` by default.\n\nIf these keys still behave like plain `Enter`, verify that the terminal outside tmux can report modified keys. See [Configure your terminal](terminal-setup.md).\n\n## Use tmux 3.2 through 3.4\n\nThese versions support extended keys but not `extended-keys-format csi-u`. Add only:\n\n```tmux\nset -g extended-keys on\n```\n\nPi supports the xterm `modifyOtherKeys` format used by these versions. Restart tmux and repeat the verification steps.\n\nFor older versions, upgrade tmux or use Pi outside tmux rather than relying on modified Enter shortcuts.\n","isInternal":false,"tokens":447,"sizeBytes":1813},{"name":"tui.md","path":"packages/coding-agent/docs/tui.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/tui.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Terminal UI\n\n`@earendil-works/pi-tui` provides the terminal component system used by Pi. Extensions use it when built-in dialogs, notifications, status text, and widgets are not enough for the interaction they need.\n\nStart with `ctx.ui` methods from an [extension](extensions.md#interact-with-the-user). Build a custom component only when the UI needs its own rendering, keyboard or mouse input, focus, layout, or lifecycle.\n\n## Choose an integration point\n\n| Need | Use |\n|---|---|\n| Select, confirm, input, or multi-line editor | `ctx.ui.select()`, `confirm()`, `input()`, or `editor()` |\n| Non-blocking feedback | `ctx.ui.notify()` or `setStatus()` |\n| Persistent content near the editor | `ctx.ui.setWidget()` |\n| Replace the header, footer, or editor | The corresponding `ctx.ui` component factory |\n| Temporary interactive screen or overlay | `ctx.ui.custom()` |\n| Custom rendering for a tool or session entry | An extension renderer |\n\nThese APIs receive Pi’s active theme and keybindings where needed. Do not create a second terminal renderer inside an extension.\n\n## Understand the component model\n\nA component renders an array of terminal lines for an available width. It can optionally handle keyboard and mouse input, and it must invalidate cached output when its state or theme-dependent content changes.\n\nEvery rendered line must fit within the supplied width. Measure visible terminal columns rather than string length because ANSI escapes, wide characters, emoji, and combining characters change display width.\n\nUse `visibleWidth()`, `truncateToWidth()`, `sliceByColumn()`, and `wrapTextWithAnsi()` instead of implementing terminal-width handling yourself. Pi resets styling and hyperlinks after every line, so reapply styles on each rendered line.\n\nAfter changing component state, invalidate the affected component and call the injected `tui.requestRender()`. The TUI coalesces render requests and updates the terminal.\n\n## Compose built-in components\n\nThe package includes components for common layouts and controls:\n\n- `Text`, `Markdown`, `Image`, and `TruncatedText` render content.\n- `Container`, `VStack`, `HStack`, `Box`, and `Spacer` compose layouts.\n- `Input` and `Editor` accept text.\n- `SelectList` and `SettingsList` implement searchable selection and settings flows.\n- `ScrollView` provides a bounded scrollable viewport.\n- `Loader` and `CancellableLoader` report ongoing work.\n- `MouseRegion` adds pointer behavior around another component.\n\nPrefer these components over rebuilding selection, scrolling, text editing, or width handling. The extension examples show how to combine them with Pi’s borders and themes.\n\n## Handle keyboard input and focus\n\nUse `matchesKey()` and `Key` for terminal keyboard input. The parser accounts for supported terminal protocols and key modifiers. Extension components should use the injected `KeybindingsManager` for configurable application actions.\n\nA component that displays a text cursor should implement `Focusable` and place `CURSOR_MARKER` immediately before its visual cursor. Wrap the cursor cell in `renderFakeCursor()` so the TUI draws it in reverse video, or hides it after `CURSOR_MARKER` when the hardware cursor is shown. The TUI uses the marker to position the hardware cursor for input method editors.\n\nContainers that wrap an `Input` or `Editor` must propagate their `focused` state to that child. Without propagation, Chinese, Japanese, Korean, and other IME candidate windows can appear at the wrong screen position.\n\nExtend Pi’s `CustomEditor` when replacing the main editor. It preserves application shortcuts and agent controls.\n\nForward keys your editor does not own to the base implementation, and restore the default by clearing the custom editor factory.\n\n## Handle mouse input\n\nFullscreen mode routes normalized mouse events to components. A handler can mark an event handled, capture a drag sequence, request focus, or request a render.\n\nUnhandled wheel events scroll the nearest `ScrollView`. Unhandled primary-button drags remain available for transcript selection. OSC 8 links take precedence over enclosing click regions.\n\nRegular mode leaves mouse input to the terminal because the terminal owns scrollback. Design every interaction with a keyboard path even when fullscreen mouse input is available.\n\n## Use custom screens and overlays\n\n`ctx.ui.custom()` temporarily gives one component control of the interactive area and resolves when that component calls the supplied completion callback.\n\n### Build a minimal custom screen\n\nThis complete extension shows the custom-component lifecycle: render within the supplied width, use injected keybindings, invalidate cached output after state changes, request a render, and finish through `done()`.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth } from \"@earendil-works/pi-tui\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"pick-target\", {\n    description: \"Pick a deployment target\",\n    handler: async (_args, ctx) => {\n      if (ctx.mode !== \"tui\") {\n        ctx.ui.notify(\"pick-target requires interactive mode\", \"error\");\n        return;\n      }\n\n      const choices = [\"development\", \"staging\", \"production\"];\n      const selected = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {\n        let selectedIndex = 0;\n        let cachedWidth: number | undefined;\n        let cachedLines: string[] | undefined;\n\n        const invalidate = () => {\n          cachedWidth = undefined;\n          cachedLines = undefined;\n        };\n\n        return {\n          render(width: number): string[] {\n            if (cachedLines && cachedWidth === width) return cachedLines;\n\n            cachedWidth = width;\n            cachedLines = [\n              theme.fg(\"accent\", \"Choose a deployment target\"),\n              ...choices.map((choice, index) => `${index === selectedIndex ? \"> \" : \"  \"}${choice}`),\n              theme.fg(\"dim\", \"up/down move, enter selects, escape cancels\"),\n            ].map((line) => truncateToWidth(line, width));\n            return cachedLines;\n          },\n          handleInput(data: string): void {\n            if (keybindings.matches(data, \"tui.select.up\")) {\n              selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;\n            } else if (keybindings.matches(data, \"tui.select.down\")) {\n              selectedIndex = (selectedIndex + 1) % choices.length;\n            } else if (keybindings.matches(data, \"tui.select.confirm\")) {\n              done(choices[selectedIndex]);\n              return;\n            } else if (keybindings.matches(data, \"tui.select.cancel\")) {\n              done(undefined);\n              return;\n            } else {\n              return;\n            }\n\n            invalidate();\n            tui.requestRender();\n          },\n          invalidate,\n        };\n      });\n\n      if (selected) ctx.ui.notify(`Selected ${selected}`, \"info\");\n    },\n  });\n}\n```\n\nThis component does not display a text cursor, so it does not need `Focusable`. A component containing `Input` or `Editor` must implement `Focusable` and propagate its `focused` value to that child.\n\nPass `overlay: true` to draw above existing content. Overlay options control size, anchors, offsets, margins, and responsive visibility. An overlay handle can change focus or temporarily hide and show the overlay with `setHidden()` while the interaction remains active.\n\nFocused overlays retain input ownership across ordinary renders. If another component should receive input while an overlay remains visible, explicitly release or redirect focus through the handle.\n\nTreat each custom component instance as belonging to one interaction. Create a new instance when starting that interaction again.\n\nFinish the interaction with the completion callback supplied to the component factory. It resolves the `ctx.ui.custom()` promise and disposes the component. Do not call `OverlayHandle.hide()` on an overlay created by `ctx.ui.custom()`.\n\nSee [`overlay-qa-tests.ts`](../examples/extensions/overlay-qa-tests.ts) for positioning, stacking, focus, responsive visibility, and animation behavior.\n\n## Apply themes correctly\n\nUse the theme passed to the extension or component callback. Theme helpers produce ANSI-styled strings for semantic colors such as accent, muted text, success, warnings, errors, tool output, and Markdown.\n\nUse `theme.style()` to combine foreground and background colors with text attributes:\n\n```typescript\nreturn new Text(\n  theme.style(\"Done!\", {\n    fg: \"success\",\n    bg: \"toolSuccessBg\",\n    bold: true,\n  }),\n  0,\n  0,\n);\n```\n\nA style color can be a semantic theme token or a concrete `Color`. Foreground tokens are accepted as `fg` and background tokens as `bg`; to use a token's color in the other position, pass its concrete color, for example `{ fg: theme.colors.userMessageBg }`. Access concrete colors through `theme.colors` and use utilities such as `mixColors()` from `@earendil-works/pi-tui` when color math is needed. Tokens that a theme sets to the terminal default render with the terminal's own color; `theme.colors` reports the color the terminal announced for them, or a guess when it did not. Use `theme.appearance` (`\"dark\"` or `\"light\"`) to decide, for example, whether to lighten or darken a color. Pi converts the result to truecolor or 256-color output based on terminal capabilities. Theme tokens are converted once per theme; compute concrete colors outside the render path when possible.\n\nThe existing `theme.fg()` and `theme.bg()` helpers remain available for applying one semantic color.\n\nDo not permanently store strings with theme colors unless `invalidate()` rebuilds them. A theme change clears render caches, but it cannot remove old ANSI colors embedded in application state.\n\nTheme callbacks evaluated during rendering do not need special rebuilding. Stateless components can also calculate themed output on every render.\n\nUse [Themes](themes.md) to create terminal palettes. Use Pi’s `getMarkdownTheme()` when rendering Markdown that should match the active application theme.\n\n## Keep rendering responsive\n\nRendering runs on the interactive path. Cache expensive layout and highlighting work by width and content, then clear that cache from `invalidate()`.\n\nKeep the default view compact and reveal detail through expansion or a dedicated screen. For custom tool rendering, handle partial results and reuse the previous component when it can be updated safely.\n\nUse `PI_TUI_WRITE_LOG` to capture the raw ANSI stream when diagnosing rendering problems. Test narrow widths, wide characters, resize events, theme changes, focus transitions, and both regular and fullscreen modes.\n\n## Examples and source\n\nThe checked extension examples cover the main patterns:\n\n- [`preset.ts`](../examples/extensions/preset.ts) and [`tools.ts`](../examples/extensions/tools.ts) use selection and settings lists.\n- [`qna.ts`](../examples/extensions/qna.ts) uses cancellable asynchronous UI.\n- [`modal-editor.ts`](../examples/extensions/modal-editor.ts) replaces the editor.\n- [`custom-footer.ts`](../examples/extensions/custom-footer.ts) replaces the footer.\n- [`widget-placement.ts`](../examples/extensions/widget-placement.ts) places persistent content around the editor.\n- [`doom-overlay/`](../examples/extensions/doom-overlay/) demonstrates a continuously rendered overlay.\n\nThe public exports are defined in [`packages/tui/src/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/tui/src/index.ts). See [Extensions](extensions.md) for extension lifecycle, state, tools, events, and mode behavior.\n","isInternal":false,"tokens":2364,"sizeBytes":11654},{"name":"usage.md","path":"packages/coding-agent/docs/usage.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/usage.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Use Pi in the terminal\n\nRun `pi` from the folder you want to work in. Pi uses that folder to discover files, instructions, and configuration, and to group saved sessions. If you have not installed Pi or chosen a model yet, follow the [Quickstart](quickstart.md).\n\nPi may ask whether you trust the working folder before loading its project resources. See [Project trust](security.md#understand-project-trust).\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Pi interactive mode showing a conversation, editor, and status information\" width=\"750\"></p>\n\nThe transcript shows your prompts, Pi's responses, tool calls, results, and errors. You write prompts and commands in the editor. The footer shows the current folder, session, model, context usage, and accumulated usage and cost.\n\n## Enter a prompt\n\nType a request and press `Enter` to send it. Use `Shift+Enter` to add a line, or press `Ctrl+G` to work on a longer prompt in your configured external editor.\n\nTo include files or images:\n\n- Type `@` to search for a file and add it to your prompt.\n- Press `Tab` to complete a path.\n- Paste an image or drag it into a compatible terminal.\n\n## Follow Pi's work\n\nPi shows each tool call and result while it works. Press `Ctrl+O` to expand or collapse tool output. Press `Ctrl+T` to show or hide thinking blocks.\n\nThe startup header lists the instructions and resources Pi loaded. The editor border indicates the current thinking level. The footer updates as the model uses context and reports usage.\n\nPi does not ask before every tool call. Review commands and changed files, and use a sandbox for untrusted or unattended work. See [Security](security.md).\n\n## Change direction\n\nYou can send more input while Pi is working:\n\n| What you want | Action |\n|---|---|\n| Adjust the current task | Type a message and press `Enter` |\n| Add work after the current task | Type a message and press `Alt+Enter` |\n| Return queued messages to the editor | Press `Alt+Up` |\n| Stop the current task | Press `Escape` |\n\nA message sent with `Enter` waits until the current response and its tool calls finish, then guides the next response. A follow-up sent with `Alt+Enter` waits until Pi finishes the current task. Aborting returns queued messages to the editor.\n\nWindows Terminal reserves some Alt shortcuts. See [Terminal Setup](terminal-setup.md) for the Windows alternatives.\n\n## Change the model or settings\n\nType `/` to search the available commands. The commands you will use most often are:\n\n- `/model` selects a model. Press `Ctrl+L` to open the same selector.\n- `/thinking` selects how much reasoning the current model uses. Press `Shift+Tab` to cycle through supported levels.\n- `/login` and `/logout` manage provider access.\n- `/settings` changes common preferences.\n\nPrompt templates, skills, and extensions can add more commands to the same menu. See [Choose a Model](models.md), [Configuration](configuration.md), or the complete [Slash Commands reference](slash-commands.md).\n\n## Continue or start over\n\nPi saves sessions automatically unless session persistence is disabled.\n\n- `/new` starts a new session.\n- `/resume` opens another saved session.\n- `/name` gives the current session a recognizable name.\n- `/session` shows its file, ID, message count, token usage, and cost.\n\nUse `/tree`, `/fork`, or `/clone` when you want to explore another approach without losing existing work. Use `/compact` to reduce the conversation history sent to the model. See [Sessions and Context](sessions.md) for these workflows.\n\nAfter leaving Pi, run `pi --continue` from the same folder to resume its most recent session.\n\n## Run a terminal command\n\nPrefix a command with `!` to run it and include its output in the conversation:\n\n```text\n!git status\n```\n\nUse `!!` when you want to run a command without sending its output to the model.\n\n## Copy, export, or share results\n\nPress `Ctrl+X` or run `/copy` to copy the last assistant response. Use `/export` to save the session as HTML or JSONL.\n\nUse `/share` to upload the session and get a viewer link. By default, Radius authentication makes the artifact visible to your Radius organization; otherwise, Pi creates a private GitHub gist through the GitHub CLI. The [`sessionShareDestination`](settings.md#sessions-and-context) setting can force either destination. Review the session first because it can contain prompts, tool output, file contents, and credentials exposed during the conversation.\n\n## Adjust the terminal\n\nFullscreen mode, the default, keeps the editor and status area fixed while the transcript scrolls within the terminal window. Regular mode uses the terminal's normal scrollback. Choose a mode through `/settings` or `--tui-mode`.\n\nTerminal support for mouse input, keyboard shortcuts, and inline images varies. See [Terminal Setup](terminal-setup.md) for platform-specific configuration and [Keybindings](keybindings.md) for every configurable shortcut. Run `/hotkeys` to inspect the shortcuts active in your current session.\n\n## Collect diagnostic information\n\nWhen troubleshooting terminal rendering or conversation state, run `/debug`. Pi writes the rendered terminal lines and current session messages to `pi-debug.log` in your [agent directory](configuration.md#agent-directory).\n\nReview this file before sharing it. It can contain prompts, model responses, tool output, file contents, and terminal data.\n","isInternal":false,"tokens":1154,"sizeBytes":5381},{"name":"virtual-models.md","path":"packages/coding-agent/docs/virtual-models.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/virtual-models.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Virtual Models\n\nA virtual model is a selectable model that picks a physical model for each request. Use one to route by task, cost, or conversation state. For example, a router can send quick questions to a small model and hard problems to a large one, while the user selects a single model.\n\nRegister virtual models from an [extension](extensions.md). They appear in `/model`, `--model`, scoped models, and settings like any other model. A virtual model can be listed under any provider, including one with physical models, such as `openai-codex/auto`.\n\n## Selection and dispatch\n\nA virtual model selects a model and a thinking level. A router maps that pair to a physical pair for each request:\n\n```\nselected (virtual model, virtual level)  ->  dispatched (physical model, physical level)\njev/auto:low                             ->  anthropic/claude-sonnet-4-5:high\n```\n\nThe virtual thinking level is an input to the router. Its meaning is up to the router; it need not correspond to a reasoning budget.\n\nPi keeps the two pairs apart:\n\n| | Selection | Dispatch |\n|---|---|---|\n| Recorded in | `model_change` and `thinking_level_change` entries | Each assistant message: `provider`, `api`, `model`, `thinkingLevel` |\n| Visible as | `ctx.model`, `ctx.thinkingLevel`, `PI_MODEL`, `PI_REASONING_LEVEL`, `/model` | The assistant message of each response |\n\nProviders only receive physical models. Assistant messages name the physical model, so replaying a conversation across different physical models works the same as after a manual model switch. Resuming a session restores the virtual selection from its latest `model_change` entry. If the virtual model is no longer registered, Pi falls back to the physical model that answered last.\n\nIn interactive mode, the footer shows the routed model next to the selection, for example `auto • high → gpt-5.6-luna • medium`. `/session` lists the cost for each physical model.\n\nContext usage uses the limits of the physical model that produced the latest response, even if that response came before switching to the virtual model. Without such a response, it uses the limits declared on the virtual model, if any. Compaction checks the same limits, and again the limits of the model each request is routed to. If that model's context window is too small for the conversation, Pi compacts before sending the request; the route stays as the router chose it.\n\n## Register a virtual model\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerVirtualModel({\n    provider: \"router\",\n    id: \"auto\",\n    name: \"Auto\",\n    thinkingLevels: [\"low\", \"high\"],\n    route(request, ctx) {\n      // Tool follow-ups and retries stay on the model that handled the turn.\n      const sticky = request.failed ?? request.previous;\n      if (request.reason !== \"user\" && sticky) {\n        return { model: sticky.model, thinkingLevel: sticky.thinkingLevel ?? \"medium\" };\n      }\n      const id = request.thinkingLevel === \"high\" ? \"claude-sonnet-4-5\" : \"claude-haiku-4-5\";\n      return { model: ctx.modelRegistry.find(\"anthropic\", id)!, thinkingLevel: \"medium\" };\n    },\n  });\n}\n```\n\n- `provider` is the provider the model is listed under. It can be any provider ID. A provider can list several virtual models next to its physical ones. On a physical provider, the virtual model is available when that provider has credentials. Under an ID that no provider uses, it is always available.\n- `id` must not be the ID of a physical model of that provider. If a catalog refresh later adds a physical model with the same ID, the virtual model hides it.\n- `thinkingLevels` lists the levels offered for selection. It defaults to `[\"off\"]`.\n- `contextWindow` and `maxTokens` are shown before the first response. Unset limits are unknown.\n- `input` lists the input types offered for selection. It defaults to text and images; physical models without image support receive placeholders.\n\nRegistration follows the same queuing and reload rules as `pi.registerProvider()`. Registering the same provider and ID again replaces the virtual model. `pi.unregisterVirtualModel(provider, id)` removes it; `pi.unregisterProvider()` does not. SDK code can register one without an extension: `modelRuntime.registerVirtualModel(definition)`.\n\n## Route requests\n\n`route(request, ctx)` runs before every request made with the virtual model and returns `{ model, thinkingLevel }`. The model can be any physical model in the catalog whose provider has credentials; look it up with `ctx.modelRegistry`. A virtual model cannot route to another virtual model. Pi clamps the thinking level to the returned model.\n\n| Field | Meaning |\n|---|---|\n| `model`, `thinkingLevel` | The selected virtual model and level |\n| `reason` | Why the request is made, see below |\n| `previous` | Physical model and thinking level of the latest successful response in `messages` |\n| `failed` | For `retry`: physical model, thinking level, and assistant `message` of the failed request, which `messages` no longer contains. The message carries `stopReason` and `errorMessage`. Absent when routing itself failed |\n| `state` | Router state last returned on this session branch, see below |\n| `messages` | The conversation for this request, including system messages |\n| `signal` | Abort signal of the request |\n\n| `reason` | Request |\n|---|---|\n| `user` | First request after a message the user wrote, including steering and follow-up messages |\n| `continuation` | Any other request in the agent loop, such as after tool results or extension messages |\n| `retry` | Automatic retry after a failed request, including after compaction for a context overflow |\n| `direct` | Request made outside the agent loop, such as a compaction summary or an extension calling `ctx.modelRegistry.streamSimple()` |\n\nReturning `previous` for `continuation` and `failed` for `retry` keeps prompt caches and thinking signatures valid. Switching models between turns is allowed but loses the prompt cache. A retry can also switch to another model, for example when `failed.message.errorMessage` reports that a provider is overloaded or the context overflowed.\n\nIf `route()` throws, or returns a virtual model or a model without credentials, the request ends with an error response.\n\n## Keep routing state\n\n`route()` can return `state` next to the model. Pi stores it on the session branch and passes it back as `request.state` on later requests. Use it for decisions the transcript does not record, such as classifier results or a routing phase:\n\n```typescript\npi.registerVirtualModel<{ phase: \"plan\" | \"build\" }>({\n  provider: \"router\",\n  id: \"phased\",\n  name: \"Phased\",\n  route(request, ctx) {\n    const state = request.state ?? { phase: \"plan\" };\n    const id = state.phase === \"plan\" ? \"claude-opus-4-5\" : \"claude-haiku-4-5\";\n    return { model: ctx.modelRegistry.find(\"anthropic\", id)!, thinkingLevel: \"medium\", state };\n  },\n});\n```\n\n- State must be JSON-serializable. Returning `undefined` or `request.state` itself keeps the current state.\n- Pi stores any other returned object as new state, before the request is sent, even when it equals the current state. Return a new object only when the state changes. The state stays stored if the request later fails.\n- State follows the session tree, so forks and `/tree` navigation see the state of their branch. It survives compaction.\n- `direct` requests have no state, and Pi ignores state they return.\n\nThe transcript already records the selection and every dispatched model, and `ctx.sessionManager.getBranch()` exposes both.\n\nRouters can call other models through `ctx.modelRegistry`, for example `ctx.modelRegistry.classify()` with a classifier model from `ctx.modelRegistry.findOfType(\"classifier\", provider, id)`. The call adds latency before the first token of the turn.\n\nSee [`jev-router.ts`](../examples/extensions/jev-router.ts) for a complete router. It plans on a strong OpenAI Codex model chosen by the Jev classifier, lets that model make the first edit, and then switches once to a cheaper model, accepting a single prompt-cache miss. It keeps the phase as router state.\n","isInternal":false,"tokens":1821,"sizeBytes":8177},{"name":"windows.md","path":"packages/coding-agent/docs/windows.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/docs/windows.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fdocs%2Fwindows.md -->","isInternal":false,"tokens":854,"sizeBytes":3660},{"name":"SKILL.md","path":"packages/coding-agent/examples/extensions/dynamic-resources/SKILL.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/examples/extensions/dynamic-resources/SKILL.md","title":"dynamic-resources","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fdynamic-resources%2FSKILL.md -->","frontmatter":{"name":"dynamic-resources","description":"Example skill loaded from resources_discover"},"isInternal":false,"tokens":32,"sizeBytes":177},{"name":"spec.md","path":"packages/durable/docs/spec.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/durable/docs/spec.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fdurable%2Fdocs%2Fspec.md -->","isInternal":false,"tokens":11314,"sizeBytes":50030},{"name":"protocol.md","path":"packages/env/docs/protocol.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/env/docs/protocol.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2Fdocs%2Fprotocol.md -->","isInternal":false,"tokens":2798,"sizeBytes":10570},{"name":"semantics.md","path":"packages/env/docs/semantics.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/env/docs/semantics.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2Fdocs%2Fsemantics.md -->","isInternal":false,"tokens":1299,"sizeBytes":5244},{"name":"README.md","path":"packages/agent/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/agent/README.md","title":"agent Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fagent%2FREADME.md -->","isInternal":false,"tokens":4926,"sizeBytes":21449},{"name":"README.md","path":"packages/ai/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/ai/README.md","title":"ai Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fai%2FREADME.md -->","isInternal":false,"tokens":11902,"sizeBytes":50044},{"name":"README.md","path":"packages/chord/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/chord/README.md","title":"chord Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fchord%2FREADME.md -->","isInternal":false,"tokens":3139,"sizeBytes":13561},{"name":"README.md","path":"packages/chord/src/delta/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/chord/src/delta/README.md","title":"delta Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fchord%2Fsrc%2Fdelta%2FREADME.md -->","isInternal":false,"tokens":2446,"sizeBytes":10890},{"name":"README.md","path":"packages/codemode/examples/cloudflare/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/codemode/examples/cloudflare/README.md","title":"cloudflare Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcodemode%2Fexamples%2Fcloudflare%2FREADME.md -->","isInternal":false,"tokens":282,"sizeBytes":1046},{"name":"README.md","path":"packages/codemode/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/codemode/README.md","title":"codemode Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcodemode%2FREADME.md -->","isInternal":false,"tokens":5659,"sizeBytes":23582},{"name":"README.md","path":"packages/coding-agent/examples/extensions/doom-overlay/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/examples/extensions/doom-overlay/README.md","title":"doom-overlay Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fdoom-overlay%2FREADME.md -->","isInternal":false,"tokens":360,"sizeBytes":1322},{"name":"README.md","path":"packages/coding-agent/examples/extensions/plan-mode/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/examples/extensions/plan-mode/README.md","title":"plan-mode Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fplan-mode%2FREADME.md -->","isInternal":false,"tokens":513,"sizeBytes":2009},{"name":"README.md","path":"packages/coding-agent/examples/extensions/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/examples/extensions/README.md","title":"extensions Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2FREADME.md -->","isInternal":false,"tokens":2382,"sizeBytes":10199},{"name":"README.md","path":"packages/coding-agent/examples/extensions/subagent/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/examples/extensions/subagent/README.md","title":"subagent Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fextensions%2Fsubagent%2FREADME.md -->","isInternal":false,"tokens":1507,"sizeBytes":6298},{"name":"README.md","path":"packages/coding-agent/examples/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/examples/README.md","title":"examples Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2FREADME.md -->","isInternal":false,"tokens":278,"sizeBytes":1282},{"name":"README.md","path":"packages/coding-agent/examples/sdk/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/examples/sdk/README.md","title":"sdk Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Fexamples%2Fsdk%2FREADME.md -->","isInternal":false,"tokens":1141,"sizeBytes":4790},{"name":"README.md","path":"packages/coding-agent/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/README.md","title":"coding-agent Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2FREADME.md -->","isInternal":false,"tokens":1040,"sizeBytes":4080},{"name":"README.md","path":"packages/coding-agent/test/mcp-conformance/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/test/mcp-conformance/README.md","title":"mcp-conformance Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Ftest%2Fmcp-conformance%2FREADME.md -->","isInternal":false,"tokens":627,"sizeBytes":2590},{"name":"README.md","path":"packages/coding-agent/test/suite/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/coding-agent/test/suite/README.md","title":"suite Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fcoding-agent%2Ftest%2Fsuite%2FREADME.md -->","isInternal":false,"tokens":110,"sizeBytes":458},{"name":"README.md","path":"packages/durable/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/durable/README.md","title":"durable Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fdurable%2FREADME.md -->","isInternal":false,"tokens":11706,"sizeBytes":50042},{"name":"README.md","path":"packages/env/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/env/README.md","title":"env Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fenv%2FREADME.md -->","isInternal":false,"tokens":1077,"sizeBytes":4526},{"name":"README.md","path":"packages/evals/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/evals/README.md","title":"evals Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fevals%2FREADME.md -->","isInternal":false,"tokens":1380,"sizeBytes":6039},{"name":"README.md","path":"packages/mcp/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/mcp/README.md","title":"mcp Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Fmcp%2FREADME.md -->","isInternal":false,"tokens":1371,"sizeBytes":5733},{"name":"README.md","path":"packages/telemetry/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/telemetry/README.md","title":"telemetry Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftelemetry%2FREADME.md -->","isInternal":false,"tokens":6968,"sizeBytes":32776},{"name":"README.md","path":"packages/tui/native/darwin/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/tui/native/darwin/README.md","title":"darwin Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Fdarwin%2FREADME.md -->","isInternal":false,"tokens":184,"sizeBytes":730},{"name":"README.md","path":"packages/tui/native/linux/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/tui/native/linux/README.md","title":"linux Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Flinux%2FREADME.md -->","isInternal":false,"tokens":201,"sizeBytes":909},{"name":"README.md","path":"packages/tui/native/win32/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/tui/native/win32/README.md","title":"win32 Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2Fnative%2Fwin32%2FREADME.md -->","isInternal":false,"tokens":242,"sizeBytes":943},{"name":"README.md","path":"packages/tui/README.md","rawUrl":"https://raw.githubusercontent.com/earendil-works/pi/HEAD/packages/tui/README.md","title":"tui Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/earendil-works/pi?path=packages%2Ftui%2FREADME.md -->","isInternal":false,"tokens":8221,"sizeBytes":34796}],"systemPromptSnippet":"<yakaai_skills repo=\"earendil-works/pi\">\n<!-- File: AGENTS.md (Tokens: ~1995 | Category: root-instruction) -->\n# Development Rules\n\n## Conversational Style\n\n- Keep answers short and concise\n- No emojis in commits, issues, PR comments, or code\n- No fluff or cheerful filler text (e.g., \"Thanks @user\" not \"Thanks so much @user!\")\n- Technical prose only, be direct\n- Use concise, clear, simple language. Define unavoidable jargon before using it.\n- Explain non-trivial designs and problems as: problem, concrete example or short trace, then solution. State why the solution is necessary and distinguish it from optional complexity.\n- Prefer concrete behavior and small illustrations over abstract summaries, dense terminology, or unexplained lists of changes.\n- When the user asks a question, answer it first before making edits or running implementation commands.\n- When responding to user feedback or an analysis, explicitly say whether you agree or disagree before saying what you changed.\n\n## Code Quality\n\n- Read files in full before wide-ranging changes, before editing files you have not fully inspected, and when asked to investigate or audit. Do not rely on search snippets for broad changes.\n- No `any` unless absolutely necessary.\n- Inline single-line helpers that have only one call site.\n- Check node_modules for external API types; don't guess.\n- **No inline imports** (`await import()`, `import(\"pkg\").Type`, dynamic type imports). Top-level imports only.\n- In `packages/coding-agent`, resolve package assets through helpers in `src/config.ts`. Do not use `__dirname` directly; the helpers account for source checkouts, npm installations, and standalone binaries.\n- Never remove or downgrade code to fix type errors from outdated deps; upgrade the dep instead.\n- Use only erasable TypeScript syntax (Node strip-only mode) in code checked by the root config (`packages/*/src`, `packages/*/test`, `packages/coding-agent/examples`): no parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other constructs needing JS emit. Use explicit fields with constructor assignments.\n- Always ask before removing functionality or code that appears intentional.\n- Do not preserve backward compatibility unless the user asks for it.\n- Never hardcode key checks (e.g. `matchesKey(keyData, \"ctrl+x\")`). Add defaults to `DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS` so they stay configurable.\n- Never modify `packages/ai/src/models.generated.ts` directly; update `packages/ai/scripts/generate-models.ts` instead, then regenerate. Including the resulting `models.generated.ts` diff is always OK, even if regeneration includes unrelated upstream model metadata changes. The generated model data in `packages/ai/src/providers/data/` is gitignored: never stage it, and don't look for it in git history.\n\n## Commands\n\n- After code changes (not docs): `npm run check` (full output, no tail). Fix all errors, warnings, and infos before committing. Does not run tests.\n- Never run `npm run build` or `npm test` unless requested by the user.\n- Never run the full vitest suite directly: it includes e2e tests that activate when endpoint/auth env vars are present. For all non-e2e tests, run `./test.sh` from the repo root. Otherwise run specific tests from the package root:\n  - Vitest: `node \"$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js\" --run test/specific.test.ts`\n  - `packages/tui` (`node:test`): `node --test test/specific.test.ts`\n- If you create or modify a test file, run it and iterate on test or implementation until it passes.\n- For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` + the faux provider. No real provider APIs, keys, or paid tokens.\n- When regressions tests for fixing a github issue, add a comment with the github issue number next to the test.\n- For ad-hoc scripts, `write` them to a temp file (e.g. `/tmp`), run, edit if needed, remove when done. Don't embed multi-line scripts in `bash` commands.\n- Never commit unless the user asks.\n\n## Dependency and Install Security\n\n- Treat npm dep and lockfile changes as reviewed code. Direct registry deps, including internal workspace deps, stay pinned to exact versions.\n- When updating `undici`, you MUST read its changelog/release notes for the target version and evaluate whether any changes may affect functionality before applying the update.\n- Hydrate/update locally with `npm install --ignore-scripts`; clean/CI-style with `npm ci --ignore-scripts`. Don't run lifecycle scripts unless the user asks.\n- If dep metadata changes, refresh `package-lock.json` with `npm install --package-lock-only --ignore-scripts`.\n- If `packages/coding-agent/install-lock/` needs regen, run `node scripts/generate-coding-agent-install-lock.mjs` (verify with `--check` or `npm run check`). New deps with lifecycle scripts require review and an explicit allowlist entry in that script; never add one silently.\n- Pre-commit blocks lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1`. Don't bypass unless the user wants the lockfile change committed.\n\n## Git\n\nMultiple pi sessions may be running in this cwd at the same time, each modifying different files. Git operations that touch unstaged, staged, or untracked files outside your own changes will stomp on other sessions' work. Follow these rules:\n\nCommitting:\n\n- Only commit files YOU changed in THIS session.\n- Stage explicit paths (`git add <path1> <path2>`); never `git add -A` / `git add .`.\n- Before committing, run `git status` and verify you are only staging your files.\n- `packages/ai/src/models.generated.ts` may always be included alongside your files.\n- Message format: `{feat,fix,docs}[(ai,tui,agent,coding-agent)]: <commit message> (optionally multiple lines)`. Message is informative and concise.\n\nNever run (destroys other agents' work or bypasses checks):\n\n- `git reset --hard`, `git checkout .`, `git clean -fd`, `git stash`, `git add -A`, `git add .`, `git commit --no-verify`.\n\nIf rebase conflicts occur:\n\n- Resolve conflicts only in files you modified.\n- If a conflict is in a file you did not modify, abort and ask the user.\n- Never force push.\n\n## Issues and PRs\n\nSee `CONTRIBUTING.md` for the contributor gate (auto-close workflows, `lgtm`/`lgtmi`, quality bar).\n\nWhen reviewing PRs:\n\n- Do not run `gh pr checkout`, `git switch`, or otherwise move the worktree to the PR branch unless the user explicitly asks.\n- Use `gh pr view`, `gh pr diff`, `gh api`, and local `git show`/`git diff` against fetched refs to inspect PR metadata, commits, and patches without changing branches.\n- If you need PR file contents, fetch/read them into temporary files or use `git show <ref>:<path>` without switching branches.\n\nWhen creating issues:\n\n- Add `pkg:*` labels for affected packages (`pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui`); use all that apply.\n\nWhen posting issue/PR comments:\n\n- Write the comment to a temp file and post with `gh issue/pr comment --body-file` (never multi-line markdown via `--body`).\n- Keep comments concise, technical, in the user's tone.\n- End every AI-posted comment with the AI-generated disclaimer line specified by the originating prompt (e.g. `This comment is AI-generated by `/wr``).\n\nWhen closing issues via commit:\n\n- Include `fixes #<number>` or `closes #<number>` in the message so merging auto-closes the issue. For multiple issues, repeat the keyword per issue (`closes #1, closes #2`); a shared keyword (`closes #1, #2`) only closes the first.\n\n## Testing pi Interactive Mode with tmux\n\nFor testing pi's interactive mode, load and follow [.pi/skills/interactive-testing.md](.pi/skills/interactive-testing.md).\n\n## Changelog\n\nLocation: `packages/*/CHANGELOG.md` (one per package).\n\nSections under `## [Unreleased]`: `### Breaking Changes` (API changes requiring migration), `### Added`, `### Changed`, `### Fixed`, `### Removed`.\n\nRules:\n\n- All new entries go under `## [Unreleased]`. Read the full section first and append to existing subsections; never duplicate them.\n- Released version sections (e.g. `## [0.12.2]`) are immutable; never modify them.\n- Do not create changelog entries when working on a branch other than `main` or pull request\n\nAttribution:\n\n- Internal (from issues): `Fixed foo bar ([#123](https://github.com/earendil-works/pi/issues/123))`\n- External contributions: `Added feature X ([#456](https://github.com/earendil-works/pi/pull/456) by [@username](https://github.com/username))`\n\n## Releasing\n\nFor release preparation, publishing, verification, or recovery, load and follow [.pi/skills/release.md](.pi/skills/release.md).\n\n## User Override\n\nIf the user's instructions conflict with any rule in this document, ask for explicit confirmation before overriding. Only then execute their instructions.\n\n<!-- File: .pi/skills/add-llm-provider.md (Tokens: ~726 | Category: anthropic-skill) -->\n---\nname: add-llm-provider\ndescription: Checklist for adding a new LLM provider to packages/ai. Covers core types, provider implementation, lazy registration, model generation, the full test matrix, coding-agent wiring, and docs.\n---\n\n# Adding a New LLM Provider (packages/ai)\n\nA new provider touches multiple files. Work through these steps in order.\n\n## 1. Core Types (`packages/ai/src/types.ts`)\n\n- Add API identifier to `Api` type union (e.g. `\"bedrock-converse-stream\"`).\n- Create options interface extending `StreamOptions`.\n- Add mapping to `ApiOptionsMap`.\n- Add provider name to `KnownProvider` type union.\n\n## 2. Provider Implementation (`packages/ai/src/providers/`)\n\nCreate a provider file exporting:\n\n- `stream<Provider>()` returning `AssistantMessageEventStream`.\n- `streamSimple<Provider>()` for `SimpleStreamOptions` mapping.\n- Provider-specific options interface.\n- Message/tool conversion functions.\n- Response parsing that emits standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`).\n\n## 3. Provider Exports and Lazy Registration\n\n- Add a package subpath export in `packages/ai/package.json` pointing at `./dist/providers/<provider>.js`.\n- Add `export type` re-exports in `packages/ai/src/index.ts` for provider option types that should remain available from the root entry.\n- Register the provider in `packages/ai/src/providers/register-builtins.ts` via lazy loader wrappers; do not statically import provider implementation modules there.\n- Add credential detection in `packages/ai/src/env-api-keys.ts`.\n\n## 4. Model Generation (`packages/ai/scripts/generate-models.ts`)\n\n- Add logic to fetch/parse models from the provider source.\n- Map to the standardized `Model` interface.\n\n## 5. Tests (`packages/ai/test/`)\n\n- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API impl such as `openai-completions`.\n- Add the provider to the broader matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.\n- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (e.g. GPT and Claude), add at least one pair per family.\n- For non-standard auth, create a utility (e.g. `bedrock-utils.ts`) with credential detection.\n\n## 6. Coding Agent (`packages/coding-agent/`)\n\n- `src/core/model-resolver.ts`: add default model ID to `defaultModelPerProvider`.\n- `src/core/provider-display-names.ts`: add API-key login display name so `/login` and related UI show the provider for built-in API-key auth.\n- `src/cli/args.ts`: add env var documentation.\n- `README.md`: add provider setup instructions.\n- `docs/providers.md`: add setup instructions, env var, and `auth.json` key.\n\n## 7. Documentation\n\n- `packages/ai/README.md`: add to providers table, document options/auth, add env vars.\n- `packages/ai/CHANGELOG.md`: add entry under `## [Unreleased]`.\n\n<!-- File: .pi/skills/github-work.md (Tokens: ~320 | Category: anthropic-skill) -->\n---\nname: github-work\ndescription: Work on GitHub issues and pull requests of this repo. Use when asked to investigate, analyze, fix, or review an issue or PR, and when asked to wrap up such work (changelog, comment, commit, push, close).\n---\n\n# Working on GitHub issues and PRs\n\nThe repo's prompt templates in `.pi/prompts/` (two directories above this skill) define the workflow. Read the matching template in full and follow it. Where a template says `$ARGUMENTS` or `$@`, substitute the issue or PR number or URL from the request, and any extra instructions the user gave.\n\n| Request | Template |\n|---|---|\n| Investigate, analyze, or fix an issue | `.pi/prompts/is.md` |\n| Review a pull request | `.pi/prompts/pr.md` |\n| Wrap up issue or PR work: changelog, final comment, commit, push, close | `.pi/prompts/wr.md` |\n\nRules:\n\n- `is.md` analyzes and proposes only. Implement only when the user asks for it, then keep following AGENTS.md (checks, tests, changelog rules).\n- `pr.md` never checks out or switches to the PR branch.\n- Run `wr.md` only when the user asks to wrap up, commit, or push. Post at most one final GitHub comment per session, ending with the exact disclaimer line it specifies.\n- When a request spans several steps, for example \"fix issue X and wrap it up\", read every template involved before starting.\n\n<!-- File: .pi/skills/interactive-testing.md (Tokens: ~232 | Category: anthropic-skill) -->\n---\nname: interactive-testing\ndescription: Test and debug pi's interactive mode in a controlled tmux terminal. Use for TUI behavior checks and interactive release smoke tests.\n---\n\n# Testing pi Interactive Mode with tmux\n\nRun the TUI in a controlled terminal (from the repo root, two directories above this skill):\n\n```bash\ntmux new-session -d -s pi-test -x 80 -y 24\ntmux send-keys -t pi-test \"./pi-test.sh\" Enter\nsleep 3 && tmux capture-pane -t pi-test -p     # capture after startup\ntmux send-keys -t pi-test \"your prompt here\" Enter\ntmux send-keys -t pi-test Escape               # special keys (also C-o for ctrl+o, etc.)\ntmux kill-session -t pi-test\n```\n\nFor release smoke tests, start the tmux session with `-c /tmp` and replace `./pi-test.sh` with the absolute path to the release binary. Test both Node and Bun binaries separately, submit a prompt, and wait for the model reply; startup alone is not a passing smoke test.\n\n<!-- File: .pi/skills/release.md (Tokens: ~1105 | Category: anthropic-skill) -->\n---\nname: release\ndescription: Prepare, publish, verify, and recover pi releases. Use for release preparation, local release smoke tests, publishing, and failed release CI or announcements.\n---\n\n# Releasing pi\n\nRun repository commands from the repo root (two directories above this skill), unless instructed otherwise.\n\n**Lockstep versioning**: all packages share one version; every release updates all together. `patch` = fixes + additions, `minor` = breaking changes. No major releases.\n\n1. **Update CHANGELOGs**: ask the user whether they ran the `/cl` prompt on the latest commit on `main`. If not, they must run `/cl` first to audit and update each package's `[Unreleased]` section before releasing.\n\n2. **Local smoke test**: build an unpublished release and smoke test from outside the repo (so it can't resolve workspace files):\n   ```bash\n   npm run release:local -- --out /tmp/pi-local-release --force\n   cd /tmp\n\n   # Node package install smoke tests\n   /tmp/pi-local-release/node/pi --help\n   /tmp/pi-local-release/node/pi --version\n   /tmp/pi-local-release/node/pi --list-models\n   /tmp/pi-local-release/node/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/node/pi\n\n   # Bun binary smoke tests\n   /tmp/pi-local-release/bun/pi --help\n   /tmp/pi-local-release/bun/pi --version\n   /tmp/pi-local-release/bun/pi --list-models\n   /tmp/pi-local-release/bun/pi -p \"Say exactly: ok\"\n   /tmp/pi-local-release/bun/pi\n   ```\n   Verify both Node and Bun startup, model/account listing, interactive startup, and at least one real prompt with the intended default provider. The bare commands `/tmp/pi-local-release/node/pi` and `/tmp/pi-local-release/bun/pi` start interactive mode; run each in tmux, submit a prompt, and wait for the model reply before considering the interactive smoke test passed. Failures are release blockers unless the user explicitly accepts the risk.\n\n   Load and follow [interactive-testing.md](interactive-testing.md) for the tmux workflow. Start each release binary from `/tmp`, not the repo root.\n\n3. **Run the release script**:\n   ```bash\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:patch    # fixes + additions\n   PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:minor    # breaking changes\n   ```\n   If the test suite already passed in `npm run release:local` and only fails from machine load, `PI_RELEASE_SKIP_TESTS=1` skips `./test.sh` in the release script; checks and the packed install check still run. Use it only with the user's approval.\n\n   Use `npm_config_min_release_age=0` only for the release command. The repo's normal npm age gate can otherwise block the release lockfile refresh when the current workspace package version was published recently. Review any lockfile or install lock diffs the release creates before push.\n\n   The release script pins the newest published model catalog for Nix (`nix/model-catalog.json`) and builds the checkout with it as the Nix build does (it fails if that catalog predates main's model changes; publish one first), bumps all package versions, updates changelogs, regenerates release artifacts, runs `npm run check`, commits `Release vX.Y.Z`, tags `vX.Y.Z`, adds fresh `## [Unreleased]` changelog sections, commits `Add [Unreleased] section for next cycle`, then pushes `main` and the tag. Do not rerun the release script after a tag was pushed.\n\n4. **CI verifies and announces the npm release**: pushing the `vX.Y.Z` tag triggers `.github/workflows/build-binaries.yml`. The `publish-npm` job uses npm trusted publishing through GitHub Actions OIDC with environment `npm-publish`; no local `npm publish`, `npm whoami`, OTP, or WebAuthn flow is required. After publishing, `announce-pi-dev-release` verifies every public workspace package resolves at the exact release version and that its npm tarball is available, then writes the verified release marker to R2. `pi.dev/api/latest-version` reads that marker; it must never announce a release from npm before this job succeeds.\n\n5. **CI builds the Nix release**: the tag push also triggers `.github/workflows/nix.yml`, which builds the tagged flake on Linux and macOS and then fast-forwards the `stable` branch to the release commit. `nix run github:earendil-works/pi/stable` runs the latest release. If the Nix build fails, `stable` stays on the previous release; the tag cannot be fixed, so fix `main` and ship the fix in the next release.\n\n6. **If CI publish or announcement fails**: inspect the failed job. The publish helper is idempotent and skips package versions already present on npm; the announcement job rechecks availability before updating the R2 marker. Rerun the failed job or workflow after fixing CI or transient npm issues. Do not rerun `npm run release:patch` or `npm run release:minor` for the same version.\n\n<!-- File: packages/chord/docs/guide.md (Tokens: ~10885 | Category: anthropic-skill) -->\n# Chord usage guide\n\n> Describes the runtime in `src`. The [specification](spec.md) defines its semantics and lists known limitations.\n\nChord builds applications out of **objects** that live in several processes (a server, workers, browser tabs) and can be **hot reloaded** while they run.\n\n- An **object type** is declared by a contract: a name, an input (the object's address) and members. A plugin's\n  **facet** (its code for one host: server, worker, browser) serves object types.\n- An **object** is one instance of an object type: state plus code at an address. It is created on demand and lives\n  while someone references it.\n- Everyone gets a **reference**, never the object. References work the same locally and across processes, and survive reloads.\n- Every object knows its **principal**: an owner and a guest get different objects, with different methods.\n- **State** is observable JSON. Remote viewers get a snapshot followed by deltas.\n- **Hot reload** swaps code while it runs. Work in flight finishes on the code it started with, state is kept, and incompatible changes migrate at a safe point.\n\n## Contents\n\n- [Quick start](#quick-start)\n- [Contracts](#contracts)\n- [Serving objects](#serving-objects)\n- [Declarations](#declarations)\n- [Activation](#activation)\n- [Principals and variants](#principals-and-variants)\n- [References](#references)\n- [State](#state)\n- [Calls, ctx and roots](#calls-ctx-and-roots)\n- [Lifetime](#lifetime)\n- [Registries](#registries)\n- [Plugins](#plugins)\n- [Hot reload](#hot-reload)\n- [Hosts and links](#hosts-and-links)\n- [Durable](#durable)\n- [Errors](#errors)\n- [Rules of thumb](#rules-of-thumb)\n\n## Quick start\n\n```ts\nimport { Type } from \"typebox\";\nimport { createHost, method, remote } from \"@earendil-works/chord\";\n\n// contract.ts: shared by provider and consumers\nexport const Counter = remote(\"demo.counter\", {\n\tversion: 1,\n\tinput: Type.Object({ name: Type.String() }),\n\tvariants: {\n\t\tdefault: {\n\t\t\tstate: { count: Type.Number() },\n\t\t\tmethods: { increment: method(Type.Object({ by: Type.Number() })) },\n\t\t},\n\t},\n});\n\nconst host = createHost({ name: \"server\", self: { kind: \"server\" } });\n\nhost.serve(Counter, ({ name }, _principal, self) => {\n\tconst count = self.state(\"count\", Type.Number(), 0);\n\treturn {\n\t\tvariant: \"default\",\n\t\tcount,\n\t\tincrement: async ({ by }, ctx) => {\n\t\t\tcount.change(ctx, (n) => n + by);\n\t\t},\n\t};\n});\n\nawait host.root(async (ctx) => {\n\tconst clicks = await ctx.get(Counter, { name: \"clicks\" });  // ready, with its state; released when this root ends\n\tclicks.count.subscribe((n) => console.log(\"count\", n));\n\tawait clicks.increment({ by: 2 }, ctx);  // logs 0, then 2\n});\n```\n\n## Contracts\n\nA contract is a runtime value: a name, a version number, and a description of the members. Shipped bundles have no TypeScript, so everything the runtime checks must be in the contract. TypeScript types are inferred from it.\n\n### Remote types\n\nRemote types can be reached from other hosts. Everything that crosses the wire has a schema, and the provider validates it.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),  // lives on `session:S1` for `{ session: \"S1\", … }`\n\tvariants: {\n\t\tviewer: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t},\n\t\tparticipant: {\n\t\t\tstate: { view: ConversationViewSchema },\n\t\t\tmethods: {\n\t\t\t\tsend: method(SendInputSchema, Type.Object({ submissionId: Type.Number() })),\n\t\t\t\tstop: method(),  // no arguments, no result: `chat.stop(ctx)`\n\t\t\t},\n\t\t},\n\t},\n});\n```\n\n**Where a remote type lives** is part of its contract, so no caller names a host:\n\n- `host: hostNamed(\"server\")`: every object lives on the host named `server`.\n- `host: hostFamily(\"session:\", \"session\")`: an object lives on `session:` + its input's `session` field, a required\n  string. TypeScript checks the field name against the input.\n\nA `get` normalizes the input (defaults count), resolves the host, and acquires the object here if that is this host,\notherwise over the link to it. `serve` refuses a type whose placement does not include the serving host, so a misplaced\ntype fails at startup or at the reload that adds it. The host that gets a hold checks that its own contract places the\ninput on itself, and a relay checks the hold's target against its contract token: a client never chooses where an\nobject lives. The placement is part of the description and the fingerprint: moving a type is a breaking change, and\nreferences to it get `upgrade_required`.\n\n### Local types\n\nLocal types are reachable only inside their host. Their members may be anything: functions, callbacks, native objects. Non-JSON members get opaque or function descriptors.\n\n```ts\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(HitSchema)),\n\t\tonChange: method(Type.Function([ChangeSchema], Type.Void()), UnsubscribeSchema),\n\t\tharness: opaque(\"pi.durable-harness@1\"),\n\t},\n});\n```\n\n### Inputs identify objects\n\n`{ session: \"S1\", conversation: 1 }` always means the same object. Inputs are normalized (defaults applied, unknown fields removed), validated, and serialized canonically. Fields equal to their default are left out, so adding a new optional field with a default doesn't change existing addresses. Changing a default, or how inputs are normalized, needs a new type name.\n\n### Versions and compatibility\n\nEvery bundle carries its own copy of each contract it was built against. When a consumer binds to a provider, the two copies are compared:\n\n> A consumer built against version M binds to a provider at version N if **N = M and the provider's description covers the consumer's**. Otherwise the provider must offer an adapter in `accepts[M]`, or binding fails with `contract_mismatch`.\n\n\"Covers\" means the provider still offers everything the consumer was built against:\n- every member exists with the same kind;\n- methods accept everything the consumer can send (new arguments must be optional) and return at least what it expects (extra result fields are fine);\n- state values fit the consumer's schema;\n- opaque members have the same name and version.\n\n| Change | Version | Result |\n| --- | --- | --- |\n| Add a method, an optional argument, a result field, a state field | keep | binds |\n| Remove or rename a member; make an argument required; change a type | **bump** | old consumers need `accepts`, or must be reloaded too |\n| Same shape, different meaning (`query` is now a regular expression) | **bump** | as above; undetectable, so it's on you |\n| Forgot to bump on a breaking shape change | keep | refused: \"consumer needs member `search`\" |\n\n```ts\n// version 2 changed what `search` means; old consumers keep working through an adapter\nhost.serve(Index, indexFactory, {\n\taccepts: {\n\t\t1: (current) => ({ search: ({ query }, ctx) => current.search({ query: escapeRegExp(query) }, ctx) }),\n\t},\n});\n```\n\n## Serving objects\n\n```ts\nhost.serve(Type, (input, principal, self) => implementation, options);\n```\n\n- The **factory** runs synchronously. It declares what the object needs and returns its implementation: methods and state members, plus `variant` for remote types. Returning a promise throws.\n- There is one **object** per `(type, input, principal)`. It holds the object's state, resources and references, and survives reloads. Only the implementation is replaced.\n- **Options:**\n  - `accepts`: adapters for consumers built against older contract versions;\n  - `lifetime`: `\"referenced\"` (default) creates the object at its first `get` and disposes it after the last reference\n    is released; `\"host\"` creates it when the plugin loads and keeps it until the host closes (see [Lifetime](#lifetime));\n  - `linger`: how long to keep a referenced object after the last reference is released.\n\n## Declarations\n\nA reload runs the factory again over the same object. Declarations that keep something across a reload have a **name**: the new code's `status` cell is the old code's `status` cell, with its value. Derivations and tasks hold nothing across a reload and have no name.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 });\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async ({ signal }) => openSqlite(statusPath(session), { signal }),\n\t\tclose: async (db) => db.close(),\n\t});\n\tself.on(\"catalogChanges\", catalog.rows, (rows, ctx) => errors.replace(ctx, checkRows(rows)));\n\tself.task(async (ctx) => {\n\t\twhile (!ctx.signal.aborted) {\n\t\t\tawait sleep(10_000, ctx.signal);\n\t\t\tawait db.value.run(\"INSERT INTO history VALUES (?)\", [JSON.stringify(status.value)]);\n\t\t}\n\t});\n\tconst summary = self.derive([status, errors], (s, e) => ({ ...s, failing: e.length > 0 }));\n\treturn { summary, report: reportMethod(status) };  // exposed state is what the implementation returns\n});\n```\n\n| Declaration | What it is | On reload |\n| --- | --- | --- |\n| `self.state(name, schema, initial \\| { init }, { version?, migrate? })` | an observable JSON cell; the schema is required | same cell and value (`init` does not run); migrated when its version rises |\n| `self.get(Type, input, { at? })` | a reference to another object; its identity is the address | kept if declared again, otherwise released |\n| `self.resource(name, { version, deps?, exclusive?, open, close? })` | a connection, watch, socket, port | kept; reopened only if `version`, `deps` or `exclusive` change |\n| `self.on(name, source, handler)` or `self.on(name, source, select, handler)` | reacts to state or to an event source | the same subscription, with the new handler, as long as the source and `select` are unchanged |\n| `self.task(async (ctx) => ...)` | long-running background work | the old run is aborted through `ctx.signal`; the new code starts once it finished (at most `taskStopDeadline`, default 5 s) |\n| `self.derive(sources, fn)` | live projection of state; `fn` is synchronous | a new derivation, computed when the reload commits |\n| `self.register(Registry, { key: value })` | an entry in a registry | replaced at commit |\n| `self.expose(resource)` | a member backed by a resource (local types) | follows the resource |\n\nNames are strings: declaring one twice in a run fails (`invalid/duplicate`). Each kind has its own names, so a cell and a resource may share one.\n\nA listener keeps its subscription across reloads when it is declared on the same resource (or source) with the same `select`: the same function, or one with the same source text. `select` is not called again, so a source that accepts a single subscriber (a Durable watch) survives reloads, and `select` may wrap the value in a new adapter object. A `select` whose result depends on captured values that change between reloads is not noticed; give such a listener a new name.\n\n**A listener on state reconciles.** A listener on state (a cell, a derivation, a reference's state member) gets the current value when it starts, then every new value. When a reload replaces its source (a derivation is new at every reload), it subscribes again and gets the current value again. Write its handler to bring things in line with the value it gets (set, compare, start or stop what the value says), not to count or append per call. An event source (a resource's events) delivers no current value.\n\nA name you no longer declare is released, closed or stopped, after the old code has finished what it was doing.\n\n**State that must survive a reload lives in cells,** never in closure variables. A closure variable lasts as long as one version of the code.\n\n```ts\n// wrong: reset on every reload\nhost.serve(Visits, (_, _p, self) => {\n\tlet visits = 0;\n\treturn { hit: async (ctx) => ++visits };\n});\n\n// right\nhost.serve(Visits, (_, _p, self) => {\n\tconst visits = self.state(\"visits\", Type.Number(), 0);\n\treturn {\n\t\thit: async (ctx) => {\n\t\t\tvisits.change(ctx, (n) => n + 1);\n\t\t\treturn visits.value;\n\t\t},\n\t};\n});\n```\n\n**Methods** are declared with `method(args?, result?)` in the contract. Without `args` a method takes none: callers write `ref.hit(ctx)` (or `ref.hit()`), and its implementation is `(ctx) => …`. Without `result` it returns nothing: the implementation returns nothing, and callers get `undefined`. `method(undefined, Type.Number())` takes no arguments and returns a number.\n\n## Activation\n\nThe factory is synchronous, but activation isn't. `self.get` and `self.resources` only *declare*; the slow work happens afterwards:\n\n```text\nget(Index, { session: \"S1\" })\n 1. run the factory               → declarations\n 2. open declared resources        → e.g. connect to SQLite\n 3. wait for declared refs        → dependencies activate first\n 4. run the cells' init           → e.g. read the rows into a cell\n 5. live                           → refs become \"live\"; listeners and tasks start\n```\n\n```ts\nhost.serve(Index, ({ session }, _host, self) => {\n\tconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(indexPath(session)), close: (db) => db.close() });\n\treturn {\n\t\tsearch: async ({ query }, ctx) => db.value.all(\"SELECT entryId, text FROM fts WHERE fts MATCH ?\", [query]),\n\t};\n});\n\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tconst index = self.get(Index, { session });  // a placeholder until Index is live\n\treturn {\n\t\tvariant: \"participant\",\n\t\tfind: async ({ query }, ctx) => index.search({ query }, ctx),  // only ever runs once Chat (and Index) are live\n\t\t// ...\n\t};\n});\n```\n\n**Rules:**\n- Inside the factory, never use what you declared: `db.value` and `index.someState.value` throw `not_ready`, and calling a reference throws. Use them in methods, listeners, tasks and `init`.\n- **The object goes live only with its references ready**: live, and (for a remote reference) every state member of its variant hydrated. The same holds for state it forwards or derives from a remote reference it holds some other way: it goes live once that state has its value. A reference that is not ready after the host's `readyTimeout` (default 60 s) fails the activation with `unavailable/ready_timeout`; a reference cycle across hosts ends that way instead of hanging.\n- **Concurrent `get`s** of one address share one activation, so the database opens once.\n- **Failure propagates:** if `openSqlite` throws, `Index` fails, `Chat` fails with that cause, and every waiting `get` rejects. The next `get` tries again.\n- **Abandoned activations:** if everyone releases before activation finishes, the result is disposed.\n\n### Startup readiness and parent admission\n\n`await host.load(facets)` and `await host.install(facet)` wait for the `lifetime: \"host\"` objects they install to\nbecome live: resources are open, declared references are live, and registrations are published. All facets in a load\nare installed before activation is awaited, so a host object can use a provider in a later facet. Referenced objects\nare created only when something references them. Factory, dependency and resource errors reject startup with the\nreference's activation error; host object failures also reach `onError`. Installation is not rolled back on failure: close the host\nwhen abandoning startup. `install` registers synchronously, so setup/registration errors still throw synchronously.\n\n`await host.ready()` waits for the host objects present when called, including direct\n`host.serve(..., { lifetime: \"host\" })` and previously deferred loads. It is an activation barrier, not a continuous health check, a wait for tasks to\nfinish. Later installations require another readiness wait.\n\nA worker may need to open its local Harness and announce control readiness **before** the parent accepts its link.\nA host object reporting to the server's SessionStatus cannot become live before that acceptance. Awaiting the\nwhole load before announcing control readiness would deadlock. Defer the whole barrier explicitly:\n\n```ts\n// Connect the upstream link first; its outgoing references can queue until the parent accepts it.\nawait worker.load(workerFacets, { awaitHostObjects: false });  // also supported by install\nconst harness = await worker.get(Harness, {});        // local bootstrap only; no upstream dependency\nannounceControlReady();                               // parent can now accept the worker link\nawait worker.ready();                                 // every host object live, including upstream references\nharness.release();\n```\n\n`awaitHostObjects: false` still starts every host object; it skips waiting, not activation. There is no\nautomatic classification of upstream dependencies. Choose a local bootstrap reference with no upstream dependency,\nconnect before loading upstream consumers, and let the parent admit the link before the full barrier. Control\nreadiness means admission is possible, not that every host object is live. Keep worker bootstrap counted as busy\nuntil the full barrier settles, and report a later failure through the worker's fatal/startup-error path. Never await a\nreference whose object needs the parent (a remote one, or a local one that holds one) before the parent admits the link.\n\nTo hand out a native object a resource opens (a local type's `opaque` member), expose the resource. References read its value once the object is live, and `undefined` before:\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", { version: 1, open: () => DurableHarness.open(storage), close: (h) => h.close() });\n\treturn { harness: self.expose(durable) };  // contract member: harness: opaque<DurableHarness>(\"pi.durable-harness@1\")\n});\n\nconst harness = await ctx.get(Harness, {});\nharness.harness.resume();  // the DurableHarness itself\n```\n\n## Principals and variants\n\nThe application defines its principals and how they're keyed. Chord invents none: the app gives every principal a\nrequest can act as.\n\n```ts\ntype Principal =\n\t| { kind: \"owner\"; access: \"local\" | \"relay\" }\n\t| { kind: \"guest\"; id: string; name: string; session: string; mode: \"view\" | \"full\"; revision: number }\n\t| { kind: \"worker\"; session: string; generation: number }\n\t| { kind: \"server\" }   // the parent's own requests, as a worker sees them\n\t| { kind: \"host\" };    // a host's own code\n\nconst host = createHost<Principal>({\n\tname: \"server\",\n\tself: { kind: \"host\" },\n\tprincipalKey: (p) =>\n\t\tp.kind === \"owner\" ? `owner:${p.access}` :\n\t\tp.kind === \"guest\" ? `guest:${p.id}:r${p.revision}` :\n\t\tp.kind === \"worker\" ? `worker:${p.session}:${p.generation}` :\n\t\tp.kind,\n});\n```\n\n- `createHost<Principal>` types every factory's `principal`, `ctx.principal`, routes, `accept` and\n  `invalidatePrincipal` patterns, so code narrows on `kind` without casts. Facet modules say it too:\n  `defineFacet<Principal>((api) => …)` (an app usually exports its own `defineFacet` and `Ctx` aliases). Without a type\n  argument, principals are `unknown`.\n- There is one object per principal key: two tabs of the owner share an object, and a guest gets its own.\n- Code inside a host acts as `self`, so internal objects are shared. A principal with the same key as `self` is the\n  host's own, whichever object carries it.\n- A link stamps the principal of whatever connects to it. Callers can never supply their own.\n- A child host names what its parent's own requests act as: `connect(link, { principal })` (or\n  `upstream: { link, principal }`). Requests the parent relays act as the caller it forwards.\n- A route names what the hosts it resolves act as when they call back: `route(prefix, { …, principal: (name) => … })`.\n\n**A remote factory picks a variant:**\n\n```ts\nhost.serve(Chat, ({ session, conversation }, principal, self) => {\n\tif (!canView(principal, session)) throw forbidden();\n\tconst c = self.get(Conversation, { session, conversation });  // as host: one shared instance\n\tif (!canSend(principal)) return { variant: \"viewer\", view: c.view };\n\tconst author = authorOf(principal);\n\treturn {\n\t\tvariant: \"participant\",\n\t\tview: c.view,\n\t\tsend: async (input, ctx) => c.submit(input, author, ctx),\n\t\tstop: async (ctx) => {\n\t\t\tawait c.stop(author, ctx);\n\t\t},\n\t};\n});\n```\n\n```ts\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // on session:S1\nif (chat.variant === \"participant\") showComposer(chat.send);  // narrows: `send` exists only there\nelse showReadOnly();\n```\n\n**What a variant doesn't do:**\n- **It isn't complete authorization.** Methods still check their arguments: that a submission belongs to this conversation, or a session ID to this principal.\n- **It doesn't cover flags that change while you hold the object.** Those are state, for example `you: { canDraw: boolean }`.\n- **Revocation** is `host.invalidatePrincipal(match)`. It force-disposes matching objects, here and over links, and their references become `gone`. A new `get` runs the factory with the new grants.\n\nLocal types have no variants. Their only callers are inside the host, acting as its `self`. If a local object needs to know who's acting, pass that explicitly, like `author` above.\n\n## References\n\n| Where | API | Released |\n| --- | --- | --- |\n| a factory | `self.get(Type, input)`: synchronous; the object goes live after it is ready | with the object (or its replaced declaration) |\n| a call | `await ctx.get(Type, input, { state? })` | when the call settles |\n| outside Chord | `await host.get(Type, input, { signal?, state? })` | `ref.release()` |\n| a UI runtime | `host.hold(Type, input)`: synchronous, possibly `loading` | `ref.release()` |\n\n```ts\nconst chat = await ctx.get(Chat, input);  // ready: live, every state member of its variant hydrated\nchat.status;  // \"loading\" | \"live\" | \"released\" | \"gone\"\nchat.variant;  // the current variant; a multi-variant reference narrows on it\nchat.shape;  // { variant, members, contract }\nconst send = chat.send;  // a stable function: always dispatches to the current code\nchat.view.value;  // the current value, typed T\nchat.view.subscribe((v) => render(v));\nchat.onStatus((status, error) => { if (status === \"gone\") reconnect(error); });\nchat.release();\n```\n\n- **A reference you hold is usable:** an awaited `get` resolves once the object is live and every state member of its variant has its first snapshot, local or remote. It rejects with the activation error (`forbidden`, unreachable, …), with `aborted` (a `ctx.get` whose ctx is aborted or whose call settles first; `host.get`'s `signal`), or with `unavailable/ready_timeout` after the host's `readyTimeout`. Only that reference is released then.\n- **A held reference stays subscribed** to the state of its variant, with or without listeners, until it is released. After a link loss the last values stay readable while it re-holds (status `loading`).\n- **Reading state before the first hydration throws `not_ready`** (a `hold` that is still loading, a member read inside a factory). A member outside the current variant throws `not_in_variant`; an ended reference throws its end (`released`, or the `gone` error). A listener added before the hydration gets its snapshot then.\n- **`state: false`** gives a method-only reference: it receives no state (unless another reference to the same object wants it) and is ready once its object is live.\n- **Calls on a `loading` reference wait** until it's live, respecting `ctx` cancellation and a deadline.\n- A member missing from the current variant rejects with `not_in_variant`.\n- After `release()`, every member rejects with `released`.\n- **`gone`** means the object was forced away while referenced: revocation, archive, its host dying. Call `get` again, or show \"unavailable\". `onStatus` reports every status change (`live`, `loading` while a link reconnects, `released`, `gone` with the error), so a UI needn't poll.\n- **References to one remote object share one binding** per host and key (type, normalized input, principal key, contract fingerprint): one hold over the link, one stream per state member, calls over the same hold. Each reference keeps its own status, observers and `release`. With `createHost({ linger: { types: { [typeName]: ms }, cap } })`, a binding whose last reference is released stays held (and hydrated) for `ms`, at most `cap` (default 4) at a time, so taking the reference again costs nothing on the link.\n- **Return values are values.** Anything long-lived you want to share is an object you reach with `get`.\n\n## State\n\n```ts\nconst todos = self.state(\"todos\", Type.Array(TodoSchema), []);\n\ntodos.change(ctx, (draft) => {\n\tdraft.push({ id: \"t1\", text: \"Write docs\", done: false });\n});\ntodos.change(ctx, (draft) => {\n\tdraft[0].done = true;\n});\ntodos.replace(ctx, []);\n\ntodos.value;  // immutable\ntodos.subscribe((value) => render(value));\n\n// mirroring an external immutable store: subscribers get a diff, not the whole value\nview.replace(ctx, conversation.view);                    // diffs when the new value reuses old containers\nview.replace(ctx, rebuilt, { diff: true });              // always diff\n```\n\n- A `change` is atomic: one revision, or nothing if the callback throws.\n- `change` only touches the paths you mutate, so fields your code doesn't know about survive. Use `replace` only where your code owns the whole value.\n- Values are immutable. Never mutate what you read or receive.\n- Remote references get a snapshot of each state member of their variant, then deltas. After a reconnect or a reload they get a `reset` snapshot, and nothing is replayed.\n- `replace` sends a structural diff when the new value shares containers with the old one (a new revision of the same state), and a single replacement otherwise. `{ diff: true | false }` forces either. The value is the same either way.\n- An implementation may expose another object's state member as its own: `return { variant: \"viewer\", view: conversation.view }`. The object goes live only once that member has its value, local or remote.\n\n**A cell whose first value comes from a source** declares `{ init }` instead of an initial value:\n\n```ts\nconst db = self.resource(\"db\", { version: 1, open: () => openSqlite(path), close: (db) => db.close() });\nconst notes = self.state(\"notes\", Type.Array(NoteSchema), { init: () => db.value.all(\"SELECT * FROM notes\") });\n```\n\n`init(ctx)` runs once the resources are open and the references live, before the object goes live, so no method,\nlistener, task or subscriber sees a value that did not come from the source. A task that loads the value instead\nstarts only after the object is live: a method called in between answers from the placeholder (a note that exists\nreads as unknown), and an async load can overwrite a write made during its `await`. `init` may be async and gets a\n`ctx` to call other objects. The `init`s of one object run in parallel, so one must not read another's cell. A throw or\nan invalid value fails the activation. A reload keeps the cell and its value and does not run `init` again; a cell the\nreload adds runs its `init` while the reload prepares, and a failure refuses the reload.\n\n`init` reads a source the object has anyway. It saves nothing: what must outlive the process is written to that source\n(a database, a file, a Durable document) by the code that changes it.\n\n```ts\nconst visible = self.derive([catalog.rows], (rows) => rows.filter((r) => visibleTo(principal, r)));\nconst openCount = self.derive([todos], (t) => t.filter((x) => !x.done).length);\n```\n\nDerivation functions are synchronous. A derivation has no name: every reload makes a new one, computed when the\nreload commits.\n\n## Calls, ctx and roots\n\nEvery method gets `(args, ctx)`, or `(ctx)` when it takes no arguments. Pass `ctx` on to everything you call:\n\n```ts\nsend: async (input, ctx) => {\n\tctx.signal.throwIfAborted();\n\tconst uploads = await ctx.get(Uploads, {});  // a reference owned by this call; released when it settles\n\tconst files = await uploads.resolve({ ids: input.uploads }, ctx);\n\treturn c.submit({ ...input, files }, author, ctx);\n},\n```\n\nA **root** is a short tree of calls, started at ingress:\n- by a link, for a request that arrives without one;\n- by `host.root(fn)`, for timers and scripts;\n- by each event delivered to a listener (`self.on`);\n- by each Chord call made from a `task` or a Durable phase.\n\nA root runs on the code that was current when it started (its **code version**), across hosts, until it ends. Keep roots short. Long-running work is a `task`, and long waits are state.\n\n```ts\n// wrong: holds a root (and its version of the code) open for minutes\napprove: async (ctx) => waitForHuman(ctx),\n\n// right: record the request as state; the answer is a separate call\nrequest: async (req, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\td.push(req);\n\t});\n},\nanswer: async ({ id, value }, ctx) => {\n\tpending.change(ctx, (d) => {\n\t\tsettle(d, id, value);\n\t});\n},\n```\n\n## Lifetime\n\nAn object lives while it's **referenced**, and every reference has an owner:\n\n| Owner | Acquired with | Released when |\n| --- | --- | --- |\n| another object | `self.get` | that object is disposed, or no longer declares it |\n| a call | `ctx.get` | the call settles |\n| a connection | a remote host's `get` | release, disconnect, revocation |\n| the host | `lifetime: \"host\"` | host shutdown (or a reload removing its facet) |\n| UI | `useObject` | unmount |\n\n- **Disposal** runs after the last reference is released, plus `linger`. Tasks are aborted, listeners removed, resources closed in reverse order (every close runs, and errors are collected), then the object's own references are released, which can dispose further objects.\n- **A resource's `close` may still use the objects its object references.** That holds for host shutdown and revocation too: `host.close()` and `invalidatePrincipal` dispose referrers before what they reference, and close links last. Bound remote calls in `close` with a signal: a call over a link that is already down waits for a reconnect.\n- **Forced disposal** (revocation, archive) makes references `gone` at once and aborts in-flight calls.\n- **References must not form cycles.** Inside one host this is checked at `get`.\n\n```ts\nhost.serve(Conversation, conversationFactory, { linger: 30_000 });  // switching back quickly doesn't reopen its watch\nhost.serve(Index, indexFactory, { lifetime: \"host\" });              // one object from load to host shutdown\n```\n\n**`lifetime: \"host\"`** is for local types with an empty input (or only defaults): the type has exactly one object,\ncreated for the host principal when its facet loads. Every `get` reaches that object, whoever asks; a `get` with\nanother input is invalid. References others hold do not change its lifetime, and it cannot `linger`. Only such\nobjects may register into registries. TypeScript refuses `lifetime: \"host\"` for a remote type, for an input with a\nrequired field and together with `linger`; `serve` checks the same at runtime, for callers without types.\n\n## Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\n\n// contribute (only from `lifetime: \"host\"` objects)\nhost.serve(Downloads, (_, _host, self) => {\n\tconst blobs = self.get(Blobs, {});\n\tself.register(Routes, {\n\t\t\"GET /download\": async (request, who, ctx) => {\n\t\t\tconst file = await blobs.find({ id: request.query.id }, ctx);\n\t\t\tif (!canView(who, file.session)) return forbidden();\n\t\t\treturn stream(file, ctx);\n\t\t},\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n\n// consume: a request sees the table of its own code version\nserver.on(\"request\", (req, res) =>\n\thost.root(async (ctx) => {\n\t\tconst route = host.registry(Routes).get(`${req.method} ${pathOf(req)}`, ctx);\n\t\treturn route ? route(req, principalOf(req), ctx) : notFound(res);\n\t}),\n);\n```\n\n- Each code version has one complete table. A reload never leaves a moment where an entry is missing.\n- A request that started before a reload keeps using the old table.\n- Order is first-registration order and survives replacement.\n- `conflict: \"error\"` makes a duplicate key fail the registration (or the reload).\n\n## Plugins\n\nA plugin is a feature with one **facet** per host kind:\n\n```text\nplugins/chat/\n  contract.ts    Chat, Conversation, Drafts\n  server.ts      facet for the server\n  worker.ts      facet for session workers\n  web.tsx        facet for browsers\n```\n\n```ts\n// plugins/chat/worker.ts\nimport { defineFacet } from \"@earendil-works/chord\";\nimport { Chat, Conversation } from \"./contract.ts\";\n\nexport default defineFacet((host) => {\n\thost.serve(Conversation, conversationFactory, { linger: 30_000 });\n\thost.serve(Chat, chatFactory);\n});\n```\n\n```ts\n// a host loads the plugins' facets for its kind\nawait host.load([\n\tnew URL(\"../plugins/chat/worker.ts\", import.meta.url),\n\tnew URL(\"../plugins/status/worker.ts\", import.meta.url),\n]);\n```\n\nBuilt-in plugins are trusted code from this repository and load directly from source.\n\nThe facet's `setup` argument is its host, scoped to what a plugin needs: `serve`, `name`, `root(fn)`, `registry(R)` and `signal`, which aborts once this run of the facet is no longer installed (a reload replaced or removed it, or the reload that loaded it aborted). Code that is not a Chord method, listener or task (Durable tools and hooks, callbacks of native libraries) keeps it to start roots and read registries, instead of reaching for a process-global host:\n\n```ts\nexport default defineFacet((host) => {\n\thost.serve(Sessions, sessionsFactory);\n\ttools.register(\"rename_session\", (args) =>\n\t\thost.root(async (ctx) => (await ctx.get(Sessions, {})).rename(args, ctx)),\n\t);\n});\n```\n\n## Hot reload\n\n```ts\nawait host.reload([new URL(\"../plugins/chat/worker.ts\", import.meta.url)]);  // or reload every changed plugin\n```\n\n### What happens\n\n1. **Load** the new modules.\n2. **Prepare:** run the new factories over the existing objects, without side effects. Cell writes throw, listeners and tasks don't start, registrations are collected, and changed resources are opened next to the old ones. Contracts are checked against every live consumer. Any failure leaves everything as it was.\n3. **Gate:** only if a cell's version was raised or an exclusive resource changes.\n4. **Commit:** in one synchronous step, install the new code, swap registry tables, restart tasks, rebind listeners, and send `reset` and shape updates to subscribers.\n5. **Retire:** old code keeps running the roots it started. Once they end, it's dropped, along with whatever it alone declared. If they are still running when the next reload starts, that reload waits up to `retireDeadline`, then aborts them (their `ctx.signal`) before it closes what only the old code used.\n\n### Compatible changes: no waiting\n\nBug fixes, new methods, new cells, new resources and registration changes all switch immediately. A root that started on the old code finishes on it, and new roots get the new code.\n\n### Changing what state looks like or means: bump the cell's version\n\n```ts\n// v1: { running, tasks } where tasks counted phases\n// v2: tasks counts tasks, and usage is new\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusV2, { running: false, tasks: 0, usage: emptyUsage() }, {\n\t\tversion: 2,\n\t\tmigrate: (old, from) => (from < 2 ? { running: old.running, tasks: 0, usage: emptyUsage() } : old),\n\t});\n\treturn {\n\t\tstatus,\n\t\treport: async (s, ctx) => {\n\t\t\tstatus.replace(ctx, s);\n\t\t},\n\t};\n});\n```\n\nAt the gate, for each live `StatusStore`:\n1. **Roots already inside finish.** Their re-entrant calls are let in.\n2. **Old roots that haven't touched it yet get `upgrade_conflict`** instead of old code over migrated state. They retry as a new root.\n3. **`migrate` runs** on a private copy, and the result is validated against the new schema.\n4. **Commit.** New roots use the new code and the migrated state.\n\nIf the roots inside don't finish within the deadline (5 s), the reload is **aborted** and nothing changes. The error names the blocking roots.\n\nThe version belongs to the cell (default 1); other cells of the object are not migrated. The runtime compares each\ncell's schema and version between old and new code:\n- unchanged: compatible;\n- version raised: migrate (without `migrate`, the value is kept and must fit the new schema);\n- schema changed **without** a raised version, or a lowered version: the reload is refused.\n\nA change of meaning with an identical schema needs a bump too. Nothing can detect that one for you.\n\n### Resources\n\n```ts\nconst db = self.resource(\"db\", { version: 2, open: () => openSqlite(path, { wal: true }), close: (db) => db.close() });  // 1 → 2: reopen\nconst port = self.resource(\"port\", { version: 1, exclusive: true, open: () => listen(8787), close: (s) => s.close() });\n```\n\n- A reload keeps a resource's open value unless its `version`, `deps` or `exclusive` change: `open` doesn't run\n  again. The version is required: a native value has no schema to detect a change. The kept resource still takes the\n  new code's declaration at the commit, so a later `close` is the new one.\n- A changed resource opens next to the old one during prepare. The old one is closed with its own `close` once the\n  old code retires.\n- An `exclusive` resource can't exist twice, so it's swapped at the gate: close the old one, open the new one, and reopen the old one if that fails.\n\n### Contract changes\n\n| Change | What happens |\n| --- | --- |\n| Additive, same version | reloads; consumers keep working |\n| Breaking, version bumped, with `accepts[old]` | reloads; old consumers go through the adapter |\n| Breaking, without an adapter | refused, unless the consumers reload in the same batch |\n| Breaking, version not bumped | refused: \"consumer needs member …\" |\n\nThe host's own **reference to a `lifetime: \"host\"` object** is a lifecycle pin, not an old-version consumer. When a reload replaces\nits provider, that reference adopts the new contract at commit; it does not require an `accepts` adapter for itself.\nThe object, cells and unchanged resources stay alive without reactivation. A refused reload keeps the original\nreference and contract. Other references (`self.get`, `ctx.get`, `host.get`, peers) still obey the compatibility rules above;\nstate-version bumps and migrations are still required when stored state changes.\n\n### Several plugins at once\n\nReload them together: `host.reload([a, b])`. Prepare sees all new code at once, so a changed contract and its updated consumer check against each other.\n\nOn the server, `host.reload(serverFacets, { remote: { \"session:\": workerFacets } })` reloads every connected worker too. Its report covers the server; `report.children[\"session:S1\"]` says what each worker changed, prepared and gated.\n\n## Hosts and links\n\n```ts\n// server\nconst server = createHost<Principal>({ name: \"server\", self: { kind: \"host\" }, principalKey });\nserver.accept(webSocketLink(wss, { principal: (req) => authenticate(req) }));  // stamps owner or guest from cookies\nserver.route(\"session:\", {\n\tauthorize: (principal, host) => canReach(principal, host),  // before resolving or spawning\n\tresolve: (host) => workers.ensure(host),  // a link to the worker, spawned if needed\n\tprincipal: (host) => workers.principal(host),  // what the worker acts as when it calls the server\n});\n\n// worker\nconst worker = createHost<Principal>({\n\tname: `session:${id}`,\n\tself: { kind: \"host\" },\n\tprincipalKey,\n\tupstream: { link: ipcLink(process), principal: { kind: \"server\" } },  // the server's own requests act as this\n});\n\n// anywhere\nconst chat = await ctx.get(Chat, { session: \"S1\", conversation: 1 });  // browser → server → worker session:S1\nconst sessions = self.get(Sessions, {});  // `hostNamed(\"server\")`: worker → server, acting as worker{S1}\n```\n\n- **The server relays** between its children: worker to worker, and browser to worker.\n- **Disconnects:** when a browser disconnects, exactly the references it had in workers are released.\n- **Principals across links:**\n  - a forwarded principal is accepted only from the parent link;\n  - calls an object makes itself act as its host;\n  - links ping, and a dead link releases everything held through it.\n- **Client origins:** a hold that comes from a browser carries an opaque id of its connection (`origin`), forwarded by the server when it relays the hold, so a worker can tell its clients apart. A client cannot set it. A disconnect releases that client's holds at once.\n- **Slow peers:** a peer acknowledges what it processed. While a window (`window`, default 256 KiB) of frames is unacknowledged, state waits and is coalesced per member's stream; calls, results and the first snapshots of a new reference are never held back, so a `get` does not wait behind other references' state. Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams are replaced by their current value. So under load a result can arrive before the state change it caused: wait for the state you need instead of assuming it is there when the call returns.\n- **Reloads across hosts:** the server coordinates prepare and commit for every host.\n- **Workers that start behind** (spawned after a reload) are caught up to the server's code version before anything is routed to them. Calls their roots make to the server meanwhile run in the server's current code version instead of failing `superseded`.\n- **Protocol version:** both ends announce the link protocol version in their hello. A peer of another version is not\n  talked to: the link closes, and `onProtocolMismatch` (a `LinkOptions` field of `connect` and `accept`) tells the\n  side that has to load new code. A browser reloads its page instead of reconnecting:\n\n  ```ts\n  host.connect(webSocketLink(socket), { onProtocolMismatch: () => location.reload() });\n  ```\n- **Browsers** don't take part in commits. They learn about new code through shape and `reset` messages, and an incompatible contract makes their calls fail with `upgrade_required`.\n\n## Durable\n\nA worker's host provides the Durable Harness as a local object of lifetime `\"host\"`. A Durable phase isn't a root: it's pinned to its registry snapshot by Durable itself. Each Chord call a hook or tool makes starts a fresh short root.\n\n```ts\nhost.serve(Harness, (_, _host, self) => {\n\tconst durable = self.resource(\"durable\", {\n\t\tversion: 1,\n\t\topen: () => DurableHarness.open(storage, { models, registry: durableRegistry(host, Extensions) }),  // a reader over the code version tables\n\t\tclose: (h) => h.close(),\n\t});\n\treturn { conversation: (id: number) => durable.value.conversation(id) };\n}, { lifetime: \"host\" });\n\n// a plugin contributes a Durable extension like any registration\nhost.serve(Guard, (_, _host, self) => {\n\tself.register(Extensions, {\n\t\tguard: defineExtension({\n\t\t\tname: \"guard\",\n\t\t\thooks: [hook(ToolTask, { beforeTool: (call) => (isDangerous(call) ? { block: \"Needs approval\" } : undefined) })],\n\t\t}),\n\t});\n\treturn {};\n}, { lifetime: \"host\" });\n```\n\nA reload changing the guard: Durable phases already running keep the old snapshot, the next phase uses the new one, and there's never a phase without a guard.\n\n## Errors\n\n| Code | Meaning |\n| --- | --- |\n| `forbidden` | The factory or a method refused this principal |\n| `invalid` | Input, arguments or a result failed validation |\n| `not_ready` | A declared resource or reference was used inside a factory, or state read before its reference's first hydration |\n| `not_in_variant` | The member doesn't exist in the variant you have |\n| `released` | The reference was released |\n| `gone` | The object was forced away while referenced. Call `get` again. |\n| `contract_mismatch` | The provider's contract doesn't cover the consumer's, and no adapter exists |\n| `upgrade_conflict` | An old root reached an object migrated meanwhile. Retry as a new root. |\n| `superseded` | The root's code version is no longer available on this host. Retry. |\n| `upgrade_required` | A browser's contract is outdated. Reload its web facets. |\n| `unavailable` | The target host can't be reached or started; reason `ready_timeout`: a reference was not ready within `readyTimeout` |\n| `aborted` | The call's `ctx` was cancelled |\n\n## Rules of thumb\n\n| Do | Not |\n| --- | --- |\n| Keep reloadable state in cells | Closure variables (lost on reload) |\n| Bump the cell's version, with `migrate`, when a cell's shape or meaning changes | Changing a schema without a bump (refused) or a meaning without a bump (silent) |\n| Bump the contract version on breaking changes; add `accepts` for old consumers | Renaming or removing members at the same version (refused) |\n| Do slow setup in a resource's `open` | Awaiting inside the factory (it's synchronous) |\n| Do side effects in resources, listeners, tasks and registrations | Effects in the factory body (they run during prepare) |\n| Use `ctx.get` inside methods | `self.get` inside methods (a new reference per call) |\n| Keep roots short; long waits are state | Awaiting a human inside a method |\n| Check arguments in methods | Treating the variant as all of authorization |\n| Pass `ctx` everywhere | Starting background work with the caller's `ctx` (use a task) |\n| Treat state values as immutable | Mutating what you read |\n\n<!-- File: packages/chord/docs/spec.md (Tokens: ~9580 | Category: anthropic-skill) -->\n# Chord specification\n\nThis document specifies the runtime in `src`. Where it and the code disagree, the disagreement is a bug. [Known limitations and decisions](#12-known-limitations-and-decisions) lists what the runtime does not do yet and the choices the sections below leave open.\n\nHard constraints:\n- Chord's runtime depends only on its own `delta` module (tracker, applyImmutable, encoder/decoder, diff) and Node/browser built-ins plus `typebox` for schemas. No other Pi packages.\n- Built-in plugins are trusted, loaded from source in-process. The plugin loader uses dynamic `import()` with a cache-busting query (owner-approved exception, `TODO(loader)`: replace with an isolating, evicting loader; old module copies leak; TUI plugins need isolation).\n- Hot reload is required. Host restart only for Chord/runtime code changes or host-level crash recovery.\n\n## 1. Concepts\n\n| Term | Meaning |\n| --- | --- |\n| facet | A plugin's code for one host (surface): it serves object types (`defineFacet`). |\n| object type | Name + contract. `remote` (addressable from other hosts; schemas for input, variant state members, method args/results) or `local` (host-only; members may be anything, including functions and native objects, but must be listed in the contract: `method`, `opaque` or a state schema). |\n| factory | `(input, principal, self) => implementation`. Synchronous. Registered with `host.serve(type, factory, options)` from a plugin facet. |\n| object | One instance of an object type. Stable identity per `(type, canonicalInput, principalKey)`. Owns cells, resources, references, listeners, tasks, registrations. Survives reloads. |\n| implementation | The object's code: what a factory run returns (methods + state members; `variant` for remote types). Immutable, per code version. |\n| reference | What `get` returns: a late-bound claim on an object, never the object or its implementation. An object lives while referenced. |\n| principal | App-defined. Stamped by links. In-host code acts as the `host` principal. |\n| root | One short Chord call tree, admitted at trusted ingress (link request without root, UI event, timer, `host.root`). Carries `{ rootId, codeVersion }`. |\n| code version | One committed set of implementations/registrations per host (server-issued id). |\n| code holder | Long-lived execution that is NOT a root: Durable phases/tool runs, browsers, `self.task` loops. Every Chord call they make starts a fresh short root. |\n\n## 2. Contracts\n\nContracts are runtime values for every type (shipped bundles have no TypeScript; third-party plugins are built separately). TS types are inferred from them.\n\n```ts\nexport const Chat = remote(\"pi.chat\", {\n\tversion: 1,                                   // contract version: a plain number, bumped only on breaking changes\n\tinput: Type.Object({ session: Type.String(), conversation: Type.Number() }),\n\thost: hostFamily(\"session:\", \"session\"),       // where its objects live: `session:` + input.session\n\tvariants: {\n\t\tviewer: { state: { view: ViewSchema } },\n\t\tparticipant: {\n\t\t\tstate: { view: ViewSchema },\n\t\t\tmethods: { send: method(SendSchema, Type.Object({ submissionId: Type.Number() })) },\n\t\t},\n\t},\n});\n\nexport const Index = local(\"pi.index\", {\n\tversion: 1,\n\tinput: Type.Object({ session: Type.String() }),\n\tmembers: {\n\t\tsearch: method(Type.Object({ query: Type.String() }), Type.Array(Hit)),\n\t\tonChange: method(Type.Function([Change], Type.Void()), Unsubscribe),   // non-JSON allowed for locals\n\t\tharness: opaque(\"pi.durable-harness@1\"),                               // native object, by name+version\n\t},\n});\n```\n\n- Input identity: normalize (apply defaults, strip unknown), validate, then canonical JSON with fields equal to their schema default omitted. Changing defaults or normalization requires a new type name.\n- Remote calls validate args/results and state values at the provider boundary. Local calls are not validated per call.\n- Local contracts list every member (`members: { search: method(...), harness: opaque(...), status: Schema }`). A reference that is still `loading` must know whether `h.x` is a method (a caller that waits until live), a state member (a readable) or an opaque value, before any implementation exists. Members missing from the contract are found only once the reference is live.\n- Every bundle embeds the contract tokens it was built against (its own copy). Fingerprint = hash of the canonical description.\n- Placement (remote types, required): `host: hostNamed(name)` (one host) or `hostFamily(prefix, key)` (the host `prefix` + the input's required string field `key`). Data only, in the description (and so the fingerprint and the `hold` token); there is no `at` option. A `get` normalizes the input with the caller's contract, resolves the host and acquires locally if that is this host, else remotely. `serve` (and a reload that loads a facet) refuses a type whose placement excludes the host's name. A host refuses a hold whose input its own contract places elsewhere (`invalid/placement`), and a relay refuses a hold whose target disagrees with its token's placement and input. Coverage requires equal placements, and an `accepts` adapter never bridges two placements: a moved type is a breaking change. A remote reference's address is the type, the normalized input and the principal: no host name.\n\n### Compatibility rule\n\nA consumer built against version M binds to a provider at version N iff N = M and the provider's description covers the consumer's; otherwise only through `accepts[M]` adapters declared by the provider (`serve(type, factory, { accepts: { 1: (current) => v1View } })`); else `contract_mismatch`.\n\nCovers: every consumer member exists with the same kind; methods: both take arguments or neither does, the provider accepts every argument the consumer can send (new fields optional, nothing newly required or retyped), and the provider's result fits the consumer's expected result (extra fields fine; a void consumer accepts any); state: provider value fits consumer schema (extra fields fine); opaque: same name@version.\n\nAdditive changes need no bump. Breaking changes (remove/rename/retype, newly required, or a change of meaning with the same shape) bump the number. Same version + non-covering description = forgotten bump, refused. Meaning changes without a bump are undetectable (author's responsibility, as with Durable document versions).\n\nChecked at: `get` binding, reload prepare (new provider token vs every surviving consumer holding the type and every loaded plugin importing it), remote references (consumer token in the `hold` frame).\n\nThe exact host-owned reference to a `lifetime: \"host\"` object is a lifecycle pin, not a surviving old-contract consumer. When its\nprovider is replaced, commit replaces that reference with one bound to the new provider contract, acquiring it before\nreleasing the old one so the same object, state and unchanged resources survive. It needs no `accepts` adapter\nfor its own previous contract. Prepare/abort do not change that reference. All other consumers retain their own tokens\nand the normal coverage/adapter checks; state-version and migration checks are unchanged.\n\n## 3. Factory and declarations\n\nFactories are synchronous; returning a promise throws. A declaration has a name only if something of it survives a reload and must be found again: state (its value) and resources (the open value), plus listeners (a kept subscription). Names are strings, unique per object per declaration kind (a repeated name is `invalid/duplicate`); kinds have separate names. Derivations and tasks have no names: a reload makes new ones. References are identified by their address.\n\n```ts\nhost.serve(StatusStore, ({ session }, _host, self) => {\n\tconst status = self.state(\"status\", StatusSchema, { running: false, tasks: 0 },  // schema required, initial value\n\t\t{ version: 2, migrate: (old, from) => upgrade(old, from) });                     // per cell; default version 1\n\tconst errors = self.state(\"errors\", Type.Array(Type.String()), []);\n\tconst catalog = self.get(Catalog, {});                         // reference; identity = canonical address (+ at)\n\tconst watch = self.resource(\"watch\", {\n\t\tversion: 1, deps: [catalog], exclusive: false,\n\t\topen: async ({ signal }) => openWatch(session, signal),\n\t\tclose: async (w) => w.close(),\n\t});\n\tself.on(\"changes\", watch, (w) => w.events, (event, ctx) => status.change(ctx, (d) => { d.tasks = event.tasks; }));\n\tself.task(async (ctx) => { /* loop until ctx.signal aborts */ });\n\tconst visible = self.derive([status], (s) => project(s));      // synchronous\n\tself.register(Routes, { \"GET /status\": handler });              // registry keys are the registry's own keys; `lifetime: \"host\"` objects only\n\treturn { status: visible, errors, report: (args, ctx) => { status.replace(ctx, args); } };  // exposed state = what is returned\n}, {\n\tlinger: 30_000,                                                  // with lifetime \"referenced\" (the default) only\n});\n```\n\n| Declaration | Survives reload | Notes |\n| --- | --- | --- |\n| `state(name, schema, initial \\| { init }, { version?, migrate? })` | same cell and value (`initial` ignored, `init` not run); migrated when its version rises; a cell the reload adds runs `init` at prepare (gated objects: at the gate) | Schema required (no TypeScript at runtime). `change(ctx, draft => void)`, `replace(ctx, value)`, `value`, `subscribe`. Writes through `change` touch only mutated paths. Private state is a cell the implementation does not return. `init(ctx)` (may be async) gives the first value, see activation |\n| `get(type, input, { at? })` | kept if declared again (same address) | returns a reference |\n| `resource(name, { version, deps?, exclusive?, open, close? })` | kept unless `version`, `deps` or `exclusive` change; a kept resource adopts the new declaration (its `close`) at commit | `open` runs at prepare (non-exclusive) or at the gate (exclusive); returns a resource whose `.value` is the open value; a replaced resource closes with its own declaration's `close` |\n| `on(name, source, [select,] handler)` | same subscription while source and `select` (identity or source text) are unchanged; `handler` swapped | each event starts a fresh root in the current code version; `select` is not re-run for a kept subscription. State sources deliver their current value at start and after every reload that replaced them |\n| `task(fn)` | restarted with new code at commit: the old run is aborted via ctx.signal, and the new one starts once it finished (at most `taskStopDeadline`, default 5 s, then anyway, reported) | a code holder; calls inside start fresh roots |\n| `derive(sources, fn)` | a new derivation, computed at commit | synchronous fn |\n| `register(registry, { key: value })` | value swapped at commit | `lifetime: \"host\"` objects only |\n| `expose(resource)` | follows the resource | a local member (usually `opaque`) whose reference value is the resource's value once live |\n\nState members of an implementation may be cells, derivations, or state members of references the object holds (they are defined once the object is live). `cell.replace(ctx, value, { diff? })` publishes a structural diff when the new value shares containers with the old one (or `diff: true`), else `r`. Facet `setup` receives `{ serve, name, root, registry, signal }` of its host; `signal` aborts once this run of the facet is no longer installed (a committed reload replaced or removed it, the reload that loaded it aborted, or the host closed).\n\n`migrate` per cell is `(old, fromVersion) => new`. A cell may be several versions behind (author bumped more than once between reloads). Without `migrate`, a raised version keeps the value, which must fit the new schema.\n\n**Listeners on state reconcile.** A listener whose source is state (a cell, a derivation, a reference's state member) is subscribed when it starts and gets the current value, then every new value. A reload that replaces its source (every derivation is new; a reference to another address is another source) subscribes it again, so it gets the current value again: its handler must reconcile (be idempotent), not count events. An event source (a resource's events, any other `subscribe`) delivers no current value; a kept subscription to it is not touched by a reload.\n\n**Methods.** `method(args?, result?)`: without `args` the method takes none (callers `ref.name(ctx?)`, implementation `(ctx) => …`; `method(undefined, result)` for one that returns something); without `result` it is void (callers get `undefined`, whatever the implementation returned). The description records both (absent fields), and the `call` frame carries no `args` and the `result` frame no `value` for them. A consumer and provider must agree on whether a method takes arguments; a consumer without a result accepts a provider's.\n\nRemoved vs guide: `self.own`, `holdWhile`, async factories, inferred dependencies.\n\n### Synchronous factory, asynchronous activation\n\n`self.get` and `self.resource` only declare and return placeholders. Activation of an object:\n\n1. run the factory synchronously (declarations);\n2. await declared resources' `open()` (e.g. connect a DB);\n3. await declared references becoming live (dependencies activate first, coalesced);\n4. run the `init` of every cell declared with one, in parallel, each in a root of its own acting as the host principal; validate and install the values;\n5. attach derivations, start listeners and tasks;\n6. object is live: references `live`, `ready` resolves.\n\nInside the factory, placeholders must not be used (`resource.value` and an `init` cell's `value` throw `not_ready`; reference calls throw). An `init` that throws, or returns a value its schema refuses, fails the activation like a failed `open`. An `init` must not wait for its own object or one that holds it (it is not live yet), and must not read another `init` cell of its object (they run in parallel). `init` gives a first value from a source the object reads anyway; it does not persist anything, and a cell is not saved anywhere by having one. Methods/listeners/tasks run only on live objects, so their dependencies are live. An object goes live only once its references are ready (live and, for remote ones, hydrated) and the state sources it exposes or derives from have their values, also where they forward a remote reference held some other way; waiting for references is bounded by the host's `readyTimeout` (default 60 s, `unavailable/ready_timeout`, also for a reference cycle across hosts). External consumers holding a `loading` reference (`host.hold`): calls wait until live (ctx cancellation, deadline); state members throw `not_ready` until the first hydration. Activation failures propagate to dependents and all waiting gets; the next get retries.\n\n### Lifetime\n\n`serve(type, factory, { lifetime })`:\n\n| `lifetime` | Created | Ends | `linger` |\n| --- | --- | --- | --- |\n| `\"referenced\"` (default) | at the first `get` of its address | after the last reference is released, plus `linger` | allowed (default 0) |\n| `\"host\"` | when its facet loads (or at `serve`); loading waits until it is live | when the host closes, or a reload removes its facet | refused |\n\n- `\"host\"` is refused at `serve` for remote types, for an input that does not accept `{}`, and together with `linger`;\n  the types (`ServeOptionsFor`) refuse the same at compile time, where such an input is one with a required field.\n  The type has exactly one object: the empty input (defaults applied), created for the host principal. Every `get`\n  reaches it, whatever its principal (the factory sees the host principal); a `get` with another input is\n  `invalid/input`. Other references do not change its lifetime: the host keeps its own until it closes.\n- Only `\"host\"` objects may register (`invalid/not_host_lifetime` otherwise).\n- A `\"host\"` object whose activation fails is started again by the next reload that changes its type. A reload that\n  makes a type `\"host\"` creates its object; one that makes it `\"referenced\"` releases the host's reference, so the\n  object lives on only while others reference it.\n\n### Host objects readiness\n\n- `host.install(facet, { awaitHostObjects? })` installs declarations synchronously and returns a promise for this\n  facet's host object activations. `host.load(facets, { awaitHostObjects? })` installs the entire batch before awaiting\n  its host object activations. `awaitHostObjects` defaults to true. Unreferenced objects are not activated.\n- Factory, resource-open, dependency and activation-finalization failures reject the returned promise with the\n  activation error (ChordError codes preserved; ordinary errors wrapped as `unavailable`). Host object failures also\n  reach `onError`. Installation is not a rollback transaction; setup/registration errors from `install` throw\n  synchronously. Abandoned startup should close the host.\n- `{ awaitHostObjects: false }` starts the same host objects but resolves after installation, without waiting for\n  their activation. `host.ready()` awaits a snapshot of the host's references to its host objects at call time\n  (including direct `serve` and deferred loads), rejects their activation failures, and does not acquire additional\n  references. It is not an ongoing health check and does not await task completion or remote state hydration.\n- Local bootstrap readiness and upstream admission are separate. A worker connects its upstream link, loads with\n  `{ awaitHostObjects: false }`, awaits its local bootstrap reference (e.g. Harness, with no upstream dependency), then\n  announces control readiness so its parent can accept the link. Only after admission does it await `host.ready()`.\n  Otherwise a host object reporting to the server's SessionStatus deadlocks with the parent waiting for control\n  readiness. Deferred loads skip the whole barrier, not selected dependencies; callers explicitly choose local\n  bootstrap references, keep bootstrap busy until full readiness, and propagate later startup errors.\n\n## 4. Principals, variants, references\n\n- Principals are the app's type `P` (`createHost<P>`, `Host<P>`, `defineFacet<P>`, `Factory<C, P>`, `Ctx<P>`; default `unknown`). Chord invents none: `createHost({ self })` is the principal of the host's own code (roots, code inside objects, `lifetime: \"host\"` objects), `connect(link, { principal })` what the parent's own requests act as, `accept(link, { principal })` what a link acts as, `Route.principal(hostName)` what a routed host acts as when it calls back. All are required.\n- `createHost({ principalKey })` maps principal to a key that includes connection provenance and grant revision. Objects are per principal key. The host's own principal is recognized by key (`principalKey(p) === principalKey(self)`), not by object identity, so an equal principal never creates a second object.\n- Remote factories return `{ variant, ...members }`; `variant` must be a declared variant; members must match it. Local factories return any object. `variant` is reserved as a member name.\n- Reference: `status` (`loading | live | released | gone`), `ready` promise, `variant`, `shape` (`{ variant, members, contract }`), `onStatus(listener)` (every status change, with the error for `gone`), late-bound members, `release()`. `self.get` is synchronous (the object goes live after its references are ready); `ctx.get` and `host.get` resolve once the reference is ready: live, and every state member of its variant hydrated (`state: false`: live only). They reject with the activation error, `aborted` (ctx aborted or settled, `signal`), or `unavailable/ready_timeout`, releasing only that reference. `host.hold` is the synchronous form for UI runtimes. A held remote reference stays subscribed to its variant's state until released; after a link loss its last values stay readable while it re-holds. State members are typed `T`; reading before the first hydration throws `not_ready`, outside the variant `not_in_variant`, after the end the end's error. A remote reference type is a union discriminated by `variant`. Shape changes (reload, variant) are pushed; members missing from the current variant reject `not_in_variant`; released references reject `released`; forced disposal makes references `gone`.\n- Shared bindings: remote references on a host with the same key (type, normalized input identity, principal key, consumer contract fingerprint) are views onto one binding: one hold, one stream per state member, calls over that hold. Views have their own owner (object, call, external), status, observers and `release`; the binding ends (gone) or re-holds for all of them. Without views a binding closes, or lingers for `linger.types[typeName]` ms (`createHost({ linger: { types, cap } })`, at most `cap`, default 4, least recently released first) while staying held and hydrated.\n- Methods are `(args, ctx) => Promise<result>`. Factories still check arguments; variants are not complete authorization.\n- Revocation: `host.invalidatePrincipal(match)` force-disposes matching objects on this host and propagates over links; links re-stamp or close.\n\n## 5. Lifetime\n\n- Reference owners: object (declared `get`), call (`ctx.get`, released at settle), connection (remote references; relay maps per origin connection), host (`lifetime: \"host\"`), UI.\n- Activation coalesced per address; if all requesters leave before it completes, the result is disposed. Factory throw: unwind declarations, reject gets, next get retries.\n- Disposal after the last reference is released + `linger`: tasks aborted, listeners removed, resources closed (reverse order; all run; errors aggregated), the object's references released (cascade).\n- Bulk forced disposal (`host.close`, `invalidatePrincipal`) goes in waves, referrers before what they reference, so a resource's `close` can still use its object's references; `host.close` closes links after the objects.\n- Forced disposal: references `gone` immediately, new calls rejected, in-flight call contexts aborted, then cooperative cleanup.\n- Reference graph acyclic in-host (checked at get); across hosts by convention.\n- Links have ping/timeout; a dead link releases everything held through it.\n\n## 6. Roots and code versions\n\n- `Ctx = { signal, rootId, codeVersion, get(...): Promise<Ref>, principal? }`. Explicit; no AsyncLocalStorage dependency (may be used in dev to warn).\n- A link creates a root for a request that carries none; nested remote calls carry `{rootId, codeVersion}`.\n- Dispatch: a call with code version E runs the object's implementation for E. A host retains at most `current` and `previous` code versions (two-version rule). A call for a code version the host doesn't have (older than previous, or host started later) is rejected `superseded` (retryable by starting a new root). Never substitute another code version.\n- Browsers never carry code versions into the server: browser calls start roots at the server. Browsers learn of changes via shape/reset pushes and a server `Manifest` object; incompatible contract fingerprint -> `upgrade_required`.\n\n## 7. Reload\n\nExplicit trigger (`reload_plugins` tool, button, API). Serialized; no nesting.\n\n1. Build/type-check gate (application tooling, outside Chord): dependency-closed set; refuse breaking remote contract changes without a contract version bump.\n2. Load: on every affected host, import new facet modules (cache-busted).\n3. Prepare: for each live object of each affected type, run the new factory in recording mode: existing cells readable, writes throw; listeners/tasks/registrations recorded, not started; references recorded; non-exclusive resources whose name is new or whose `version`/`deps` changed are opened as candidate-owned (closed on abort). Validate keys, variants, registry collisions. New objects activated during prepare use the current code version (old code). Any failure: abort, discard candidates. Vote.\n4. Gate (only objects with a cell whose version was raised or which replace an exclusive resource, plus objects depending on those resources):\n   - Admission cut: from now, an old-code-version root that has not touched the object gets `upgrade_conflict` (retryable), not queued. Roots that already hold an activation lease on it are grandfathered; their re-entry is admitted.\n   - Wait for grandfathered activations to finish, deadline (default 5 s). Missed deadline: abort the reload, reopen, nothing changed (report blocking roots).\n   - Exclusive resources: close old, open new; on failure reopen old and abort.\n   - Run `migrate` into private copies; validate.\n5. Commit (server commits itself, then sends commit to children; the decision is not persisted, see section 12; FIFO links guarantee no host receives a new-code-version root before committing): each host in one synchronous step installs migrated cell values and cell versions, installs implementations as code version E', swaps registry tables, restarts tasks with new code, rebinds listeners, pushes shape/reset to subscribers, opens gates. E' current, E previous.\n   The coordinator's result carries `children`: each participating child's prepared report (its vote).\n6. Retire: when no roots of `previous` remain (and, on a child, the parent retired it), drop its implementations and release declarations only it declared. The next reload first waits for that, at most `retireDeadline`; then it aborts the roots still running on `previous` (their ctx signals), gives them up to 1 s to stop, and retires anyway. A coordinator retires before it sends `prepare`, so its `retire` frames reach the children first.\n\nCompatible changes (no state bump, no exclusive resource change) skip the gate: no waiting.\n\nState compatibility rule: reload prepare compares each cell's old and new schema and version. Unchanged: compatible (no gate). Version raised: gate + `migrate` (or the kept value), validated against the new schema. Schema changed without a raised version: reload refused. A lowered version: refused. A change of meaning with an identical schema needs a bump too (undetectable; author's responsibility). Initial values and migrate outputs are validated; per-write validation in dev mode.\n\n## 8. Registries\n\n```ts\nexport const Routes = defineRegistry<RouteHandler>(\"http.routes\", { conflict: \"error\" });\nself.register(Routes, { \"GET /download\": handler });       // in a `lifetime: \"host\"` object; keys are the registry's keys\nhost.registry(Routes).get(\"GET /download\", ctx);            // table of ctx.codeVersion (or current without ctx)\nhost.registry(Routes).subscribe((table, codeVersion) => ...);     // bridges for imperative consumers (Durable)\n```\n\nPer-code-version immutable tables; order = first registration order, kept on replace. An application can implement Durable's `RegistryReader` from the table (snapshot per code version) so phases pin a complete snapshot; Chord does not ship that bridge.\n\n## 9. Hosts and links\n\n- `createHost({ name, self, principalKey, upstream?: { link, principal } })`; `host.accept(link, { principal })` for children; `host.connect(link, { principal })` for the parent; `host.route(prefix, { authorize(principal, hostName), resolve(hostName) => Promise<Link>, principal(hostName) })`.\n- Links: `memoryLinkPair()` (tests), Node IPC (child_process), WebSocket (server `ws`, browser `WebSocket`).\n- Principal stamping: the accepting side stamps the principal of what connects (from its authenticator); forwarded principals accepted only from the parent link; the parent's own requests (no forwarded principal) act as the principal given to `connect`; payload principal fields ignored.\n- A child that connects behind its parent's code version is fenced (nothing routed to it) and reconciled by a reload of its modules to the parent's code version. Calls its roots of the old code version make upstream are admitted in the parent's current code version (the parent never ran that code version), not rejected `superseded`.\n- Relay: server forwards a hold to the child link of its target host (`at`, resolved by the consumer from the contract's placement and the normalized input, and checked against both), mapping references per origin connection; authorizes before resolving/spawning and re-checks after.\n- Wire frames (JSON): `hello`/`bye`, `hold`/`release` (a reference over a link: `hold` returns `shape` or `gone`; there is no separate `get`), `shape`, `call`/`result`/`cancel`, `sub`/`unsub`/`resync` and `state`, `gone`, `root_end`, `retire`, `invalidate`, `ping`/`pong`, `ack`, reload control (`prepare`/`vote`/`commit`/`abort`).\n- State: a `hold` implies the state of its variant (`state: false`: none until `sub`; `unsub` stops it). The provider answers with units: a `shape` whose `state` lists the members the consumer lacks, followed by one `state` frame `as: \"snap\"` per listed member; the consumer applies the shape once those snapshots are here. Later `state` frames of a member (addressed by hold id and member `m`) are `ops` (per-stream delta encoder), `reset`, or `error`; a `snap`/`reset` without `value` carries `undefined`. A failed snapshot before the first hydration ends the reference with its error. A reload or variant change sends a unit with the members that are new; members that left stop. `again` marks a re-hold after a link loss (the consumer has values). `origin` names the client connection a hold comes from (stamped by the host that accepted the client, forwarded by relays, honored only from the parent link).\n- Protocol version: `hello` carries `protocol` (`PROTOCOL_VERSION`, now 4; absent means 1). A side that sees another version processes nothing more, calls `LinkOptions.onProtocolMismatch({ local, peer })` and closes the link with reason `protocol-version <its version>`; the other side's connection reads that reason and reports the mismatch too. Peers do not negotiate: the outdated side must load the current code (a browser reloads its page). Frame fields follow the version: the `shape` frame names the variant `variant`, code versions travel as `codeVersion` (2); remote contract tokens carry their placement (`host`), and a `hold` carries the input normalized by the consumer (3); a hold implies its state, units, `origin`, and `shape` without a code version (4).\n- Flow control: both ends announce `acks` in their hello and acknowledge what they processed (`ack`, a cumulative count of every frame but `ack`). While the frames not yet acknowledged exceed the window (`window`, default 256 KiB of JSON text), state waits at the sender in slots flushed in creation order: one per member stream (ops are appended, a snapshot or reset replaces it, a release or `unsub` drops it) and one per hold for a unit (lazy: built from the current values when sent; a newer unit merges into it). Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams become a reset of their current value built at flush (a relay sends `resync` downstream and drops that member's ops until the reset arrives). The first unit of a hold that is not `again` and every frame but state are sent at once. Links that report no frame sizes (IPC) are never windowed; a peer without `acks` is neither windowed nor acknowledged. An acceptor can limit the calls a peer has in progress (`maxCalls`); calls beyond it are answered at once with `unavailable/too_many_calls`. Nothing outside the connection may add frames to a link: acknowledgements count them.\n\n## 10. Host manager (library)\n\n`createHostManager({ driver, keepAlive })`: fork driver (entry, init, lock, env), serialized start/stop/replace, exit-before-replace, process-tree kill after grace, control channel (ready/fatal/shutdown), liveness callback (booting, activeCalls, viewers with heartbeats, durable tasks, idleFor).\n\n## 11. Test coverage\n\nThe tests cover at least: lifetime/references/linger/forced disposal/gone; activation coalescing and abandonment; principals and variants; remote calls over memory links incl. relay and principal forgery attempts; state snap/ops/reset over a link with reconnect; registries per code version; reload: compatible (no wait, pinned roots), prepare isolation (writes throw, failing factory leaves everything unchanged), state bump with migrate and gate, `upgrade_conflict` for late old roots, deadline abort, exclusive resource replacement, cross-host commit ordering, two-version retirement and `superseded`.\n\n## 12. Known limitations and decisions\n\n### Still open\n\n- **Retirement across hosts.** A child retires a code version only after its parent retired it (`retire` frame) and its own roots of that code version ended; the server decides from its own roots. A child that comes in step (hello or reconciliation) is told which code versions the parent retired before. A root started on a worker that calls up to a server which has already retired the code version both had still gets `superseded` (retryable).\n- **The commit decision is not persisted** (section 7, step 5). A server crash between its own commit and a child's commit leaves that child on the old code version until it reconnects and is reconciled.\n- **One level of reload participants.** Only the coordinator's direct child hosts take part; a child does not forward `prepare` to its own children.\n- **Reconciliation needs the child's module list.** It is remembered from cluster reloads (`host.remoteModules`), so a server restart forgets it, and a child behind a restarted server then reloads no modules: it gets the code version id with its own code. The reconciliation vote deadline is fixed at 30 s; a failed reconciliation closes the link, so the host manager restarts the worker.\n- **Links do not resume.** The provider releases everything held through a closed link at once, so re-held objects are new ones. Keeping references for a reconnect window would need session ids on links. Calls in flight when a link drops reject `unavailable`; whether they ran is unknown.\n- **Refused clients cannot tell.** The WebSocket adapter closes unauthenticated sockets; the client sees a dropped upstream and its calls wait for a reconnect until their ctx is aborted.\n- **The publication queue is process-wide and synchronous.** A slow observer delays every later notification in the process, including those of other hosts in the same process.\n- **No automatic restart.** A terminal host failure is picked up by the host manager's `keepAlive` or `fatal` path, not inside Chord.\n- **The Node loader leaks.** Every reload imports fresh module copies that are never evicted, and it does not isolate plugins (`TODO(loader)`).\n- **Stale socket locks.** On Unix systems other than Linux, two processes that probe the same stale socket file at the same moment could both take the lock. Managers serialize starts per lock name.\n- **Listener `select` identity.** A listener's subscription is kept across factory runs when its origin and `select` are unchanged; `select` counts as unchanged when identical or with the same source text, so a `select` whose implementation depends on captured values that change between runs is not detected.\n- **Shutdown and dead links.** `host.close()` disposes objects in waves (referrers before what they reference) and closes links last. A `close` that awaits a remote call over a link that is already down waits for a reconnect; bound such calls.\n\n### Decisions\n\n- **Errors.** The guide's codes; a `reason` field carries the detail, for example `invalid/args`, `invalid/cycle`, `invalid/duplicate`, `invalid/read_only`, `invalid/not_host_lifetime`, `invalid/registry_conflict`, or `unavailable/unknown_type`. A `ChordError` thrown by a factory or a dependency keeps its code through every dependent; other errors become `unavailable`, with the original as `cause`.\n- **Reload errors.** There is no `reload_aborted` code. A failed reload throws the cause's code and reason with the message `reload aborted: …` (the cause is attached). Reasons of its own are `aborted/gate_deadline` (with the blocking roots in `details`), `aborted/deadline`, `aborted/vote_deadline` and `unavailable/reload_running`. Across hosts, the message names the host that failed. A child cannot reload on its own (`unavailable/not_coordinator`).\n- **New objects during prepare.** A reload pre-allocates the next code version (\"pending\"). Types the same reload adds are served from it; their objects install implementation only for the pending code version and are force-disposed if the reload aborts.\n- **Gate admission.** From the gate until commit or abort, creating a new object of a changed type fails with `upgrade_conflict` (retryable, reason `gate`); nothing is paused, so nothing can deadlock. Gated objects get `minCodeVersion = E'`: calls and writes carrying an older code version are rejected `upgrade_conflict` permanently, and a gated object drops its previous-code-version implementation at commit. Only objects whose cell versions or exclusive resources change are gated.\n- **Tasks and listeners during the gate.** Tasks of gated objects are aborted at the admission cut and restart with new code at commit, or with the old code on abort. Listener events for gated objects are queued and replayed through the new listener at commit, or the old one at abort.\n- **Grandfathered roots** are the roots that called into the object and are still running at the cut. Every host remembers on which links a root's calls went out and sends `root_end` on them when the root ends, so leases outlive calls across hosts.\n- **Removed types and missed retirement.** Objects of removed types are force-disposed at commit. When a retirement deadline is missed, retirement is forced: those roots are aborted first (their ctx signal, reason `superseded`; a relayed call's root cancels its downstream call), then what only that code version declared is closed, and their next call is `superseded`.\n- **Joining during a reload.** Participants are the child hosts in step when a reload starts. A child whose hello arrives while it runs is decided when it ends: in step, or reconciled to the committed code version; a child already behind is fenced at once, so the calls its roots make meanwhile are admitted in the parent's code version. A reconciliation that is running when a reload starts syncs its child only after that reload ended, against the code version it committed. A reconciliation announces the parent's retired code versions before its `prepare`.\n- **Two-version rule.** An object of a changed type created after a commit has only new code; a previous-code-version root calling it gets `superseded`.\n- **Validation.** Remote calls are validated at reference dispatch, in-host too: a direct binding against the provider's schema, a binding through an adapter against the consumer's. A provider validates each snapshot and reset of a remote state member once per (re)subscription, against the consumer's contract through an `accepts` adapter and the provider's otherwise; ops are validated only in dev mode. At reload, the new provider's description must cover the old provider's when the version is unchanged, in addition to the check against live consumers and facets' `uses`.\n- **Contracts.** Local contracts list their members, so a loading reference knows whether a member is a method, state or opaque. Canonical identity omits input fields equal to their default; with TypeBox, use `Type.Optional(Type.X({ default }))` to let callers omit them. Provider variants must cover consumer variants; members the consumer's contract does not list are hidden. Schema change detection compares canonical JSON, so reordering `anyOf` branches counts as a change. Fingerprints use a non-cryptographic 64-bit hash: synchronous and browser-safe; contracts are trusted.\n- **Principals.** A root ctx at ingress acts as its principal (`host.root(fn, { principal })`); contexts inside objects act as `host`. The acceptor decides a link's kind and name (`accept(link, { principal, kind, name })`); only configured host links are addressable, carry roots and may vote, and control frames are accepted only from the parent. Clients never carry roots or code versions. Relaying needs a matching `route`; `authorize(principal, hostName)` runs before `resolve` and again after, and revoking or re-stamping a principal closes its relays at once. `invalidatePrincipal(match)` re-stamps or closes matching links and propagates pattern matches to child hosts.\n- **Cells and readables.** A value returned from `change` replaces the value and is always validated; `replace` transfers ownership without copying; by default (`{ diff }` overrides) it publishes a diff when the new value reuses a container of the old one at the same key, else `r`. `subscribe(fn)` delivers values, starting with the current one; `events(fn)` delivers `snap`, then `ops`, or `reset`. Reference state members are stable readables that throw `not_ready` while loading and `not_in_variant` outside the variant, and reset when their source moves; a listener added before they are readable gets its snapshot then. Remote state members stay hydrated while the reference is held.\n- **References and calls.** A reference's identity is its address; a second `self.get` of the same address in one run returns the same reference. A reference call without ctx, or with a task ctx, starts a fresh root in the current code version; a ctx whose call or root already settled is refused (`aborted/expired`), one from another host `invalid/foreign_ctx`. Calling a reference inside a factory throws `not_ready`.\n- **Resources and disposal.** New and replaced exclusive resources open at the gate, and so do non-exclusive resources of the same object that depend on them. A new object for an address that is still being disposed waits for that disposal. A resource open that completes after its signal aborted closes its value at once.\n- **Links and the host manager.** Every received frame is validated against its schema; malformed frames, duplicate ids and control frames from the wrong side close the link. Each connection pings every `pingInterval` and closes after `pingTimeout` without a frame. The child holds a kernel lock (an abstract socket on Linux and Android, a named pipe on Windows, a probed socket file elsewhere), acquired by `childControl().lock()` before storage opens. The manager kills the child's process group on every exit path and derives `booting` and `idleFor` from the child's liveness reports. WebSocket support is structural, so Chord does not depend on `ws`.\n- **Flow control holds back state only.** A relay (Cloudflare to a browser) can hide a slow reader, so transport backpressure cannot be relied on; acknowledgements are end to end. Results must not wait behind state, so links are FIFO except that waiting state frames can be overtaken: a result may arrive before state changes made before it. Frames of one subscription stay in order. Waiting state is coalesced rather than queued and capped per link, so a stalled peer costs bounded memory, and the next frame it gets is current. The first unit of a new reference is not held back, so a `get` never waits behind other streams.\n- **The state API stays.** Objects keep named cells and unnamed derivations; giving each object exactly one state value was rejected. Do not reopen this without a new, concrete problem.\n\n<!-- File: packages/chord/docs/tutorial.md (Tokens: ~9268 | Category: anthropic-skill) -->\n# Build a shared todo list with Chord\n\n## 1. What problem does Chord solve?\n\nA todo list has data, operations on that data, and several people looking at it.\nYou could hand-write object creation, subscription cleanup, messages between\nprocesses, and code replacement. Chord connects these pieces for you.\n\nA **host** is the runtime that owns objects. An **object** combines state with\nmethods at an address, such as “the family todo list.” A **reference** is a caller's\nreference to that object, not the object itself. Several references can reach the\nsame object. References can also reach another host over a connection.\n\n```text\nbrowser host                         server host\nref + subscribed rows -- link --> Todos view (owner or guest)\n                                           |\n                                        ref\n                                           v\n                                   TodoStore: rows + methods\n```\n\nWe will start inside one host, add observable state, then share it with a\nbrowser. Finally we will add background work, plugins, and hot reload.\n**Live state** means subscribers receive new values when data changes, without\nasking for it again.\n\n<!-- example:01 -->\n```ts\nconst host = createHost({ self: \"server\", name: \"todo-server\" });\nassert.equal(host.name, \"todo-server\");\nawait host.close();\n```\n\nThe assertion passes: the host's name is `todo-server`. Creating a host does\nnot create a todo object, start a server socket, or persist data. `close()`\nshuts down its objects and connections.\n\n> **Tip:** These are executable TypeScript examples, using assertions to show\n> results. Shared imports are at the end. Later sections reuse earlier contracts\n> and functions, but each section's test starts with fresh hosts. Test-only\n> cleanup closes hosts even when an assertion fails; snippets omit that cleanup.\n\n## 2. Your first object\n\nBefore callers can use an object, they need to agree on its name and members.\nThat agreement is a **contract**: a runtime value that also gives TypeScript its\ntypes. A **schema** describes a value's shape. We use TypeBox's `Type` to write\nschemas. `method(args, result)` declares a method by its argument and result\nschemas: `method()` takes no arguments and returns nothing, and\n`method(undefined, Type.String())`, used below, takes no arguments and returns a\nstring.\n\nStart with a label for a named list. `local` means only code inside this host\ncan reach it. The input `{ list: \"family\" }` identifies which list we want;\nit is not an argument to `label`.\n\n<!-- example:02-contract -->\n```ts\nconst TodoLabel = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { label: method(undefined, Type.String()) },\n});\n```\n\nA **factory** is a synchronous function that creates the object's implementation:\nthe methods and state callers will see. `serve` registers a contract and its\nfactory. Registration alone does not activate this lazy object; the first\n`get` does.\n\nA **root** is one short tree of calls, such as the work for a button click.\n`host.root` starts it. Its **context**, called `ctx`, carries cancellation,\ncaller identity, and the code version for nested calls. Pass it to methods you\ncall from that root.\n\n<!-- example:02-call -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoLabel, ({ list }) => ({ label: () => `Todos for ${list}` }));\nconst result = await host.root(async (ctx) => {\n\tconst todos = await ctx.get(TodoLabel, { list: \"family\" });\n\treturn todos.label(ctx);\n});\nassert.equal(result, \"Todos for family\");\n```\n\nThe result is `\"Todos for family\"`. `await ctx.get` resolves once the object is\nlive (and, as we will see, has its state). The reference acquired through\n`ctx.get` is released when this root ends. We never receive the factory's\nreturned object directly. A standalone call without `ctx`, such as\n`todos.label()`, starts its own root. A method without arguments takes only\nthe context: `todos.label(ctx)`; its implementation is `(ctx) => …`. Inside a root or method, pass the\nexisting `ctx` so nested calls stay in the same call tree.\n\nThe factory may also receive `principal` (who is asking) and `self` (the\nobject's declaration API). We did not need them yet.\n\n> **Why:** The reference separates callers from implementations. Later, replacing\n> code will not require replacing every caller's reference.\n\n## 3. Give the list live state\n\nA label is not a todo list. We need rows that can change, and a count that always\nagrees with them. A **cell** owns an observable JSON value. A **derivation**\ncomputes another observable value from cells or other readable sources.\n\nExtend the contract with `rows`, `openCount`, and editing methods. State\nmembers describe values, not methods. This adds members at contract version 1;\nolder callers that only need `label` still have everything they need.\n\n`self.state(name, schema, initial)` declares a cell and returns it. The name\n`rows` is important: on reload, Chord matches that name to the existing cell and\nkeeps its value. `self.derive(sources, fn)` declares a derivation; it has no\nname, because it holds nothing a reload must keep.\n\n<!-- example:03-contract -->\n```ts\nconst Todo = Type.Object({ text: Type.String(), done: Type.Boolean() });\nconst Rows = Type.Array(Todo);\nconst TodoStore = local(\"tutorial.todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: {\n\t\tlabel: method(undefined, Type.String()),\n\t\trows: Rows,\n\t\topenCount: Type.Number(),\n\t\tadd: method(Type.Object({ text: Type.String() })),\n\t\tfinish: method(Type.Object({ index: Type.Integer() })),\n\t\treset: method(Rows),\n\t},\n});\n```\n\nThe factory now declares a cell and a derivation. It is wrapped in `todoStore`,\nwhich takes the cell's options (section 9 raises its version); for now\n`todoFactory` uses the defaults. Each method receives its arguments and a\ncontext; changing the cell updates its readers and its derived count together.\n`add`, `finish` and `reset` declare no result, so they return nothing.\n\n<!-- example:03-factory -->\n```ts\n/** The store's factory; `options` versions its rows (section 9). */\nconst todoStore =\n\t(options?: StateOptions<Static<typeof Rows>>): Factory<typeof TodoStore> =>\n\t({ list }, _principal, self) => {\n\t\tconst rows = self.state(\"rows\", Rows, [], options);\n\t\tconst openCount = self.derive([rows], (todos) => todos.filter((todo) => !todo.done).length);\n\t\treturn {\n\t\t\tlabel: () => `Todos for ${list}`,\n\t\t\trows,\n\t\t\topenCount,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tdraft.push({ text, done: false });\n\t\t\t\t});\n\t\t\t},\n\t\t\tfinish: ({ index }, ctx) => {\n\t\t\t\trows.change(ctx, (draft) => {\n\t\t\t\t\tconst todo = draft[index];\n\t\t\t\t\tif (!todo) throw invalid(\"args\", \"No such todo\");\n\t\t\t\t\ttodo.done = true;\n\t\t\t\t});\n\t\t\t},\n\t\t\treset: (next, ctx) => {\n\t\t\t\trows.replace(ctx, next, { diff: true });\n\t\t\t},\n\t\t};\n\t};\nconst todoFactory = todoStore();\n```\n\n`change(ctx, fn)` gives `fn` a temporary mutable **draft**. Here `add`\npushes a row and `finish` changes one field. The draft is discarded if the\ncallback throws. The callback must be synchronous.\n\n`replace(ctx, value)` replaces the whole value. We use it for resetting a\nlist we own completely. `{ diff: true }` asks Chord to publish just the\nstructural changes. Without that option, replacement diffs automatically when\nthe new value shares containers at the same keys with the old one; unrelated\nvalues normally publish a whole-value replacement. Either way, subscribers\nreceive the same resulting value.\n\n<!-- example:03-call -->\n```ts\nconst host = createHost({ self: \"server\", dev: true });\nhost.serve(TodoStore, todoFactory);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nconst counts: (number | undefined)[] = [];\nconst stop = todos.openCount.subscribe((value) => counts.push(value));\nawait host.root(async (ctx) => {\n\tawait todos.add({ text: \"Buy milk\" }, ctx);\n\tawait todos.finish({ index: 0 }, ctx);\n\tawait todos.reset([], ctx);\n});\nassert.deepEqual(counts, [0, 1, 0]);\nassert.deepEqual(todos.rows.value, []);\nstop();\ntodos.release();\n```\n\nThe count subscriber sees `0, 1, 0`: initially empty, one unfinished todo,\nthen no unfinished todos. Resetting to empty does not add another count\nnotification because the derived count is still zero. The final rows are `[]`.\n`subscribe` calls back immediately with the current value and returns an\nunsubscribe function. The awaited `get` makes sure there is a value: a reference\nyou hold is live, with its state.\n\n> **Why:** Store reloadable state in cells, not closure variables. Treat\n> `cell.value`, `ref.rows.value`, and subscriber values as immutable.\n> Only mutate drafts inside `change`; do not keep a draft for later use.\n> A derivation runs synchronously, so use it for projections, not database work.\n\n## 4. Let objects use objects and resources\n\nSaving a todo may require both the list and a database connection. A\n**reference** keeps the object it refers to alive. `self.get` declares a\nreference owned by this object; it lasts longer than a single method call.\n\nA **resource** is something the runtime opens and later closes, such as a\ndatabase connection. Its `open` may be asynchronous even though the factory\nmust be synchronous. The example database is deliberately fake: it just saves\nstrings in memory.\n\n`opaque<T>(\"name@version\")` describes a native, host-only member. It is not\nJSON state and cannot be sent over a link. `self.expose` connects that member\nto a declared resource, without reading the resource during the factory.\n\n<!-- example:04-database -->\n```ts\nconst Database = local(\"tutorial.database\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { db: opaque<{ saved: string[] }>(\"tutorial.database@1\") },\n});\nconst SavedTodos = local(\"tutorial.saved-todos\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\tmembers: { add: method(Type.Object({ text: Type.String() })) },\n});\nconst host = createHost({ self: \"server\" });\nlet opens = 0;\nconst closed = Promise.withResolvers<void>();\nhost.serve(Database, (_input, _principal, self) => {\n\tconst db = self.resource(\"db\", {\n\t\tversion: 1,\n\t\topen: async () => {\n\t\t\topens++;\n\t\t\treturn { saved: [] as string[] };\n\t\t},\n\t\tclose: () => closed.resolve(),\n\t});\n\treturn { db: self.expose(db) };\n});\n```\n\nThe factory returns an exposed marker, not an opened connection. Chord runs\n`open` after declarations finish. The promise named `closed` lets the test\nobserve eventual cleanup.\n\nNow declare `SavedTodos` as an object that holds both the store and the database.\nIts editing method uses these dependencies only after activation.\n\n<!-- example:04-saving -->\n```ts\nhost.serve(TodoStore, todoFactory);\nhost.serve(\n\tSavedTodos,\n\t({ list }, _principal, self) => {\n\t\tconst database = self.get(Database, {});\n\t\tconst todos = self.get(TodoStore, { list });\n\t\treturn {\n\t\t\tadd: async ({ text }, ctx) => {\n\t\t\t\tdatabase.db!.saved.push(text);\n\t\t\t\tawait todos.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t},\n\t{ linger: 0 },\n);\n```\n\nAn outside consumer holds a `SavedTodos` reference. We also hold `Database`\ndirectly so we can inspect what was saved.\n\n<!-- example:04-call -->\n```ts\nconst saved = await host.get(SavedTodos, { list: \"family\" });\nconst database = await host.get(Database, {});\nawait host.root((ctx) => saved.add({ text: \"Buy milk\" }, ctx));\nassert.deepEqual(database.db!.saved, [\"Buy milk\"]);\nassert.equal(opens, 1);\nconst statuses: string[] = [];\nsaved.onStatus((status) => statuses.push(status));\ndatabase.release();\nsaved.release();\nawait closed.promise;\nassert.equal(saved.status, \"released\");\nassert.ok(statuses.includes(\"released\"));\n```\n\nAfter the method, the fake database contains `[\"Buy milk\"]`. It opened once,\neven though two objects referenced it. `SavedTodos` and `Database` become live\nonly after their resources and declared dependencies are ready. That is why\n`database.db!` is safe inside this live object's method: activation has\nalready established that dependency. An outside caller awaits its `get`.\n\nWhen both external references are released, `SavedTodos` is disposed and\nreleases its own database reference. With no references left, the database closes; we\nwait for `closed.promise` to prove it. The released reference stays\n`released`, and `onStatus` reports that transition. `onStatus` watches\nchanges; unlike state subscription, it does not deliver an initial status.\n\n`linger` is a delay in milliseconds after the last reference is released. Zero disposes\nwithout a grace period; a positive value can avoid reopening an expensive\nresource when a screen is briefly hidden. Linger is not persistence.\n\n> **Tip:** A factory only declares dependencies. Never call a referenced object's method or\n> read `resource.value` there, including during reload. Activation happens\n> afterward. Bump a resource's `version` when its configuration should be reopened.\n> Disposal stops tasks and listeners, closes resources, then releases references.\n\n## 5. Who is asking? One store, two views\n\nOwners may add todos; guests may only read them. A **principal** is the\napplication-defined identity of a caller. A **variant** is a named set of\nmembers a remote object offers to that principal. It lets the UI discover\nwhether an editing method is available.\n\nDo not create a separate data cell for each viewer: their lists would diverge.\nInstead, each view holds the same host-local `TodoStore` and exposes its rows.\n`self.get` acts as the host's own principal, so owner and guest views share\nthat store. Remote view objects themselves are separate per principal key.\nA remote contract also says where its objects live: `host: hostNamed(\"server\")`\nputs every `Todos` object on the host named `server`. Section 6 uses that.\n\n<!-- example:05-views -->\n```ts\nconst Todos = remote(\"tutorial.todo-view\", {\n\tversion: 1,\n\tinput: Type.Object({ list: Type.String() }),\n\thost: hostNamed(\"server\"),\n\tvariants: {\n\t\tviewer: { state: { rows: Rows } },\n\t\teditor: {\n\t\t\tstate: { rows: Rows },\n\t\t\tmethods: { add: method(Type.Object({ text: Type.String() })) },\n\t\t},\n\t},\n});\n\n/** Who may ask: the owner, a guest, or the server's own code. */\ntype Principal = \"owner\" | \"guest\" | \"server\";\n\nfunction serveViews(api: Pick<Host<Principal>, \"serve\">): void {\n\tapi.serve(Todos, ({ list }, principal, self) => {\n\t\tif (list !== \"family\") throw forbidden(\"This list is not shared\");\n\t\tconst store = self.get(TodoStore, { list });\n\t\tif (principal === \"guest\") {\n\t\t\treturn { variant: \"viewer\", rows: store.rows };\n\t\t}\n\t\treturn {\n\t\t\tvariant: \"editor\",\n\t\t\trows: store.rows,\n\t\t\tadd: ({ text }, ctx) => {\n\t\t\t\tif (text.trim().length === 0) throw invalid(\"args\", \"A todo needs text\");\n\t\t\t\treturn store.add({ text }, ctx);\n\t\t\t},\n\t\t};\n\t});\n}\n```\n\nHere principals are the strings `\"owner\"` and `\"guest\"`, assigned by\ntrusted application code, and `\"server\"`, the host's own code: every host is\ncreated with `self`, the principal its own roots and objects act as. Chord\ninvents no principals. The `Principal` type parameter of `createHost` and\n`Host` makes `principal` in the factory that type, so TypeScript checks the\ncomparisons. `principalKey` maps identity to object identity. Real\napplications should use stable identity, connection provenance, and grant\nrevision, not display names.\n\n<!-- example:05-call -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", principalKey: (principal) => principal });\nhost.serve(TodoStore, todoFactory);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nconst guest = await host.get(Todos, { list: \"family\" }, { principal: \"guest\" });\nassert.ok(owner.variant === \"editor\"); // narrows the reference to the editor variant\nassert.equal(guest.variant, \"viewer\");\nawait host.root((ctx) => owner.add({ text: \"Buy milk\" }, ctx), { principal: \"owner\" });\nassert.deepEqual(guest.rows.value, [{ text: \"Buy milk\", done: false }]);\n// @ts-expect-error A viewer has no add: TypeScript refuses it, and so does the provider.\nawait assert.rejects(guest.add({ text: \"Not allowed\" }), (error) => isChordError(error, \"not_in_variant\"));\nawait assert.rejects(owner.add({ text: \" \" }), (error) => isChordError(error, \"invalid\"));\n```\n\nThe owner gets `editor`; the guest gets `viewer`. Both see the same milk\ntodo. A reference is typed as the union of its variants: `add` is reachable only\nafter narrowing on `variant` (`assert.ok(owner.variant === \"editor\")` does that).\nThe guest's `add` is refused by TypeScript, and, called anyway, rejects with\n`not_in_variant`. Whitespace-only text\nrejects with `invalid`: it is a string, as the schema demands, but it breaks a\nrule only the method knows. Use `invalid` for bad input and `forbidden` only\nfor callers who may not do something.\n\n> **Why:** A variant is not all of authorization. The factory checks which\n> list may be opened; the method checks the operation's arguments. Production\n> code must also enforce grants that can change. Use\n> `host.invalidatePrincipal(match)` to revoke matching objects; held references\n> become `gone`. A UI watches `onStatus` and gets a new reference when appropriate.\n> Guest-provided names or message text must never establish identity.\n\n## 6. Reach the list from another process\n\nA browser cannot call a host-only contract. `remote` contracts describe only\nschema-checked state and methods that can travel as JSON. Our `Todos` view is\nremote; the underlying `TodoStore` and database stay local. A caller never\nnames a host: `browser.get(Todos, …)` goes to `server` because the contract\nsays so, and the server refuses to serve, or to be asked for, a type it is not\nthe place of. A type that lives on one host per session says\n`host: hostFamily(\"session:\", \"session\")`: its input's `session` field picks\nthe host (`{ session: \"S1\" }` lives on `session:S1`).\n\nA **link** carries ordered messages between hosts. A memory link runs both ends\nin one test process, but uses the remote protocol. The accepting host stamps\nthe caller's principal from its authenticator, not from method arguments or a\nclient's claimed identity. The connecting side names the principal its parent's\nown requests act as (`upstream: { link, principal }`, or `connect(link, {\nprincipal })`).\n\n<!-- example:06 -->\n```ts\nconst server = createHost<Principal>({ self: \"server\", name: \"server\" });\nserver.serve(TodoStore, todoFactory);\nserveViews(server);\nconst [down, up] = memoryLinkPair();\nserver.accept(down, { principal: \"owner\", kind: \"client\" });\nconst browser = createHost({\n\tself: \"browser\",\n\tname: \"browser\",\n\tkind: \"client\",\n\tupstream: { link: up, principal: \"server\" },\n});\nconst todos = await browser.get(Todos, { list: \"family\" });\nassert.ok(todos.variant === \"editor\");\nassert.deepEqual(todos.rows.value, []);\nconst updated = Promise.withResolvers<void>();\nconst stop = todos.rows.subscribe((rows) => {\n\tif (rows.length === 1) updated.resolve();\n});\nawait browser.root((ctx) => todos.add({ text: \"Buy milk\" }, ctx));\nawait updated.promise;\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\n// Deliberately bypass TypeScript to simulate a malformed caller.\nawait assert.rejects(todos.add({ text: 123 } as never), (error) => isChordError(error, \"invalid\"));\nstop();\n// A held reference keeps its state without listeners.\nassert.deepEqual(todos.rows.value, [{ text: \"Buy milk\", done: false }]);\ntodos.release();\n```\n\nThe browser sees an empty snapshot, then the new milk todo. `ready` means\nthe remote reference has bound to a live object **and** has its state: every\nstate member of its variant has a snapshot. A held reference stays subscribed,\nwith or without listeners, until it is released.\n\nWhat crosses the wire: the input and consumer's contract description, the\nprovider's shape (variant and members), method arguments and results, and the\nstate of the reference's variant. State starts with a **snapshot**, the whole current value;\nsubsequent **deltas** describe changes. Rebinds and reloads can send a\n`reset`, a fresh whole value. Functions, resource connections, and cell\ninstances do not cross the wire.\n\nThe deliberate bad argument rejects with `invalid`. Remote arguments,\nresults, and outgoing state are validated at the provider boundary. Local\nmethods do not get per-call schema validation; they still need app-level checks.\n`dev: true` additionally checks cell writes while developing.\n\n> **Tip:** Replace `memoryLinkPair` with `webSocketLink` for a WebSocket or\n> `ipcLink` for a child-process IPC channel; object contracts stay the same.\n> Those adapters wrap an existing connection, not an authenticator. Configure\n> `accept` with an authenticated principal. A browser is `kind: \"client\"`;\n> a trusted child host is accepted with `kind: \"host\"` and its configured name.\n\n## 7. React to changes and run background work\n\nNot all work comes from method calls. The app may update an activity indicator\nwhen rows change, or run a background reminder. A **listener** reacts to an\nobservable source. A **task** is background execution owned by an object,\nwhich is aborted when the object is disposed or its code is reloaded.\n\nOur activity object mirrors the row count. Its task inserts a sample todo,\nthen waits for shutdown. The promises make those events deterministic in a\ntest; they are not stored application state.\n\n<!-- example:07 -->\n```ts\nconst Activity = local(\"tutorial.activity\", {\n\tversion: 1,\n\tinput: Type.Object({}),\n\tmembers: { seen: Type.Number() },\n});\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst observed = Promise.withResolvers<void>();\nconst seeded = Promise.withResolvers<void>();\nconst stopped = Promise.withResolvers<void>();\nhost.serve(Activity, (_input, _principal, self) => {\n\tconst todos = self.get(TodoStore, { list: \"family\" });\n\tconst seen = self.state(\"seen\", Type.Number(), 0);\n\tself.on(\"rows\", todos.rows, (rows, ctx) => {\n\t\tseen.replace(ctx, rows?.length ?? 0);\n\t\tif (rows?.length === 1) observed.resolve();\n\t});\n\tself.task(async (ctx) => {\n\t\tawait todos.add({ text: \"Review the list\" }, ctx);\n\t\tseeded.resolve();\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tif (ctx.signal.aborted) resolve();\n\t\t\telse ctx.signal.addEventListener(\"abort\", () => resolve(), { once: true });\n\t\t});\n\t\tstopped.resolve();\n\t});\n\treturn { seen };\n});\nconst activity = await host.get(Activity, {});\nawait Promise.all([seeded.promise, observed.promise]);\nassert.equal(activity.seen.value, 1);\nactivity.release();\nawait stopped.promise;\n```\n\nThe final `seen` is `1`. Each listener event receives a fresh root's\n`ctx`; its write belongs to that short call tree. A task is not one enormous\nroot: Chord method calls made with its task context start fresh short roots.\nWe pass that context into `todos.add` so cancellation follows the task.\nReleasing the activity reference aborts its task and resolves `stopped`.\n\n`self.on(name, source, handler)` declares a listener; `self.task(fn)` a task.\nA listener on state, like `rows` here, gets the current value when it starts,\nthen every change. It gets the current value again whenever a reload replaced\nits source (a derivation is new at every reload), so its handler must\nreconcile (`seen` is set from the rows, not counted up). Listeners can also\nsubscribe to a resource's event stream with `self.on(name, resource, select,\nhandler)`: `select` gets the opened resource and returns something with\n`subscribe`; an event stream delivers no current value. Kept subscriptions\nsurvive reload while the resource and selector remain unchanged; the handler\nswitches to new code. A selector that captures changing configuration should\nget a new listener name. Tasks have no names: a reload restarts them all.\n\n> **Why:** Do not wait for a person, sleep for minutes, or run an endless loop\n> inside a method. That pins a root and can block state migration. Store a\n> pending request in a cell and answer in another call, or use a task that obeys\n> `ctx.signal`. Never retain a completed method's `ctx` for background work.\n\n## 8. Package features as plugins\n\nNow a plugin can contribute a menu action without editing a central switch.\nA **registry** is a named table of contributions. A **facet** is a plugin's code\nfor one kind of host: it serves object types. A plugin can have server, worker, and browser\nfacets, each installing that part of the feature.\n\nRegistrations belong to objects of **lifetime `\"host\"`**: the host creates such\nan object when its facet loads and keeps it until the host closes, even if no\nscreen asks for it. That keeps our menu contribution present. Objects of the\ndefault lifetime, `\"referenced\"`, are still created only when referenced.\n\n<!-- example:08 -->\n```ts\nconst Actions = defineRegistry<(ctx: Ctx) => Promise<number>>(\"tutorial.actions\");\nconst Menu = local(\"tutorial.menu\", { version: 1, input: Type.Object({}) });\nlet click: (() => Promise<number>) | undefined;\nconst storeFacet: Facet = {\n\tname: \"todo-store\",\n\t...defineFacet((api) => api.serve(TodoStore, todoFactory)),\n};\nconst menuFacet: Facet = {\n\tname: \"todo-menu\",\n\t...defineFacet(\n\t\t(api) => {\n\t\t\tapi.serve(\n\t\t\t\tMenu,\n\t\t\t\t(_input, _principal, self) => {\n\t\t\t\t\tconst todos = self.get(TodoStore, { list: \"family\" });\n\t\t\t\t\tself.register(Actions, {\n\t\t\t\t\t\taddSample: async (ctx) => {\n\t\t\t\t\t\t\tawait todos.add({ text: \"Try plugins\" }, ctx);\n\t\t\t\t\t\t\treturn todos.openCount.value;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\treturn {};\n\t\t\t\t},\n\t\t\t\t{ lifetime: \"host\" },\n\t\t\t);\n\t\t\tclick = () => api.root((ctx) => api.registry(Actions).get(\"addSample\", ctx)!(ctx));\n\t\t},\n\t\t{ uses: [TodoStore] },\n\t),\n};\nconst host = createHost({ self: \"server\" });\nawait host.load([menuFacet, storeFacet], { awaitHostObjects: false });\nawait host.ready();\nassert.equal(await click!(), 1);\nassert.equal(host.registry(Actions).table().size, 1);\n```\n\nClicking returns `1`, and the registry has one entry. The menu's dependency\ncan come from a later facet in the load batch: `load` installs the whole\nbatch before waiting for host objects to activate. The facet declares `uses` so\nreload can also check the contract it consumes.\n\nThe facet API supplies `serve`, `name`, `root`, and `registry`.\nOur ordinary UI callback uses its saved `api.root`, not a process-global\nhost. Looking up a registry entry with `ctx` uses the table belonging to that\nroot's code version. Without `ctx`, lookup uses the current table. Duplicate\nkeys fail by default; `subscribe` can bridge tables to an imperative consumer.\n\nNormally use `await host.load(facets)` (or `await host.install(facet)`),\nwhich waits for those host objects. This example explicitly defers that barrier\nwith `awaitHostObjects: false`, then awaits `host.ready()`. That waits for a\nsnapshot of the host's objects, not for task completion or later loads.\nFailures reject the readiness promise; abandoning startup requires closing the\nhost, because installation is not a rollback transaction.\n\n> **Tip:** Use deferred readiness only when needed. A worker may have to await\n> an independent local bootstrap reference, announce control readiness so its\n> parent admits its link, and only then await `host.ready()`. Waiting for\n> upstream-dependent host objects before admission would deadlock.\n\nFor real plugin files, default-export `defineFacet(...)`. Node hosts configure\n`createHost({ loader: nodeLoader })`, with `nodeLoader` imported from\n`@earendil-works/chord/node`, then pass module URLs to `load` and `reload`.\nThe in-memory facet objects here avoid filesystem setup. Built-in facets are\ntrusted in-process code, not a sandbox; the current Node loader keeps old module\ncopies in memory, so repeated loading has that limitation.\n\n## 9. Change running code without losing the list\n\nA reference is useful precisely because callers keep it while implementations\nchange. An **code version** identifies one committed set of code and registry tables.\nA root keeps the code version it started with. New roots use the latest one.\n\n### Compatible code change: no waiting\n\nGive the same named facet a new factory. It changes the label but keeps the\ncell declaration. This test deliberately pauses one caller to show what an\nold root sees; application methods should not pause like this.\n\n<!-- example:09-compatible -->\n```ts\nconst host = createHost({ self: \"server\" });\nconst facet = (label: string): Facet => ({\n\tname: \"todos\",\n\tsetup: (api) =>\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tlabel: () => label,\n\t\t})),\n});\nawait host.load([facet(\"Original\")]);\nconst todos = await host.get(TodoStore, { list: \"family\" });\nawait todos.add({ text: \"Buy milk\" });\nconst entered = Promise.withResolvers<void>();\nconst resume = Promise.withResolvers<void>();\nconst oldRoot = host.root(async (ctx) => {\n\tconst first = await todos.label(ctx);\n\tentered.resolve();\n\tawait resume.promise; // Test-only pause: pin an old caller during reload.\n\treturn [first, await todos.label(ctx)];\n});\nawait entered.promise;\nconst compatible = await host.reload([facet(\"Updated\")]);\nassert.deepEqual(compatible.gated, []);\nassert.equal(await todos.label(), \"Updated\");\nresume.resolve();\nassert.deepEqual(await oldRoot, [\"Original\", \"Original\"]);\nassert.equal(todos.rows.value?.[0]?.text, \"Buy milk\");\n```\n\nReload commits while the old root is paused: `gated` is empty. New calls see\n`\"Updated\"`; the resumed old root sees `\"Original\"` twice. The milk row\nsurvives. Compatible code changes do not wait for that old root to finish.\n\n### State change: bump its version and migrate\n\nSuppose a new policy converts existing todo text to uppercase. The schema is\nunchanged, but the data's meaning changes. We still bump the cell's **state\nversion** (`self.state`'s `version` option, default 1). It belongs to one cell,\nindependently of the contract version. A **migration** (`migrate`) converts an\nold value of that cell to the new representation.\n\n<!-- example:09-migrate -->\n```ts\nconst events: string[] = [];\nconst stopEvents = todos.rows.events((event) => events.push(event.type));\nconst migrated = await host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tTodoStore,\n\t\t\t\ttodoStore({\n\t\t\t\t\tversion: 2,\n\t\t\t\t\tmigrate: (old, from) => {\n\t\t\t\t\t\tassert.equal(from, 1);\n\t\t\t\t\t\treturn (old as Static<typeof Rows>).map((todo) => ({ ...todo, text: todo.text.toUpperCase() }));\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t),\n\t},\n]);\nassert.equal(migrated.gated.length, 1);\nassert.deepEqual(todos.rows.value, [{ text: \"BUY MILK\", done: false }]);\nassert.deepEqual(events, [\"snap\", \"reset\"]);\nstopEvents();\n```\n\nThe migration receives the old rows and their state version, `1`. It returns\nnew rows; the reference now reads `[{ text: \"BUY MILK\", done: false }]`.\nSubscribers get a `reset` event containing the migrated value. The event\nassertion sees an initial `snap`, then that `reset`.\nOne object is gated. A **gate** waits for calls already inside that object to\nfinish before migrating a private copy, validating it, and committing. Old\nroots that had not entered it cannot later use old code over migrated data:\nthey get `upgrade_conflict` and should retry in a new root.\n\nChanging a cell's schema also requires a version bump, and a migration unless\nthe old value already fits the new schema. A change of meaning with the same schema,\nlike this example, cannot be detected automatically. Real migrations must\nreference each stored version they support, not assume every installation is\nexactly one version behind.\n\n### Contract change: keep an adapter for old callers\n\nRename `label` to `heading`. That is a breaking contract change, so use\ncontract version 2. The provider's `accepts[1]` adapter builds the implementation\nthat version-1 consumers still expect.\n\n<!-- example:09-contract -->\n```ts\n// Remove label from the new provider's contract: it is now called heading.\nconst { label: _label, ...members } = TodoStore.def.members;\nconst RenamedStore = local(\"tutorial.todos\", {\n\tversion: 2,\n\tinput: TodoStore.def.input,\n\tmembers: { ...members, heading: method(undefined, Type.String()) },\n});\nawait host.reload([\n\t{\n\t\tname: \"todos\",\n\t\tsetup: (api) =>\n\t\t\tapi.serve(\n\t\t\t\tRenamedStore,\n\t\t\t\t(input, principal, self) => {\n\t\t\t\t\tconst { label, ...implementation } = todoStore({ version: 2 })(input, principal, self);\n\t\t\t\t\treturn { ...implementation, heading: label };\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\taccepts: {\n\t\t\t\t\t\t1: (current) => ({ ...current, label: current.heading }),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t},\n]);\nassert.equal(await todos.label(), \"Todos for family\");\nconst newer = await host.get(RenamedStore, { list: \"family\" });\nassert.equal(await newer.heading(), \"Todos for family\");\nassert.equal(todos.rows.value?.[0]?.text, \"BUY MILK\");\n```\n\nThe old reference's `label` and the new reference's `heading` both return\n`\"Todos for family\"`. Both still reach the migrated rows. The adapter maps\nimplementations, not references; forwarding the existing method preserves its context.\nWe keep the cell at version 2 here so this code-only change does not repeat\nmigration.\n\n> **Why:** Contract versions protect callers; state versions protect stored\n> data; resource versions control reopening. Do not use one number for all\n> three. Additive compatible contracts can keep their version; breaking shape\n> or meaning changes need a contract bump and adapters for old consumers.\n> Reload prepares candidates first; preparation failure leaves the old code\n> running. A migration gate that misses its deadline aborts rather than\n> switching data underneath active old code.\n\n## 10. Test the objects, not a whole deployment\n\nMost implementation needs only a bare host. Use real factories for state tests, and a\nfake facet when you want to test delegation without a real database or service.\nHere the fake records what the editing view sends to the store.\n\n<!-- example:10 -->\n```ts\nconst host = createHost<Principal>({ name: \"server\", self: \"server\", dev: true });\nconst received: string[] = [];\nconst fake: Facet = {\n\tname: \"fake-store\",\n\t...defineFacet((api: FacetApi) => {\n\t\tapi.serve(TodoStore, (input, principal, self) => ({\n\t\t\t...todoFactory(input, principal, self),\n\t\t\tadd: ({ text }) => {\n\t\t\t\treceived.push(text);\n\t\t\t},\n\t\t}));\n\t}),\n};\nawait host.load([fake]);\nserveViews(host);\nconst owner = await host.get(Todos, { list: \"family\" }, { principal: \"owner\" });\nassert.ok(owner.variant === \"editor\");\nawait owner.add({ text: \"Test delegation\" });\nassert.deepEqual(received, [\"Test delegation\"]);\nawait assert.rejects(owner.add({ text: \"\" }), (error) => isChordError(error, \"invalid\"));\nowner.release();\nawait assert.rejects(owner.add({ text: \"Too late\" }), (error) => isChordError(error, \"released\"));\n```\n\nThe fake received `[\"Test delegation\"]`. Empty text failed before reaching\nthe fake. The released reference rejects further calls with `released`.\n`isChordError(error, code)` checks a stable error code; avoid testing only a\nmessage's wording.\n\nOur other tests exercise the real cells (section 3), resource cleanup (4),\nprincipal variants (5), a client over a memory link (6), and callers kept through\nreload (9). After an awaited `get` the state is there; for later changes, await\nthe value you need. Always unsubscribe and close hosts; leaked references can hide\nlifetime bugs or leave background tasks running.\n\n> **Tip:** A fake must honor the contract it serves. This fake delegates all\n> ordinary members to `todoFactory` and replaces only `add`; it deliberately\n> objects calls rather than updating rows. Do not confuse that with persistence.\n\n## 11. Keep these habits\n\nA temporary call reference should not accidentally become a permanent one. A\nscreen should not lose its data merely because a button click finishes. Use\n`ctx.get` for the first lifetime and `host.get` for the second.\n\n<!-- example:11 -->\n```ts\nconst host = createHost({ self: \"server\" });\nhost.serve(TodoStore, todoFactory);\nconst screen = await host.get(TodoStore, { list: \"family\" });\nawait host.root(async (ctx) => {\n\tconst temporary = await ctx.get(TodoStore, { list: \"family\" });\n\tawait temporary.add({ text: \"Keep roots short\" }, ctx);\n}); // The temporary reference is released here; screen still holds the store.\nassert.equal(screen.openCount.value, 1);\nscreen.release(); // No reference remains; without persistence, disposal loses these rows.\n```\n\nThe screen still sees count `1` after the root finishes, because it owns a\nseparate reference. When the screen releases it, no reference remains. This tutorial's cells\nare in memory: reload preserves a live object, but disposal or process restart\nloses its rows. Real persistence belongs in a database resource, with an\nexplicit load/save policy.\n\n### Checklist and common mistakes\n\n- Keep reloadable state in cells, not closure variables: factories rerun.\n- Change drafts, not published values: mutation outside cells bypasses updates.\n- Use `change` for field edits and `replace` for owned snapshots: replacing\n  can erase fields your code does not know about.\n- Declare dependencies in a synchronous factory, not during methods: activation\n  and reload need a complete declaration list.\n- Use `ctx.get` for temporary method dependencies: their references end with the call.\n- Pass `ctx` into nested calls: cancellation and code versions must stay together.\n- Keep roots short and tasks cancellable: migration and shutdown need work to settle.\n- Check grants and arguments, not just variants: available methods are not a full policy.\n- Stamp principals at trusted ingress, not from client payloads: names are not identities.\n- Await `get` before reading state: a reference you hold is live and hydrated, a\n  loading one (`host.hold`) throws `not_ready`.\n- Release references and unsubscribe: otherwise unused objects remain referenced or observed.\n- Bump state versions, contract versions, and resource versions for their own changes:\n  each protects a different boundary.\n- Await host object readiness before reporting startup success: declared is not yet live.\n- Keep reference dependencies acyclic: an object cannot wait for itself through another object.\n\n### Imports and running the examples\n\nThese are the shared imports from `test/tutorial.test.ts`. The relative\npath is correct from that test directory; applications import the same names\nfrom `@earendil-works/chord`. `assert` expresses expected values; `it` and\n`onTestFinished` come from the test runner; `readFileSync` is used by the\ntutorial's source-matching check, not by the application.\n\n<!-- example:imports -->\n```ts\nimport assert from \"node:assert/strict\";\nimport { readFileSync } from \"node:fs\";\nimport { type Static, Type } from \"typebox\";\nimport { it, onTestFinished } from \"vitest\";\nimport {\n\ttype Ctx,\n\tcreateHost,\n\tdefineFacet,\n\tdefineRegistry,\n\ttype Facet,\n\ttype FacetApi,\n\ttype Factory,\n\tforbidden,\n\ttype Host,\n\thostNamed,\n\tinvalid,\n\tisChordError,\n\tlocal,\n\tmemoryLinkPair,\n\tmethod,\n\topaque,\n\tremote,\n\ttype StateOptions,\n} from \"../src/index.ts\";\n```\n\nFrom `packages/chord`, run:\n\n```sh\nnode ../../node_modules/vitest/dist/cli.js --run test/tutorial.test.ts\n```\n\nThere is one test per numbered section. Every TypeScript block is matched\nagainst its runnable source, so changing either copy without the other fails a\ntest. The test harness adds teardown and drift checks; it never uses a real\ndatabase, network service, or provider API.\n\nNext read [the usage guide](guide.md) for the API reference and\n[the specification](spec.md) for compatibility, lifetime, and reload\nrules, including its known limitations. If a reference disagrees with\n`src`, the current implementation wins.\n\n<!-- File: packages/coding-agent/docs/cli-integration.md (Tokens: ~1311 | Category: anthropic-skill) -->\n# CLI Integration\n\nBy default, running `pi` opens the interactive terminal interface. When input or output is piped or redirected, Pi uses print mode instead. You can also select print, JSON, or RPC mode explicitly for scripts and applications.\n\nAll four modes use the same agent, sessions, resources, and tools. The mode determines how input enters Pi, how output is exposed, and whether the process remains available for more commands.\n\nThe SDK is not a CLI mode. It embeds the agent directly in a Node.js or Bun process. See the [SDK](sdk.md) when direct TypeScript access is preferable to a process boundary.\n\n## Choose a mode\n\n| Mode | Interface | Lifetime | Use it when |\n|---|---|---|---|\n| Interactive | Terminal UI | Until the user exits | A person is working with Pi directly |\n| Print | Final text on stdout | One invocation | A script needs the final assistant response |\n| JSON | JSONL events on stdout | One invocation | A process needs structured progress from a run |\n| RPC | JSONL commands, responses, and events | Long-lived | A process needs bidirectional control |\n\nCLI options still select the working directory, model, tools, resources, and session persistence independently of the mode. See [Command Line](cli.md) for the complete startup options.\n\n## Print to stdout\n\nPrint mode runs the supplied prompts, writes the final assistant text to stdout, and exits:\n\n```bash\npi --print \"Summarize the changes in this repository\"\n```\n\nUse print mode when only the final text is needed, including command substitution, pipelines, and one-shot jobs. Intermediate events are not exposed.\n\nPrint mode writes errors to stderr. A final assistant response with an `error` or `aborted` stop reason produces a nonzero exit status. A response cut off at the model's output token limit (`length` stop reason) still prints its partial text to stdout, writes a warning to stderr, and produces a nonzero exit status.\n\nWhen no mode is selected explicitly, non-TTY stdin or stdout also selects print mode. This allows piped input and output without adding `--print`.\n\n## Stream JSON events\n\nJSON mode writes a session header followed by agent and session events as newline-delimited JSON:\n\n```bash\npi --mode json \"Review this repository\" > events.jsonl\n```\n\nThis is structured event output, not a single JSON result or a constraint on the format of the model’s response.\n\nAll prompts are supplied when the process starts. The process streams events for that run and then exits; it does not accept later commands.\n\nA failed or aborted assistant response appears in the event stream but does not by itself produce a nonzero exit status. Inspect the events when success or failure matters. Pi still exits nonzero if the invocation throws an error.\n\nStreaming `message_update` records contain deltas rather than a growing message snapshot. Assemble live output from the delta events, then replace it with the authoritative message from `message_end`.\n\n`agent_end` can be followed by automatic recovery or queued work. `agent_settled` marks the end of automatic work for the current run.\n\nStdout is reserved for JSONL. Diagnostics and application logging are written to stderr. See [JSON Event Stream](json.md) for framing, event shapes, and reconstruction rules.\n\n## Control Pi with RPC\n\nRPC mode keeps Pi running while another process sends commands and receives responses and events:\n\n```bash\npi --mode rpc --no-session\n```\n\nCommands are JSON objects written to stdin. Responses and events are JSON objects written to stdout. Every record occupies one line.\n\nAdd an `id` to commands that need correlation. The matching response repeats that ID. Events generally have no command ID because they describe session activity rather than one request.\n\nA successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean the run completed. Continue consuming events through `agent_settled` when completion matters.\n\nRPC commands can change models, inspect state, manage sessions, run shell commands, and answer extension UI requests.\n\nExtension dialogs form a request-response subprotocol. Other extension UI updates are notifications that a client may display or ignore. TUI-only extension capabilities are unavailable or degraded outside interactive mode.\n\nFor Node.js or TypeScript integrations, prefer `RpcClient` from `@earendil-works/pi-coding-agent`. It starts a Pi RPC child process, correlates requests, exposes typed command methods, and delivers session events to listeners.\n\nThe [RPC client example](../examples/rpc-client.ts) sends one prompt, streams text and tool activity, waits for `agent_settled`, and shuts down the child process. It is included in the repository’s TypeScript checks.\n\n`RpcClient.promptAndWait()` installs its event listener before sending the prompt, avoiding a race with fast completions. For separate operations, subscribe before calling `prompt()` and call `waitForIdle()` only while a run is active.\n\nThe client requires a path to a runnable Pi CLI. The repository example points at `dist/cli.js`, so the package must be built before that example runs from a checkout.\n\nIf you are building a client without `RpcClient`, start with [RPC Protocol](rpc.md), then use [RPC Commands](rpc-commands.md) and [JSON Event Stream](json.md) as the wire references.\n\n## Fork and rebrand Pi\n\nA source fork can change the CLI name and configuration directory through `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"my-agent\",\n    \"configDir\": \".my-agent\"\n  }\n}\n```\n\nChange the top-level `bin` field to set the executable name. These settings affect the CLI banner, configuration paths, and derived environment variable names.\n\n## Examples and references\n\n- [RPC client](../examples/rpc-client.ts): typed Node.js integration\n- [RPC extension UI](../examples/rpc-extension-ui.ts): custom terminal client with extension dialogs\n- [Command Line](cli.md): startup options and mode selection\n- [JSON Event Stream](json.md): JSON event reference\n- [RPC Protocol](rpc.md): RPC lifecycle, framing, errors, and shutdown\n- [RPC Commands](rpc-commands.md): command and response reference\n- [RPC Extension UI](rpc-extension-ui.md): extension interaction subprotocol\n- [SDK examples](../examples/sdk/): in-process TypeScript integrations\n\n<!-- File: packages/coding-agent/docs/cli.md (Tokens: ~4339 | Category: anthropic-skill) -->\n<a id=\"cli-and-modes-reference\"></a>\n\n# Command Line\n\nThis page documents Pi's built-in command-line commands and options. Run `pi --help` or append `--help` to a command for the exact interface in your installed version. The top-level help also includes options registered by loaded extensions.\n\n```sh\npi [options] [--] [@files...] [messages...]\npi install <source> [options]\npi remove <source> [options]\npi uninstall <source> [options]\npi update [target] [options]\npi list\npi config [options]\npi auth <check|print-api-key|print-bearer-token> [options]\npi mcp <list|login|logout> [options]\n```\n\n<a id=\"modes\"></a>\n\n## Invocation and output\n\n```sh\npi\npi --print \"Summarize this repository\"\ngit diff | pi --print \"Review this change\"\npi --mode json \"Inspect this repository\" > events.jsonl\n```\n\nWith terminal stdin and stdout, Pi opens the terminal UI unless `--print`, `--mode json`, or `--mode rpc` selects another interface. When either stream is redirected and neither JSON nor RPC mode is selected, Pi uses print mode. See [CLI Integration](cli-integration.md) for choosing between interactive, print, JSON, RPC, and SDK integration.\n\n| Input | Behavior |\n|---|---|\n| `message` | Provide an initial prompt |\n| `@path` | Include a text file or image in the first prompt |\n| Piped stdin | Prepend its contents to the first prompt |\n| `--` | Stop option parsing so a prompt can begin with `-` |\n\nPi resolves `@path` from the current working directory. The working directory also controls project configuration, resource discovery, and session grouping.\n\n`--print` controls whether Pi runs once and exits. `--mode` selects the output interface. `--mode text` does not force one-shot execution when stdin and stdout are terminals; use `--print` for that behavior.\n\n| Option | Behavior |\n|---|---|\n| `-p`, `--print` | Run the supplied prompts, write the final assistant text to stdout, then exit |\n| `--mode text` | Select text output; still open the terminal UI when stdin and stdout are terminals |\n| `--mode json` | Run the supplied prompts, write JSONL events to stdout, then exit |\n| `--mode rpc` | Read JSONL commands from stdin and write responses and events to stdout until shutdown |\n| `--export <input> [output]` | Export a session file to HTML and exit; derive the destination when `output` is omitted |\n\nRPC mode rejects `@file` arguments. JSON and RPC modes reserve stdout for protocol records. See [JSON Event Stream](json.md) and [RPC Protocol](rpc.md).\n\n<a id=\"model-options\"></a>\n\n## Models\n\n```sh\npi --model sonnet:high\n```\n\nSee [Choose a Model](models.md) for model selection and [Providers](providers.md) for credentials.\n\n- `--provider <name>`<br>\n  Restricts `--model` lookup to one provider. It requires `--model`.\n- `--model <pattern>`<br>\n  Selects by exact ID or fuzzy ID/name match. It accepts `provider/id` and an optional `:<thinking>` suffix. If several providers match, Pi prefers the one with credentials, otherwise it asks for `--provider`.\n- `--api-key <key>`<br>\n  Uses a non-persistent API-key override. It requires a model selected through `--model` or `--models`. It does not choose between providers that offer the same model.\n- `--thinking <level>`<br>\n  Sets `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. It overrides a `--model` suffix and is clamped to the model's capabilities.\n- `--models <patterns>`<br>\n  Sets a comma-separated scope for startup and cycling. It accepts exact IDs, fuzzy matches, case-insensitive globs, and optional `:<thinking>` suffixes. A model offered by several providers is added for each, with a warning.\n- `--list-models [search]`<br>\n  Lists available models, optionally filtered by a fuzzy search, then exits.\n\n<a id=\"session-options\"></a>\n\n## Sessions\n\n```sh\npi --continue\n```\n\nSee [Sessions and Context](sessions.md) for resuming, forking, naming, and storing sessions.\n\n- `-c`, `--continue`<br>\n  Continues the most recent session for the current project.\n- `-r`, `--resume`<br>\n  Opens the session selector.\n- `--session <path|id>`<br>\n  Opens by file path, exact ID, or partial ID. Pi searches the current project first and offers to fork a cross-project match.\n- `--session-id <id>`<br>\n  Opens the exact project session ID or creates it if absent. IDs accept letters, numbers, `.`, `_`, and `-`.\n- `--fork <path|id>`<br>\n  Forks an existing session into a new session for the current project.\n- `--session-dir <dir>`<br>\n  Overrides storage and lookup. It takes precedence over `PI_CODING_AGENT_SESSION_DIR` and the `sessionDir` setting.\n- `--no-session`<br>\n  Uses an in-memory session that is not persisted.\n- `-n`, `--name <name>`<br>\n  Sets the session display name.\n\nConstraints:\n\n- Session IDs must start and end with a letter or number.\n- `--fork` cannot be combined with `--session`, `--continue`, `--resume`, or `--no-session`.\n- `--session-id` cannot be combined with `--session`, `--continue`, or `--resume`. Combine it with `--fork` to choose the new ID.\n\n<a id=\"tool-options\"></a>\n\n## Tools\n\n```sh\npi --tools read,grep,find,ls --print \"Review this project\"\n```\n\nSee [Settings](settings.md#tools) for configuring the default tool selection.\n\n- `-t`, `--tools <list>`<br>\n  Replaces the default selection with a comma-separated allowlist of built-in, extension, or custom tools. Entries are tool names or patterns where `*` matches any characters. MCP tools are kept unless an entry starts with `mcp__` (see [MCP tools](#mcp-tools)). A list of only `+name` and `-name` entries is not an allowlist; it changes the default selection instead.\n- `-xt`, `--exclude-tools <list>`<br>\n  Disables comma-separated tool names or patterns after all other selection options, MCP tools included.\n- `-nbt`, `--no-builtin-tools`<br>\n  Disables default built-in tools while retaining extension and custom tools.\n- `-nt`, `--no-tools`<br>\n  Starts with all built-in, extension, custom, and MCP tools disabled.\n\nDefault enabled tools are `read`, `bash`, `edit`, and `write`, unless `defaultTools` changes them. `--tools` with plain names replaces the whole selection, so name every tool you want. Like `defaultTools`, it also accepts a list of only `+name` and `-name` entries, which adds tools to or removes them from the default selection: `pi --tools +codemode,-write` keeps the other default tools, enables `codemode`, and disables `write`. These entries take exact tool names, not `*` patterns; use `--exclude-tools` to disable tools by pattern. Plain names and `+name`/`-name` entries cannot be mixed. `/reload` enables tools newly added to `defaultTools`, but a tool removed with `-name` stays removed.\n\n<a id=\"mcp-tools\"></a>\n\n`--tools` selects the tools declared to the model. It does not remove MCP tools, whose reach is set by their [exposure](mcp.md#control-tool-exposure): `pi --tools read,codemode` keeps every MCP tool callable from codemode scripts. An MCP tool that no entry names or matches is never declared directly, whatever its exposure; only `tool_search`, if listed, can load it. Once an entry starts with `mcp__`, `--tools` filters MCP tools too, so this keeps only the tools of the `radius` server:\n\n```sh\npi --tools read,bash,codemode,'mcp__radius__*'\n```\n\nThe MCP resource tools (`list_mcp_resources`, `list_mcp_resource_templates`, `read_mcp_resource`) count as MCP tools. To remove MCP tools, use `--exclude-tools 'mcp__*'` or [`--no-mcp`](#resource-options).\n\n| Built-in | Purpose |\n|---|---|\n| `read` | Read text files and supported images |\n| `bash` | Run shell commands |\n| `powershell` | Run PowerShell commands on Windows |\n| `edit` | Apply exact text replacements to an existing file |\n| `write` | Create or overwrite a file |\n| `grep` | Search file contents |\n| `find` | Find paths using glob patterns |\n| `ls` | List directory contents |\n\nBuilt-in extensions add two more tools. They are off by default; the MCP extension turns them on when an MCP server needs them (see [MCP](mcp.md#exposure)). To enable them yourself, name them in `--tools` or `defaultTools`.\n\n| Built-in extension | Purpose |\n|---|---|\n| `codemode` | Run JavaScript that calls the other tools, for example in parallel with `Promise.allSettled`; only the script's output reaches the model |\n| `tool_search` | Search tools that are not declared to the model (`codemode` and `deferred` exposure, such as MCP tools) and declare the matches for the next call |\n\n### Enable codemode\n\nTo turn on `codemode` for every session, add it to the default tools in `~/.pi/agent/settings.json` or a project's `.pi/settings.json`:\n\n```json\n{\n  \"defaultTools\": [\"+codemode\"]\n}\n```\n\nThis keeps `read`, `bash`, `edit`, and `write` and adds `codemode`. For one invocation, add it with `--tools`:\n\n```sh\npi --tools +codemode\n```\n\nCodemode is useful without MCP: scripts can run several tool calls in parallel, filter large output before it reaches the model, call classifier models such as TypeSafe's Jev through `models.classify()` (see [Classifier models](models.md#use-classifier-models)), and generate images through `models.generateImages()` (see [Image models](models.md#use-image-models)).\n\n### How codemode works\n\nScripts run in a QuickJS sandbox and reach the other tools through `tools.<name>(args)`. [Codemode](codemode.md) describes the script API, how tools are listed and found, the `store()` and `models` globals, and the limits.\n\n### Tool search\n\n`tool_search` is off by default; enable it with `\"defaultTools\": [\"+tool_search\"]` or `--tools`. It uses the same ranking as `searchTools()` over tools that are not declared yet and declares the matches for the next model call. Loaded tools are recorded in the session like other tool changes, so they stay declared on that branch.\n\n<a id=\"resource-options\"></a>\n\n## Resources\n\n```sh\npi --extension ./review.ts\n```\n\nSee [Configuration](configuration.md) for conventional directories and project trust, [Settings](settings.md#resources) for configured paths, and [Pi Packages](packages.md) for package sources.\n\n- `-e`, `--extension <path>`<br>\n  Loads an extension file or directory, or a built-in extension such as `builtin:mcp`, and is repeatable.\n- `-ne`, `--no-extensions`<br>\n  Disables discovered, configured, and built-in extensions. Explicit `-e` paths still load, so `pi -ne -e builtin:mcp` keeps only the built-in MCP support.\n- `--no-mcp`<br>\n  Disables the built-in MCP support for this run: no servers connect, and there are no MCP tools or `/mcp`. It does not affect an extension that replaces the built-in MCP support.\n- `--skill <path>`<br>\n  Loads a skill file or directory and is repeatable.\n- `-ns`, `--no-skills`<br>\n  Disables discovered and configured skills. Explicit `--skill` paths still load.\n- `--prompt-template <path>`<br>\n  Loads a prompt-template file or directory and is repeatable.\n- `-np`, `--no-prompt-templates`<br>\n  Disables discovered and configured templates. Explicit `--prompt-template` paths still load.\n- `--theme <path>`<br>\n  Loads a theme file or directory and is repeatable.\n- `--use-theme <name[/name]>`<br>\n  Selects the initial interactive theme for this run.\n- `--no-themes`<br>\n  Disables discovered and configured themes. Explicit `--theme` paths still load.\n- `-nc`, `--no-context-files`<br>\n  Disables `AGENTS.md` and `CLAUDE.md` discovery.\n\nResource paths apply only to the current process. Relative paths resolve from the current working directory.\n\n<a id=\"prompt-and-display-options\"></a>\n\n## Prompts and process\n\n```sh\npi --append-system-prompt ./instructions.md\n```\n\nSee [Configuration](configuration.md) for saved configuration, [Security](security.md#understand-project-trust) for project trust, and [Environment Variables](environment-variables.md) for process controls.\n\n- `--system-prompt <text|path>`<br>\n  Replaces the default system prompt with text or the contents of an existing file.\n- `--append-system-prompt <text|path>`<br>\n  Appends text or an existing file to the system prompt and is repeatable.\n- `--tui-mode <mode>`<br>\n  Uses `fullscreen` (default) or `regular` terminal mode.\n- `--verbose`<br>\n  Shows verbose interactive startup information, overriding `quietStartup`.\n- `-a`, `--approve`<br>\n  Trusts project-local configuration and resources for this process.\n- `-na`, `--no-approve`<br>\n  Ignores trust-gated project-local configuration and resources for this process.\n- `--offline`<br>\n  Disables automatic network activity, including model catalog refreshes. Equivalent to `PI_OFFLINE=1`.\n- `-h`, `--help`<br>\n  Shows help, including flags registered by loaded extensions, then exits.\n- `-v`, `--version`<br>\n  Shows the Pi version, then exits.\n\nExtensions may register additional long-form options. Unknown short options are rejected.\n\n## Package commands\n\n```sh\npi install npm:@scope/package\n```\n\nSee [Pi Packages](packages.md) for source formats, filtering, installation, and project scope.\n\n### Common tasks\n\n| Task | Command |\n|---|---|\n| Install a package | `pi install <source>` |\n| List configured packages | `pi list` |\n| Remove a package and its settings entry | `pi remove <source>` |\n| Configure which package resources load | `pi config` |\n\nAdd `--local` or `-l` to `install`, `remove`, `uninstall`, or `config` to use project settings instead of global settings.\n\n### Update Pi or packages\n\nRunning `pi update` without a target updates Pi itself.\n\n| Task | Command |\n|---|---|\n| Update Pi | `pi update` |\n| Update all installed packages | `pi update --extensions` |\n| Update one installed package | `pi update <source>` |\n| Refresh model catalogs | `pi update --models` |\n| Update Pi and all installed packages | `pi update --all` |\n\nAdd `--force` to reinstall Pi when the selected update includes Pi.\n\n`pi update` cannot update Pi when another package manager provides it, such as Nix. Update Pi with that package manager, for example `nix profile upgrade pi`. Package and model catalog updates still work.\n\n### Aliases and command options\n\n- `pi uninstall <source>` is an alias for `pi remove <source>`.\n- `pi update --self`, `pi update self`, and `pi update pi` are aliases for `pi update`.\n- `pi update --extension <source>` is an alias for `pi update <source>`.\n- `-a`, `--approve` trusts project-local files for one command. `-na`, `--no-approve` ignores trust-gated project-local files.\n- Append `-h` or `--help` to a command for its exact usage and option constraints.\n\n## Credential commands\n\n```sh\npi auth check --provider openai --json\n```\n\nAuthentication commands require `--provider <provider>` or `--model <model>`. See [Providers](providers.md) for supported methods.\n\n| Command | Description |\n|---|---|\n| `pi auth check` | Print `ready`, `not_ready`, or `invalid`; exit with status `0`, `1`, or `2`, respectively |\n| `pi auth print-api-key` | Print the resolved API key |\n| `pi auth print-bearer-token` | Print a resolved OAuth bearer token |\n\n| Option | Applies to | Description |\n|---|---|---|\n| `--provider <provider>` | All | Resolve credentials for a provider |\n| `--model <model>` | All | Resolve credentials from a model; may be combined with `--provider` |\n| `--json` | `auth check` | Write the structured result as JSON |\n| `--credentials` | `auth check` | Emit the resolved credential when ready |\n| `--no-refresh` | `auth check` | Do not refresh expired OAuth credentials; refresh is the default |\n| `--min-expiry <duration>` | `print-bearer-token` | Require remaining token lifetime using `ms`, `s`, `m`, or `h`, such as `30m` |\n\nCredential-printing commands write secrets to stdout.\n\n## MCP commands\n\nThese commands work outside a session, so agents can run them through `bash`. See [MCP Servers](mcp.md).\n\n| Command | Description |\n|---|---|\n| `pi mcp add <server> [options] -- <command> [args...]` | Add or replace a stdio server in `mcp.json`; `--env KEY=VALUE` (repeatable) and `--cwd <dir>` set its environment and working directory. Arguments after the command are passed to it |\n| `pi mcp add <server> [options] --url <url>` | Add or replace a streamable HTTP server; `--header KEY=VALUE` (repeatable), `--bearer-token-env-var <NAME>` (sends `Authorization: Bearer ${NAME}`), `--oauth-client-id`, `--oauth-client-secret`, `--oauth-callback-port`, and `--oauth-client-name` configure authentication |\n| `pi mcp remove <server>` | Remove a server from `mcp.json`; stored OAuth credentials are kept |\n| `pi mcp list [--json]` | Connect to every enabled server and print its state, tools, and errors; exit with `1` when a config entry is invalid or an enabled server is not connected |\n| `pi mcp login <server> [--timeout <seconds>]` | Sign in to an OAuth server: open the authorization page and wait for the browser; a terminal also accepts the pasted redirect URL. `--timeout` limits the whole sign-in, including requests to the authorization server (default 300 seconds) |\n| `pi mcp logout <server>` | Delete the stored OAuth credentials of a server |\n\n`add` and `remove` change `~/.pi/agent/mcp.json`, or `.pi/mcp.json` in the current directory with `--local` (`-l`). `add` also takes `--exposure <mode>` (see [Exposure](mcp.md#exposure)) and `--description <text>` and does not connect; run `pi mcp list` to check the server.\n\nProject `.pi/mcp.json` files are only read for projects that are already trusted.\n\n<!-- File: packages/coding-agent/docs/codemode.md (Tokens: ~3492 | Category: anthropic-skill) -->\n# Codemode\n\nThe `codemode` tool lets the model write a JavaScript script that calls pi's other tools and runs non-LLM models, such as classifiers and image models. Only the script's output reaches the model, so a script can run calls in parallel and filter large results before the model sees them. To turn it on, see [Enable codemode](cli.md#enable-codemode).\n\n## Scripts\n\nThe tool input is raw JavaScript source, not JSON and not a markdown code fence. It runs as the body of an async function in a QuickJS sandbox, so top-level `await` and `return` work. The sandbox has no Node APIs, file system, network, or timers; scripts reach the outside world only through tools and `models`.\n\nA script may start with an options line. It must be the first line; an options line anywhere else fails the call before the script runs:\n\n```js\n// @options: {\"max_output_tokens\": 2000, \"timeout_ms\": 60000}\n```\n\n- `max_output_tokens` (default 10000) limits the output. Longer output keeps its start and end, and the full text is written to a temp file whose path is included in the result. A script fails when its output passes 16777216 characters of text and base64 image data or 100000 `text()`, `image()`, and `console` calls; write large data to a file with a tool instead.\n- `timeout_ms` is a hard deadline for the whole script, including any wait for a free worker. It is unset by default. Image generation can take minutes, so do not set a short deadline for scripts that generate images.\n\nThe result starts with `Script completed` or `Script failed`, the wall time, and the output. Text and image items appear in order, each on its own line. When the output has more than one text item (from `text()` or `return`), each starts with a `==> text N/M <==` line. `console` calls follow in one `<console_output>` block with one line per call. A failed script keeps its partial output, followed by `Script error:` and the error. Tool calls are real: calls made before a failure are not undone. Calls still running when the script ends are cancelled, and unawaited promises are discarded.\n\n## Globals\n\n| Global | Purpose |\n|---|---|\n| `tools.<name>(args)` | Call a tool. See [Call tools](#call-tools). |\n| `text(value)` | Add a text item to the output. Strings are added as is, other values as JSON. |\n| `image(value)` | Add an image to the output: a base64 `data:` URL, an `{ image_url }` object, or an image block `{ type: \"image\", data, mimeType }` such as those returned by MCP tools and `models.generateImages()`. Remote URLs are not supported. PNG, JPEG, GIF, and WebP are accepted. Each image is also saved to a temp file, and the result names the path before the image. |\n| `console.log(...)` | Add a line to the `<console_output>` block after the other output. Arguments are joined with spaces; `info`, `warn`, `error`, and `debug` do the same. |\n| `return value` | A top-level `return` adds the value like `text()`. |\n| `exit()` | End the script successfully. |\n| `store(key, value)` / `load(key)` | Keep small JSON values across `codemode` calls. See [Store values](#store-values). |\n| `ALL_TOOLS` | Every callable tool as `{ name, description }`, including tools the description does not list. |\n| `searchTools(query, { limit?, namespace? })` | Rank callable tools by relevance (BM25, default limit 8). Resolves to `{ name, description }[]`. |\n| `describeTool(name)` | Resolves to a tool's description and TypeScript declaration, or `undefined`. |\n| `describeNamespace(name)` | Resolves to `{ name, description?, instructions?, tools }` for a namespace such as an MCP server, or `undefined`. |\n| `models` | List and run non-LLM models. See [Models](#models). |\n\n## Call tools\n\nEvery tool the session can call is a method of `tools`, named by its identifier: characters that are not valid in a JavaScript identifier become `_`, so the MCP tool `mcp__dev-radius__search` is `tools.mcp__dev_radius__search`. Each method takes one object with the tool's arguments.\n\nWhat a call resolves to depends on the tool:\n\n- Tools with an output schema resolve to a structured value. `bash` resolves to `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`, also for non-zero exit codes. Its `output` is not limited to the 2000 lines or 50KB the model sees: it holds up to 1 MiB, and longer output keeps its first and last 512 KiB around an omission marker, with `truncated` set and the full output in `full_output_path`.\n- MCP tools resolve to their `CallToolResult`, including `isError` and `structuredContent`.\n- `read` resolves to the file's text, or for an image to an image block `{ type: \"image\", data, mimeType, note }` that `image()` shows. `data` is the base64 image the model would see and `note` the text that goes with it, such as resize hints.\n- Other tools, such as `edit` and `write`, resolve to their text output.\n\nA call that fails, is blocked, or gets invalid arguments rejects with an `Error` that carries the tool's error text. Use `Promise.allSettled()` to keep the results of the calls that succeed.\n\nThe `codemode` description lists tools with their TypeScript declarations, grouped by namespace (for example one MCP server). Tools with `deferred` exposure, which includes MCP tools with the default `codemode` exposure, are not listed, so the description stays the same while MCP servers connect. Listed declarations share a budget of 3000 estimated tokens (`codemode.inlineBudget` in [settings](settings.md#tools)). Scripts find the other tools with `searchTools()`, `describeTool()`, `describeNamespace()`, or by filtering `ALL_TOOLS`.\n\nWhile `codemode` is active, `codemode.mode` in [settings](settings.md#tools) decides how the other tools are presented. With `on` (default) declared tools stay declared, and their descriptions say how to call them from scripts. With `only` they are hidden from the model and listed in the `codemode` description instead, so the model calls them through scripts. Tool declarations in the `codemode` description, `describeTool()`, and `ALL_TOOLS` carry the tools' prompt guidelines, since the system prompt rules only cover declared tools.\n\n## Store values\n\n`store(key, value)` keeps a JSON value under a string key for later `codemode` calls; storing `undefined` deletes the key. `load(key)` returns the value, or `undefined`. Writes are kept only when the script succeeds: each successful script that stores values appends a `codemode-store` custom entry to the session, so resumed sessions keep the values and each branch sees only the values written on its path.\n\nThe store is for small state such as IDs, cursors, or summaries. One value may have at most 262144 characters of JSON and all values together at most 1048576. Do not store image data; show images with `image()`, which also saves them to a temp file.\n\n## Models\n\n`models` reaches the model catalog and runs non-LLM models with the session's credentials: classifiers, which answer typed questions about JSON state and, for some models, images, and image models, which generate images. Chat models are listed but cannot be run from scripts. Which classifier and image models exist is described in [Use classifier models](models.md#use-classifier-models) and [Use image models](models.md#use-image-models).\n\n```ts\ntype ModelType = \"chat\" | \"image\" | \"classifier\";\n\n/** A catalog entry. `provider` and `id` identify it; other fields depend on the type. */\ninterface ModelInfo {\n  type?: ModelType;\n  provider: string;\n  id: string;\n  name: string;\n  api: string;\n  input: (\"text\" | \"image\")[];\n  contextWindow?: number;\n  [key: string]: unknown;\n}\n\ndeclare const models: {\n  /** Every known model of a type, optionally for one provider. */\n  getModelsOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** Models of a type whose provider has working credentials. */\n  getAvailableOfType(type: ModelType, provider?: string): Promise<ModelInfo[]>;\n  /** One catalog entry, or undefined. */\n  getModelOfType(type: ModelType, provider: string, id: string): Promise<ModelInfo | undefined>;\n  /** Answer `context.questions` about `context.state`; answers are in `result.answers` by question ID. */\n  classify(model: ModelInfo, context: ClassifierContext): Promise<ClassifierResult>;\n  /** Generate images from `context.input` text and image blocks; show `result.output` blocks with image(). Can take minutes. */\n  generateImages(model: ModelInfo, context: ImagesContext): Promise<ImagesResult>;\n};\n```\n\n`classify()` and `generateImages()` use only the `provider` and `id` of `model`, so `{ provider, id }` works as well. They do not throw on provider errors: check `stopReason` and `errorMessage`. At most four such calls run at once per script; more calls wait for a free slot, so `Promise.all()` over many items is fine. Their usage is added to the `codemode` tool result and counts toward the session cost.\n\nModel IDs differ between providers, for example `typesafe/jev-latest` and `openrouter/typesafe/jev-1.13`. Use `models.getAvailableOfType(type)` to find the IDs that work with the current credentials.\n\n### Classify\n\n```ts\ninterface ClassifierContext {\n  /** The data to classify. */\n  state: Record<string, unknown>;\n  /** Images judged together with `state`. Only models whose `input` includes \"image\" accept them. */\n  images?: { type: \"image\"; data: string; mimeType: string }[];\n  /** Questions by ID. One call answers all of them. */\n  questions: Record<string, ClassifierQuestion>;\n}\n\ntype ClassifierQuestion =\n  /** Pick one label. `criteria` maps each label to what it means. */\n  | { type: \"choice\"; instructions: string; criteria: Record<string, string> }\n  /** Score on an ordered scale. `criteria` describes each level, lowest first. */\n  | { type: \"score\"; instructions: string; criteria: string[] }\n  /** Yes or no. */\n  | { type: \"bool\"; instructions: string; criteria: { true: string; false: string } };\n\ninterface ClassifierResult {\n  provider: string;\n  model: string;\n  /** Answers by question ID. */\n  answers: Record<string, ClassifierAnswer>;\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype ClassifierAnswer =\n  | { type: \"choice\"; choice: string; probabilities: Record<string, number>; confidence: number }\n  /** `score` is the expected level index, from 0 to `criteria.length - 1`. */\n  | { type: \"score\"; score: number; confidence: number }\n  /** Probability of `true`. */\n  | { type: \"bool\"; probability: number };\n\n/** Token counts and cost in USD, when the service reports them. */\ntype ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };\n```\n\nClassify several items by calling `classify()` once per item. This script sorts feedback messages, for example ones a tool returned earlier in the script:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst results = await Promise.all(\n  messages.map((message) =>\n    models.classify(jev, {\n      state: { message },\n      questions: {\n        sentiment: {\n          type: \"choice\",\n          instructions: \"How does the user feel about the product?\",\n          criteria: { positive: \"Satisfied or happy\", negative: \"Unhappy or frustrated\", neutral: \"Neither\" },\n        },\n        urgency: {\n          type: \"score\",\n          instructions: \"How urgently does this need a reply?\",\n          criteria: [\"no reply needed\", \"reply this week\", \"reply today\"],\n        },\n      },\n    }),\n  ),\n);\nreturn results.map((result, i) =>\n  result.stopReason === \"stop\"\n    ? { message: messages[i], sentiment: result.answers.sentiment.choice, urgency: result.answers.urgency.score }\n    : { message: messages[i], error: result.errorMessage },\n);\n```\n\nClassifiers whose `input` includes `\"image\"` also judge images. `tools.read()` returns an image file as an image block that `images` accepts. Other classifiers return an error result when `images` is not empty.\n\n```js\nconst luna = await models.getModelOfType(\"classifier\", \"openai\", \"gpt-6-luna\");\nconst photo = await tools.read({ path: \"screenshot.png\" });\nconst result = await models.classify(luna, {\n  state: { task: \"Settings page redesign\" },\n  images: [photo],\n  questions: {\n    broken: {\n      type: \"bool\",\n      instructions: \"Does the screenshot show a broken layout?\",\n      criteria: { true: \"Overlapping, cut-off, or misaligned elements\", false: \"Clean layout\" },\n    },\n  },\n});\n```\n\n### Generate images\n\n```ts\ninterface ImagesContext {\n  /** The prompt as text blocks, plus image blocks to edit or use as references. */\n  input: (TextBlock | ImageBlock)[];\n}\n\ninterface ImagesResult {\n  provider: string;\n  model: string;\n  /** Generated images, and text blocks for models that also return text. */\n  output: (TextBlock | ImageBlock)[];\n  usage?: ModelUsage;\n  stopReason: \"stop\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n}\n\ntype TextBlock = { type: \"text\"; text: string };\n/** `data` is base64. */\ntype ImageBlock = { type: \"image\"; data: string; mimeType: string };\n```\n\nShow generated images with `image(block)`. Do not print `data` with `text()`, `console`, or `return`: it is large and the model cannot read it as text. `image()` also saves each image to a temp file and puts its path in the result, so a later turn can copy or move the file.\n\n```js\n// @options: {\"timeout_ms\": 300000}\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) {\n  if (block.type === \"image\") image(block);\n  else text(block.text);\n}\n```\n\n## Limits\n\n- A script's VM has 256 MB of memory. Running out throws `InternalError: out of memory`; filter or aggregate large data instead of accumulating it.\n- On Bun, stopping a script or reaching its deadline ends the call promptly, but a built-in that does not poll QuickJS's interrupt handler can keep computing in its worker until it returns.\n- At most two codemode workers may be alive across all sessions in one pi process; `PI_CODEMODE_MAX_WORKERS` changes the limit. A cancelled worker occupies its slot until it exits. At capacity, new scripts wait for a free worker in arrival order. A script that gets no worker within 60 seconds, or before its `timeout_ms`, fails without running.\n- A script that waits on a promise that can never settle (no tool call pending) fails immediately, since there are no timers.\n- Scripts cannot start other `codemode` scripts.\n\n<!-- File: packages/coding-agent/docs/compaction.md (Tokens: ~5158 | Category: anthropic-skill) -->\n# Compaction Reference\n\nThis reference describes automatic compaction, branch summarization, persisted entries, and extension hooks. For the user workflow, see [Sessions and Context](sessions.md#manage-conversation-context).\n\n**Source files** ([pi](https://github.com/earendil-works/pi)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - Auto-compaction logic\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - Branch summarization\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - Shared utilities (file tracking, serialization)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts) - Entry types (`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) - Extension event types\n\nFor TypeScript definitions in your project, inspect `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Overview\n\nPi has two summarization mechanisms:\n\n| Mechanism | Trigger | Purpose |\n|-----------|---------|---------|\n| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |\n| Branch summarization | `/tree` navigation | Preserve context when switching branches |\n\nBoth use closely related structured formats and track file operations cumulatively. Summarization requests disable prompt-cache writes because these one-off prompts are unlikely to be reused.\n\n## Compaction\n\n### When It Triggers\n\nAuto-compaction triggers when:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nBy default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`). It only decides when compaction starts. Summary requests use the model's output limit, reduced when needed so input and output fit the context window.\n\nDuring a multi-turn agent run, Pi checks the canonical projected context after tools finish and their results are appended, before starting the next assistant response. If the threshold is crossed, Pi compacts during `prepareNextTurn`, then performs the existing catch-up steering poll before `turn_start`. It skips this between-turn check when the completed tool batch terminates the run and no queued message requires another response. Pi also checks before a new user prompt and performs final-attempt overflow recovery after the low-level run ends.\n\nA provider context-overflow error or an early final `stopReason: \"length\"` can select one compact-and-retry recovery attempt. Length responses with tool calls retain their synthetic failed tool results and follow the ordinary tool/queue scheduler rather than forcing the run to end.\n\nYou can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary.\n\n### How It Works\n\n1. **Find cut point**: Walk backwards through the finalized session projection, accumulating token estimates until `keepRecentTokens` (default 20k, configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`) is reached\n2. **Extract messages**: Collect projected messages from the previous kept boundary (or session start) up to the cut point\n3. **Generate summary**: Call LLM to summarize with structured format, passing the previous summary as iterative context when present\n4. **Append entry**: Save `CompactionEntry` with summary and `firstKeptEntryId`\n5. **Rebuilds context**: Session rebuilds the context for the next request, using summary + messages from `firstKeptEntryId` onwards\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\nOn repeated compactions, the summarized span starts at the previous compaction's kept boundary (`firstKeptEntryId`), not at the compaction entry itself, falling back to the entry after the previous compaction if that kept entry cannot be found in the path. A retain-none compaction records its own ID as `firstKeptEntryId`; repeated compaction starts after that entry. This preserves messages that survived the earlier compaction by including them in the next summarization pass as well. Pi also recalculates `tokensBefore` from the rebuilt, context-edited session projection before writing the new `CompactionEntry`, so the token count reflects the actual pre-compaction context being replaced. Omitted raw entries remain stored but do not affect cut selection, summaries, checkpoints, or token estimates.\n\n### Overflow and Length Recovery Ordering\n\nRecovery preserves the existing lifecycle and queue order. The completed attempt remains visible to `turn_end` and `agent_end`; post-run recovery then repairs persisted model context before a fresh retry:\n\n```text\npersist final assistant response\n→ extension/public turn_end\n→ extension/public agent_end\n→ append context_edit omissions for the selected attempt\n→ for overflow/length: run session_before_compact and append compaction on success\n→ start the retry as a fresh run\n```\n\nIf recovery compaction fails or is cancelled, Pi keeps the omission edits, appends no compaction, and schedules no internal retry. Existing queued work remains governed by ordinary steering and follow-up rules. `agent_before_settle` sees the repaired projection after recovery processing. Raw transcript history, exports, billing totals, and history-search extensions can still inspect the omitted attempt.\n\n### Split user-message spans\n\nA user-message span starts with a user message and includes all turns until the next user message. Normally, compaction cuts at user-message boundaries.\n\nWhen one user-message span exceeds `keepRecentTokens`, the cut point lands within that span at an assistant message. This is a split user-message span:\n\n```\nSplit user-message span (one span exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no earlier user-message spans)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\nFor split user-message spans, Pi generates two summaries and merges them:\n1. **History summary**: Previous context (if any)\n2. **User-message-span prefix summary**: The early part of the split user-message span\n\n### Cut Point Rules\n\nValid cut points are:\n- User messages\n- Assistant messages\n- BashExecution messages\n- Custom messages (custom_message, branch_summary)\n\nNever cut at tool results (they must stay with their tool call).\n\nPreparation advances the kept boundary into a context-invisible suffix only when that suffix contains an omitted assistant attempt and no unomitted context-producing entries. Recovery `context_edit` omissions satisfy this rule; intrinsically context-invisible metadata may coexist with them. Metadata alone and newly appended custom messages do not move the cut. A replacement edit affecting the candidate input or summarized prefix also blocks advancement because the omitted assistant answered the pre-edit input; replacements of suffix entries that are ultimately omitted remain safe. This allows an over-budget recovered input to be summarized while retaining the edits that keep the abandoned attempt omitted, without making bookkeeping change whether new model input is preserved verbatim.\n\n### CompactionEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions can store any JSON-serializable data in `details`. The default compaction tracks file operations, but custom extension implementations can use their own structure. Generated and extension-provided summaries store their LLM `usage` when available so session totals include summarization work.\n\nSee [`prepareCompaction()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) and [`compact()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) for the implementation. For direct programmatic summarization, `generateSummary()` returns the summary text and `generateSummaryWithUsage()` returns `{ text, usage }`.\n\n## Branch Summarization\n\n### When It Triggers\n\nWhen you use `/tree` to navigate to a different branch, Pi offers to summarize the work you're leaving. This injects context from the left branch into the new branch.\n\n### How It Works\n\n1. **Find common ancestor**: Deepest node shared by old and new positions\n2. **Collect entries**: Walk from old leaf back to common ancestor\n3. **Prepare with budget**: Include messages up to token budget (newest first)\n4. **Generate summary**: Call LLM with structured format\n5. **Append entry**: Save `BranchSummaryEntry` at navigation point\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### Cumulative File Tracking\n\nDefault compaction and branch summarization track files cumulatively. Both extract file operations from tool calls in the messages being summarized. Compaction also carries file lists from the previous Pi-generated compaction. Branch summarization carries file lists from Pi-generated branch summaries in the entries it summarizes.\n\nFile tracking therefore accumulates across default compactions and nested default branch summaries. Pi does not automatically carry file lists from extension-generated summaries whose `fromHook` field is `true`; extensions manage their own `details` format.\n\n### BranchSummaryEntry Structure\n\nDefined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string | null;\n  timestamp: string;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nSame as compaction, extensions can store custom data in `details`.\n\nSee [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), and [`generateBranchSummary()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) for the implementation.\n\n## Summary Format\n\nBoth formats include Goal, Constraints & Preferences, Progress, Key Decisions, and Next Steps. Compaction summaries also include Critical Context. Branch summaries stop after Next Steps. Pi appends file lists to either format when relevant.\n\nCompaction summaries use this format:\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### Message Serialization\n\nBefore summarization, messages are serialized to text via [`serializeConversation()`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts):\n\n```\n[User]: What they said\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\nThis prevents the model from treating it as a conversation to continue.\n\nThinking blocks are omitted. Providers return summaries of the real reasoning, and replayed thinking is often not part of the model's live context, so including it could make the summarization request larger than the context it summarizes.\n\nTool results are truncated to 2000 characters during serialization. Content beyond that limit is replaced with a marker indicating how many characters were truncated. This keeps summarization requests within reasonable token budgets, since tool results (especially from `read` and `bash`) are typically the largest contributors to context size.\n\n## Custom Summarization via Extensions\n\nExtensions can intercept and customize both compaction and branch summarization. See [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for event type definitions.\n\n### session_before_compact\n\nFired before auto-compaction or `/compact`. Can cancel or provide custom summary. See `SessionBeforeCompactEvent` and `CompactionPreparation` in the types file.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - user-message-span prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - effective settings after applying model overrides\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### Converting Messages to Text\n\nTo generate a summary with your own model, convert messages to text using `serializeConversation`:\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\nSee [custom-compaction.ts](../examples/extensions/custom-compaction.ts) for a complete example using a different model.\n\n### session_compact_failed\n\nFired when manual or automatic compaction fails or is aborted. This is useful for telemetry extensions that need to pair `session_before_compact` attempts with terminal outcomes.\n\n```typescript\npi.on(\"session_compact_failed\", async (event, ctx) => {\n  const { reason, errorMessage, aborted, willRetry, fromExtension } = event;\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // errorMessage - present for non-abort failures\n  // aborted - true for canceled/aborted compactions\n  // willRetry - whether the aborted turn would have retried after compaction\n  // fromExtension - whether extension-provided compaction content was being used\n});\n```\n\n### session_before_tree\n\nFired before `/tree` navigation. Always fires regardless of whether user chose to summarize. Can cancel navigation or provide custom summary.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\nSee `SessionBeforeTreeEvent` and `TreePreparation` in the types file.\n\n## Settings\n\nConfigure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`:\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `enabled` | `true` | Enable auto-compaction |\n| `reserveTokens` | `16384` | Free context tokens below which auto-compaction starts |\n| `keepRecentTokens` | `20000` | Recent tokens to keep (not summarized) |\n\nDisable auto-compaction with `\"enabled\": false`. You can still compact manually with `/compact`.\n\n### Per-model overrides\n\nUse `compaction.modelOverrides` to tune token budgets for different models:\n\n```json\n{\n  \"compaction\": {\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000,\n    \"modelOverrides\": {\n      \"some-provider/big-model\": {\n        \"reserveTokens\": 400000\n      }\n    }\n  }\n}\n```\n\nFor a model with a 1M context window, this override triggers compaction above 600K tokens and keeps the ordinary 20000 recent tokens. Other models retain the ordinary 16384-token reserve.\n\nKeys are exact, case-sensitive `provider/modelId` values, including any slashes within the model ID. Each `reserveTokens` and `keepRecentTokens` value falls back independently from the model override to the ordinary setting to the built-in default. Values must be non-negative safe integers. Invalid values in the matching model override produce an error when read; only omitted fields fall back to the ordinary setting. Model override entries must be objects. Invalid ordinary token settings produce an error when read, even if the active model has a valid override. Only omitted ordinary values use built-in defaults. `enabled` remains global, not model-specific.\n\nThese resolved values are used for manual compaction, all automatic threshold checks, overflow recovery, and extension-visible `preparation.settings`. Model switches affect subsequent checks and compactions without changing ordinary settings. Compaction already in progress uses the model and settings captured for that operation. Branch summarization settings are unaffected.\n\nOverrides work in both global and project settings. The files merge recursively before lookup, so a global model-specific value beats a project-wide fallback; a project must override that model entry to change it. See [Settings](settings.md#per-model-compaction-overrides) for details.\n\n<!-- File: packages/coding-agent/docs/configuration.md (Tokens: ~754 | Category: anthropic-skill) -->\n# Configuration\n\nPi supports user-level and project configuration. User-level configuration lives in the agent directory, which defaults to `~/.pi/agent`. Project configuration lives in `.pi` under the working directory and loads after [project trust](security.md#understand-project-trust) is granted. The only exception is `sessionDir`, which Pi reads before resolving trust so it can locate sessions.\n\nIn interactive mode, use `/settings` to change common preferences. For other options, ask Pi to update the configuration or edit the relevant files directly. Run `/reload` after manually changing settings, keybindings, instructions, or resources.\n\n## Agent directory\n\nThe agent directory is shown as `<agent-dir>` below. Set its location with the `PI_CODING_AGENT_DIR` environment variable or the SDK's [`agentDir`](sdk.md) option.\n\n| Path | Responsibility |\n|---|---|\n| `<agent-dir>/settings.json` | User-level [settings](settings.md), including preferences, defaults, resource paths, and Pi package declarations. |\n| `<agent-dir>/keybindings.json` | Custom terminal UI and application [keybindings](keybindings.md). |\n| `<agent-dir>/mcp.json` | [MCP servers](mcp.md) available in every project. |\n| `<agent-dir>/models.json` | [Compatible endpoints, models, and model overrides](models.md#configure-a-compatible-endpoint). |\n| `<agent-dir>/auth.json` | Saved API keys and OAuth credentials. |\n| `<agent-dir>/AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, or `CLAUDE.MD` | User instructions applied across working directories. |\n| `<agent-dir>/SYSTEM.md` | Replaces Pi’s default system prompt. |\n| `<agent-dir>/APPEND_SYSTEM.md` | Adds instructions to Pi’s system prompt. |\n| `<agent-dir>/extensions/` | User [extensions](extensions.md). |\n| `<agent-dir>/skills/` | User [skills](skills.md) and supporting files. |\n| `<agent-dir>/prompts/` | User [prompt templates](prompt-templates.md) exposed as slash commands. |\n| `<agent-dir>/themes/` | User [theme](themes.md) files. |\n\n## Project `.pi` directory\n\n| Path | Responsibility |\n|---|---|\n| `.pi/settings.json` | Project-level [settings](settings.md), resource paths, and Pi package declarations. |\n| `.pi/mcp.json` | Project [MCP servers](mcp.md). |\n| `.pi/SYSTEM.md` | Replaces the system prompt for the project. |\n| `.pi/APPEND_SYSTEM.md` | Adds project-specific instructions to the system prompt. |\n| `.pi/extensions/` | Project extensions. |\n| `.pi/skills/` | Project skills and supporting files. |\n| `.pi/prompts/` | Project prompt templates exposed as slash commands. |\n| `.pi/themes/` | Project theme files. |\n\nFor `SYSTEM.md` and `APPEND_SYSTEM.md`, the trusted project file takes precedence over the corresponding agent-directory file. Files with the same name are not combined.\n\n## Context files\n\nContext files are separate from project `.pi` configuration. Pi loads them from the agent directory, the working directory, and its parent directories. A context file applies whenever Pi runs in its directory or anywhere below it.\n\nAn `AGENTS.override.md` replaces `AGENTS.md` or `CLAUDE.md` only in the same directory. It does not suppress context files from the agent directory or other directories.\n\nContext-file discovery does not require project trust.\n\n<!-- File: packages/coding-agent/docs/containerization.md (Tokens: ~1660 | Category: anthropic-skill) -->\n# Run Pi in an isolated environment\n\nUse an isolated environment to limit the files, credentials, processes, and network services that generated commands can access or affect.\n\nYou can isolate the complete Pi process or keep Pi on the host and route selected tools into an isolated environment.\n\n## Choose an isolation method\n\n| Method | Where Pi runs | What is isolated | Credential handling | Best for |\n|---|---|---|---|---|\n| Plain Docker | Container | Pi, built-in tools, `!` commands, and extensions | Credentials passed into the container | A straightforward local container boundary |\n| Docker Sandboxes | Managed sandbox | Pi, built-in tools, `!` commands, and extensions | Provider credentials remain on the host and are substituted by the proxy | Managed local isolation without exposing the real provider key |\n| OpenShell | Local or remote sandbox | Pi, built-in tools, `!` commands, and extensions | Policy-controlled credentials and inference routing | Filesystem, process, network, and credential policies |\n| Gondolin extension | Host | Built-in tools and `!` commands | Stored Pi credentials remain on the host, but commands inherit host environment variables | A local micro-VM for tool execution while retaining the host interface |\n\nThe method changes where extensions run. When the complete Pi process runs inside an isolated environment, its extensions run there too. When host Pi delegates built-in tools through Gondolin, other extension tools still run on the host unless they also delegate their work.\n\n## Decide what Pi can access\n\nAn isolated process can still affect resources you expose to it:\n\n- A read-write host mount lets Pi modify those host files.\n- Mounting `~/.pi/agent` exposes your Pi credentials, settings, extensions, and sessions.\n- Environment variables passed into a container are available to processes inside it.\n- Network access may allow code or tool output to leave the environment.\n- Tool-only isolation does not constrain the host Pi process or extension tools that do not use the isolated backend.\n\nExpose only the working folder, credentials, and network destinations needed for the task. Use read-only mounts or copy files into and out of the environment when you do not want writes to affect the host.\n\n## Run Pi in plain Docker\n\nPlain Docker provides the simplest whole-process container boundary.\n\n### Build the image\n\nCreate `Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\nBuild it from the directory containing the file:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n```\n\n### Start Pi\n\nFrom the working folder you want Pi to access, run:\n\n```bash\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\nReplace `ANTHROPIC_API_KEY` with the credential required by your provider. The named `pi-agent-home` volume keeps container-local settings, credentials, and sessions between runs.\n\nDo not mount the host's `~/.pi/agent` unless the container should have access to your host Pi configuration and credentials.\n\n### Verify the workspace\n\nInside Pi, run:\n\n```text\n!pwd\n```\n\nThe command should report `/workspace`. Changes under `/workspace` write through to the mounted host folder. Remove the bind mount or use a read-only mount when that is not acceptable.\n\n## Run Pi with Docker Sandboxes\n\n[Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) runs the complete Pi process inside a managed sandbox. Its proxy can keep the real provider credential on the host and substitute it when requests leave the sandbox.\n\nConfigure credentials before creating the sandbox. Do not run `/login` inside the sandbox because that writes a real credential into it.\n\n### Use a Claude Pro or Max token\n\nGenerate the token with `claude setup-token` on a machine with Claude Code. If an `anthropic` secret is already configured, remove it first so the proxy does not add an API-key header alongside the bearer token:\n\n```bash\nsbx secret rm anthropic\n\nsbx secret set-custom \\\n  --host api.anthropic.com \\\n  --env ANTHROPIC_OAUTH_TOKEN \\\n  --placeholder 'sk-ant-oat01-{rand}'\n```\n\n`sbx secret set-custom` reads the real token from standard input. The sandbox receives an OAuth-shaped placeholder, which the proxy replaces only for requests to the configured host.\n\nFor an Anthropic API key, use `sbx secret set anthropic` instead.\n\n### Start Pi\n\nRun this from the working folder you want mounted:\n\n```bash\nsbx run --kit \"docker.io/sbx/pi-kit:latest\" pi\n```\n\nFor an existing sandbox, run Pi non-interactively with:\n\n```bash\nsbx exec <sandbox-name> -- pi -p \"list the failing tests\"\n```\n\nSee the [Pi kit documentation](https://github.com/docker/sbx-kits-contrib/tree/main/pi) for other providers, troubleshooting, and image pinning.\n\n## Run Pi with OpenShell\n\n[NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) provides local or remote sandboxes with filesystem, process, network, credential, and inference policies.\n\n### Select a gateway\n\nEvery sandbox requires an active gateway:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\n### Create the sandbox\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nPi, its built-in tools, `!` commands, and extension tools run inside the OpenShell boundary.\n\n### Transfer files to a remote sandbox\n\nA remote gateway does not bind-mount your host working folder. Clone the repository inside the sandbox or transfer files explicitly:\n\n```bash\nopenshell sandbox upload pi-sandbox ./working-folder /workspace\nopenshell sandbox download pi-sandbox /workspace/working-folder ./working-folder-out\n```\n\nOpenShell inference routing can keep raw model credentials outside the sandbox. When configured, point Pi at the corresponding OpenAI-compatible or Anthropic-compatible endpoint exposed by the gateway.\n\n## Route tools through Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) is a local Linux micro-VM. Its example extension keeps the Pi process and file-based provider credentials on the host while routing the built-in tools and user `!` commands into the VM.\n\nCommands inside the VM inherit the host process environment. Provider keys supplied through environment variables can therefore be visible inside the VM. Do not use this pattern as a credential boundary unless you remove sensitive variables or change the extension's environment handling.\n\nGondolin requires Node.js 23.6 or newer and QEMU installed through your operating-system package manager.\n\n### Install the extension\n\nFrom a Pi source checkout:\n\n```bash\nmkdir -p ~/.pi/agent/extensions\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\n### Start Pi\n\nRun Pi from the working folder you want mounted:\n\n```bash\ncd /path/to/working-folder\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nThe extension mounts the host working folder at `/workspace` in the VM and overrides `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls`. File changes under `/workspace` write through to the host.\n\nOther extension tools still run on the host unless they explicitly delegate their operations. Review the [Gondolin example](../examples/extensions/gondolin/) before adding tools that could bypass the VM boundary.\n\n<!-- File: packages/coding-agent/docs/custom-provider.md (Tokens: ~2583 | Category: anthropic-skill) -->\n# Custom Providers\n\nA provider extension connects Pi to a model service that needs custom authentication, model discovery, request handling, or streaming. If the service already speaks a supported API, configure it in `models.json` instead.\n\nProvider extensions run inside Pi and can inspect credentials, prompts, tool definitions, model responses, and usage. Treat them as trusted code and avoid logging secrets or provider payloads.\n\n## Choose the smallest integration\n\n| Requirement | Use |\n|---|---|\n| Add models behind a supported API | [`models.json`](models.md#configure-a-compatible-endpoint) |\n| Change an existing provider endpoint or headers | `models.json` or a small provider extension |\n| Discover models dynamically | A provider with `refreshModels` |\n| Add a `/login` flow | A provider with native or legacy OAuth configuration |\n| Implement an unsupported wire protocol | A provider with `stream` or `streamSimple` |\n\nA provider extension is an [extension](extensions.md), so it follows the same loading, trust, reload, and error behavior.\n\n## Register a provider\n\nCall `pi.registerProvider()` from the extension factory. Pi waits for asynchronous factories before startup continues, so providers registered there are available to startup model selection and `pi --list-models`.\n\nThere are two registration forms:\n\n- Register a complete `Provider` from `@earendil-works/pi-ai` for native authentication, filtering, discovery, refresh, and streaming behavior.\n- Register a provider name with `ProviderConfig` for the legacy configuration form used by existing extensions.\n\nPrefer a complete provider for new integrations that own more than static endpoint and model metadata. Pi composes `models.json` overrides above a registered native provider.\n\nRegistering only `baseUrl` or `headers` for an existing provider preserves its built-in models. Supplying `models` in the legacy form replaces that provider's models across chat, image, and classifier operations. An omitted `type` means `\"chat\"`; image and classifier models require explicit discriminants and implementations keyed by their `api` values through the `images` and `classifiers` fields.\n\n### Redirect an existing provider\n\nRegister only the fields that should change. This keeps the provider's built-in catalog, authentication, API implementation, and compatibility settings:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://gateway.example.com/anthropic\",\n    headers: {\n      \"X-Gateway-Key\": \"$GATEWAY_API_KEY\",\n    },\n  });\n}\n```\n\nUse this form for a proxy or gateway that preserves the upstream protocol. Do not copy the built-in model list into the extension because doing so turns a small endpoint override into a catalog replacement that can drift.\n\n### Register a compatible endpoint\n\nWhen a service implements a supported protocol but has its own model catalog, register the endpoint and only the models the service actually exposes:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"acme\", {\n    name: \"Acme AI\",\n    baseUrl: \"https://api.acme.example/v1\",\n    apiKey: \"$ACME_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        type: \"chat\",\n        id: \"acme-chat\",\n        name: \"Acme Chat\",\n        reasoning: false,\n        input: [\"text\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 8192,\n      },\n    ],\n  });\n}\n```\n\nPrefer `models.json` for this case when no executable extension behavior is needed. Use an extension when registration depends on runtime discovery, custom authentication, or code shared with other extension features.\n\n### Register mixed-operation models\n\nA mixed-operation provider can register non-chat models and their implementations together:\n\n```typescript\npi.registerProvider(\"media-tools\", {\n  apiKey: \"$MEDIA_TOOLS_API_KEY\",\n  models: [\n    {\n      type: \"image\",\n      id: \"image-v1\",\n      name: \"Image V1\",\n      api: \"media-images\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      output: [\"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n    },\n    {\n      type: \"classifier\",\n      id: \"classifier-v1\",\n      name: \"Classifier V1\",\n      api: \"media-classifier\",\n      baseUrl: \"https://media.example.com/v1\",\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 64000,\n    },\n  ],\n  images: {\n    \"media-images\": { generateImages: async (model, context, options) => result },\n  },\n  classifiers: {\n    \"media-classifier\": { classify: async (model, context, options) => result },\n  },\n});\n```\n\nModel-level `baseUrl` values take precedence over the provider endpoint. If no `models` list is supplied, built-in models of every operation remain registered. Equal model IDs in different operations remain distinct, including their model-specific headers.\n\nCalls made after initial extension loading take effect immediately. Use `pi.unregisterProvider()` to remove the dynamic provider and restore built-in behavior that it replaced.\n\nSee the checked [GitLab Duo provider](../examples/extensions/custom-provider-gitlab-duo/) for a complete registration that delegates streaming to built-in API implementations.\n\n## Provide authentication\n\nStatic providers can resolve an API key from a literal, environment interpolation, or a command. These values use the same syntax as `models.json`:\n\n- `$NAME` and `${NAME}` read environment variables.\n- A leading `!command` uses command output.\n- `$$` emits a literal `$`.\n- `$!` emits a literal leading `!`.\n\nUse native provider authentication when the integration needs stored credentials, custom resolution, provider-scoped environment, or multiple login methods.\n\nAn OAuth provider supplies a display name, login flow, token refresh, and access-token resolution. After registration it appears in `/login`, and Pi stores returned credentials in `~/.pi/agent/auth.json`.\n\nOAuth callbacks are UI-neutral. They can open an authorization URL, show a device code, report progress, request input, or ask the user to choose a login method. Honor cancellation and the supplied abort signal during network requests.\n\nPress Enter (`tui.select.confirm`) to open the device-login URL and copy the code, or log in manually. Providers can disable either action with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false` in `onDeviceCode` or native `AuthEvent` notifications.\n\nNever write access tokens, refresh tokens, authorization headers, or complete provider responses to ordinary logs.\n\n## Supply and refresh models\n\nEvery model needs an ID, display name, input capabilities, and cost metadata. Chat and classifier models also need a context window; chat models need an output limit and reasoning support; image models declare their output modalities. Choose the API implementation at the provider level unless one model requires an override.\n\nSet `promptCache.short` or `promptCache.long` to the provider's best-effort cache lifetime in seconds when Pi should keep an idle prompt cache warm. Leave them unset to disable cache warming for that retention tier.\n\nCompatibility flags describe verified differences in an otherwise supported API. Do not enable them based only on an endpoint claiming compatibility.\n\nConfirm the request fields and response behavior against the actual server.\n\nUse `refreshModels` when the available catalog comes from a live service. Pass `context.signal` to blocking I/O so callers can cancel refreshes.\n\nThe two registration forms have different refresh contracts:\n\n- A complete `Provider` returns nothing. It calls `context.publish({ update })` to install provider-owned model state, after which its synchronous `getModels()` exposes the latest list.\n- Legacy `ProviderConfig.refreshModels` returns mixed-operation model definitions. Pi replaces that registration’s live models with the returned list and applies any requested persistence.\n\nPublish persisted catalog data only when it should survive across runs. A live service such as llama.cpp can update its in-memory list without persisting it; a remote catalog can retain a snapshot for offline startup.\n\n## Reuse a supported streaming API\n\nUse one of Pi AI’s API implementations whenever the provider protocol matches it.\n\nSupported implementations cover Anthropic Messages, OpenAI Chat Completions and Responses, Google Generative AI and Vertex, Azure OpenAI Responses, Mistral Conversations, and Bedrock Converse.\n\nThe provider can still customize authentication, base URLs, headers, model filtering, and discovery while delegating request conversion and streaming to an existing API implementation.\n\nThis is safer than copying a stream implementation because it preserves Pi’s message conversion, tool handling, usage accounting, cancellation, and compatibility behavior.\n\n## Implement custom streaming\n\nImplement `streamSimple` only when no existing API implementation can represent the service. Study the implementations under [`packages/ai/src/api`](https://github.com/earendil-works/pi/tree/main/packages/ai/src/api) first.\n\nThe stream receives a normalized `TranscriptContext`. System prompts and tool declarations live in transcript system messages, so read them with `getCurrentSystemPrompt(context.messages)` and `getCurrentTools(context.messages)` rather than expecting `context.systemPrompt` or `context.tools`. A model that supports mid-conversation system messages can receive them in place; otherwise call `collapseSystemMessages(context)` to fold later system messages into the leading one.\n\nA custom stream must:\n\n1. Create an assistant message with provider, model, timestamp, pending stop reason, content, and zeroed usage.\n2. After request setup succeeds, emit one `start` event before content events.\n3. Update the message while emitting balanced text, thinking, and tool-call events.\n4. Finalize usage, cost, content, and stop reason.\n5. Emit exactly one terminal `done` or `error` event and close the stream.\n6. Convert cancellation into an aborted result.\n\nRequest setup can fail before `start`; in that case the stream can terminate directly with `error`. Missing request authentication may also throw synchronously before a stream is returned.\n\nContent indexes refer to blocks in the assistant message. Update each block before emitting the event whose `partial` field exposes that state. Tool-call arguments must contain valid parsed input by `toolcall_end`.\n\nThe stream must also honor request instrumentation supplied through `SimpleStreamOptions`:\n\n- Call `options.onPayload` before sending the provider request and use any replacement payload it returns.\n- Call `options.onResponse` after receiving the response but before consuming its body.\n- Await `options.onProviderStreamEvent?.(providerEvent, model)` for each parsed provider event before normalizing it.\n- Pass through the abort signal and provider-scoped environment.\n\nThese hooks power extension request inspection, response-header events, and provider-stream observation. Omitting them makes the provider behave differently from Pi’s built-in providers.\n\n## Report failures and usage\n\nSet a concrete terminal stop reason. Error and aborted messages need an `errorMessage`; successful messages need accurate input, output, cache, total-token, and cost values.\n\nPi can compact and retry after recognized context-overflow errors. If the service uses an unknown message, normalize only that provider’s overflow response to `context_length_exceeded` in a guarded `message_end` handler.\n\nDo not rewrite rate limits or transient provider failures as context overflow. Those failures use Pi’s normal retry behavior instead.\n\n## Test the integration\n\nTest at least:\n\n- ordinary and empty text responses\n- tool calls and tool results\n- image input and image tool results when supported\n- usage and cost accounting\n- abort behavior\n- context overflow\n- malformed or partial streams\n- Unicode boundaries\n- cross-provider session handoff\n- authentication refresh and cancellation\n\nThe provider tests under [`packages/ai/test`](https://github.com/earendil-works/pi/tree/main/packages/ai/test) define the behavior expected from built-in providers. Adapt the relevant suites rather than relying only on manual prompts.\n\nRun the extension directly while developing, then move it to a discovered extension location or distribute it through a [Pi package](packages.md). Use `/reload` after changing a discovered provider extension in an active session.\n\n<!-- File: packages/coding-agent/docs/docs.json (Tokens: ~1144 | Category: anthropic-skill) -->\n{\n  \"navigation\": [\n    {\n      \"title\": \"Get Started\",\n      \"items\": [\n        {\n          \"title\": \"Overview\",\n          \"path\": \"index.md\"\n        },\n        {\n          \"title\": \"Quickstart\",\n          \"path\": \"quickstart.md\"\n        },\n        {\n          \"title\": \"How Pi Works\",\n          \"path\": \"how-pi-works.md\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Guides\",\n      \"items\": [\n        {\n          \"title\": \"Run Pi\",\n          \"items\": [\n            {\n              \"title\": \"Use Pi in the Terminal\",\n              \"path\": \"usage.md\"\n            },\n            {\n              \"title\": \"Choose a Model\",\n              \"path\": \"models.md\"\n            },\n            {\n              \"title\": \"Manage Sessions and Context\",\n              \"path\": \"sessions.md\"\n            },\n            {\n              \"title\": \"Run Pi Safely\",\n              \"path\": \"security.md\"\n            },\n            {\n              \"title\": \"Isolate Pi\",\n              \"path\": \"containerization.md\"\n            },\n            {\n              \"title\": \"Run Local Models\",\n              \"path\": \"llama-cpp.md\"\n            },\n            {\n              \"title\": \"Run Local Models on NVIDIA RTX\",\n              \"path\": \"nvidia-rtx-llama-cpp.md\"\n            },\n            {\n              \"title\": \"Configure Your Terminal\",\n              \"path\": \"terminal-setup.md\"\n            },\n            {\n              \"title\": \"Configure Shell Commands\",\n              \"path\": \"shell-aliases.md\"\n            },\n            {\n              \"title\": \"Run Pi in tmux\",\n              \"path\": \"tmux.md\"\n            },\n            {\n              \"title\": \"Run Pi on Windows\",\n              \"path\": \"windows.md\"\n            },\n            {\n              \"title\": \"Run Pi on Android\",\n              \"path\": \"termux.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Customize Pi\",\n          \"items\": [\n            {\n              \"title\": \"Configure Pi\",\n              \"path\": \"configuration.md\"\n            },\n            {\n              \"title\": \"Create Prompt Templates\",\n              \"path\": \"prompt-templates.md\"\n            },\n            {\n              \"title\": \"Add Skills\",\n              \"path\": \"skills.md\"\n            },\n            {\n              \"title\": \"Create Themes\",\n              \"path\": \"themes.md\"\n            },\n            {\n              \"title\": \"Use Pi Packages\",\n              \"path\": \"packages.md\"\n            },\n            {\n              \"title\": \"Connect MCP Servers\",\n              \"path\": \"mcp.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Build on Pi\",\n          \"items\": [\n            {\n              \"title\": \"Build Extensions\",\n              \"path\": \"extensions.md\"\n            },\n            {\n              \"title\": \"Add Custom Providers\",\n              \"path\": \"custom-provider.md\"\n            },\n            {\n              \"title\": \"Route with Virtual Models\",\n              \"path\": \"virtual-models.md\"\n            },\n            {\n              \"title\": \"Build Terminal UI Components\",\n              \"path\": \"tui.md\"\n            },\n            {\n              \"title\": \"Integrate with the CLI\",\n              \"path\": \"cli-integration.md\"\n            },\n            {\n              \"title\": \"Use the SDK\",\n              \"path\": \"sdk.md\"\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"title\": \"Reference\",\n      \"items\": [\n        {\n          \"title\": \"CLI\",\n          \"path\": \"cli.md\"\n        },\n        {\n          \"title\": \"Codemode\",\n          \"path\": \"codemode.md\"\n        },\n        {\n          \"title\": \"Slash Commands\",\n          \"path\": \"slash-commands.md\"\n        },\n        {\n          \"title\": \"Settings\",\n          \"path\": \"settings.md\"\n        },\n        {\n          \"title\": \"Models Configuration\",\n          \"path\": \"models-configuration.md\"\n        },\n        {\n          \"title\": \"Environment Variables\",\n          \"path\": \"environment-variables.md\"\n        },\n        {\n          \"title\": \"Keybindings\",\n          \"path\": \"keybindings.md\"\n        },\n        {\n          \"title\": \"Providers\",\n          \"path\": \"providers.md\"\n        },\n        {\n          \"title\": \"Session File Format\",\n          \"path\": \"session-format.md\"\n        },\n        {\n          \"title\": \"Compaction and Branch Summaries\",\n          \"path\": \"compaction.md\"\n        },\n        {\n          \"title\": \"JSON Event Stream\",\n          \"path\": \"json.md\"\n        },\n        {\n          \"title\": \"RPC Protocol\",\n          \"path\": \"rpc.md\"\n        },\n        {\n          \"title\": \"RPC Commands\",\n          \"path\": \"rpc-commands.md\"\n        },\n        {\n          \"title\": \"RPC Extension UI\",\n          \"path\": \"rpc-extension-ui.md\"\n        },\n        {\n          \"title\": \"Message Types\",\n          \"path\": \"message-types.md\"\n        }\n      ]\n    }\n  ],\n  \"redirects\": [\n    {\n      \"from\": \"development.md\",\n      \"to\": \"index.md\"\n    },\n    {\n      \"from\": \"session.md\",\n      \"to\": \"session-format.md\"\n    },\n    {\n      \"from\": \"tree.md\",\n      \"to\": \"sessions.md\"\n    },\n    {\n      \"from\": \"process-integration.md\",\n      \"to\": \"cli-integration.md\"\n    }\n  ]\n}\n\n<!-- File: packages/coding-agent/docs/environment-variables.md (Tokens: ~1321 | Category: anthropic-skill) -->\n# Environment Variables\n\nPi uses environment variables in three ways:\n\n- Variables such as `PI_OFFLINE` configure the Pi process.\n- Pi sets process markers so child processes can identify Pi as the launching agent.\n- Commands run by the LLM-callable shell tools receive `PI_*` variables describing the current session.\n\nProvider API-key variables are documented separately in [Providers](providers.md#use-an-api-key-from-the-environment).\n\n## Process Marker\n\nThe CLI and RPC entry points set two process markers:\n\n- `AI_AGENT=pi` is a generic marker that lets tooling identify Pi as the agent that launched the process.\n- `PI_CODING_AGENT=true` is Pi-specific and lets child processes detect that they run inside Pi.\n\nChild processes inherit both markers. They are not session-specific and are not set automatically when Pi is embedded through the SDK.\n\n## Shell Tool Session Environment\n\nCommands run by the `bash` and `powershell` tools receive the current Pi session state:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_SESSION_ID` | Current session ID |\n| `PI_SESSION_FILE` | Absolute path to the current session JSONL file; unset for ephemeral sessions |\n| `PI_PROVIDER` | Currently selected model provider |\n| `PI_MODEL` | Currently selected model ID |\n| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |\n\nThe values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next shell command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.\n\nWhen asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt:\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\nThe session file can be inspected directly when the session is persistent:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nThese variables are injected into the LLM-callable `bash` and `powershell` tools. They are not injected into user-entered `!` or `!!` commands.\n\n### Custom Shell Tools\n\nTools created with `createBashTool()` or `createPowerShellTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nDisable session metadata independently of the spawn hook:\n\n```typescript\nconst powershellTool = createPowerShellTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nWhen disabled, Pi removes inherited values for these variables so nested Pi processes do not expose stale parent-session metadata.\n\n## Pi Process Configuration\n\nThese variables are read by Pi itself:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Override the config directory; default is `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Override session storage; overridden by `--session-dir` |\n| `PI_PACKAGE_DIR` | Override the package directory, useful for Nix/Guix store paths |\n| `PI_OFFLINE` | Disable automatic network activity, including model catalog refreshes |\n| `PI_SKIP_VERSION_CHECK` | Disable the `pi.dev` latest-version request |\n| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no` |\n| `PI_CODEMODE_MAX_WORKERS` | Maximum codemode workers alive at once across all sessions in the process; default `2`. Each worker's script may use up to 256 MB. Raise it when one process runs several sessions, such as in-process subagents. Further scripts wait for a free worker. See [Codemode](codemode.md) |\n| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |\n| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |\n| `PI_RADIUS_GATEWAY` | Override the Radius gateway origin used by `/bug` uploads |\n| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |\n| `PI_CLIPBOARD_OSC52` | Set to `1` to also send copied text to the terminal with OSC 52 after the local clipboard write, as Pi does over SSH. Use it when a remote client reaches Pi without SSH variables, such as a multiplexer attached from another machine or a remote IDE daemon |\n| `PI_HYPERLINKS` | Override OSC 8 hyperlink detection with `1`, `0`, or `auto` |\n| `PI_PROGRAM_STATUS` | Override OSC 7501 program status detection: `1` always reports, `0` never reports; otherwise Pi reports only after the terminal confirms support. See [Terminal setup](terminal-setup.md#program-status) |\n| `PI_IMAGE_PROTOCOL` | Override inline image detection with `kitty`, `iterm2`, `none`, or `auto` |\n| `PI_TRUE_COLOR` | Override truecolor detection with `1`, `0`, or `auto` |\n| `PI_TUI_ESC_TIMEOUT` | How long to wait after a lone ESC before treating it as Escape, in milliseconds; defaults to `100` over SSH and `10` otherwise. Increase if Alt-key input is misread as Escape |\n| `VISUAL`, `EDITOR` | External editor fallback when `externalEditor` is unset |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |\n\nProvider credentials such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and provider-specific configuration are listed in [Providers](providers.md#use-an-api-key-from-the-environment).\n\n<!-- File: packages/coding-agent/docs/extensions.md (Tokens: ~6204 | Category: anthropic-skill) -->\n# Extensions\n\nExtensions are TypeScript modules that add executable behavior to Pi. Use one when a workflow needs tools, commands, event handlers, model providers, session state, or terminal UI rather than instructions alone.\n\nAn extension runs inside the Pi process with the same operating-system permissions. It can inspect prompts, tool calls, files, credentials, and session history, so load extensions only from sources you trust.\n\nTypical extensions add an agent tool, protect paths, confirm dangerous commands, react to session events, modify context, expose a command, or display persistent status.\n\n<a id=\"quick-start\"></a>\n<a id=\"writing-an-extension\"></a>\n<a id=\"create-an-extension\"></a>\n\n## Create and load an extension\n\nAn extension exports a default factory that receives `ExtensionAPI`. The factory registers capabilities for the current extension runtime.\n\nCreate `~/.pi/agent/extensions/hello.ts`:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"hello\", {\n    description: \"Show a greeting\",\n    handler: async (name, ctx) => {\n      ctx.ui.notify(`Hello, ${name || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\nStart Pi and run `/hello`. During development, load a file directly:\n\n```bash\npi --extension ./hello.ts\n```\n\nPi uses `jiti`, so local TypeScript extensions do not need a separate compilation step. Use [Pi packages](packages.md) for distributed extensions and dependencies.\n\n<a id=\"extension-locations\"></a>\n<a id=\"available-imports\"></a>\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the extension in your user or project extensions directory. Pi loads direct TypeScript or JavaScript files and subdirectories containing an `index.ts` or `index.js` entry point.\n\nUse a single file for a small extension and a directory for a multi-file implementation. Put npm dependencies in a nearby `package.json`. See [Configuration](configuration.md) for conventional locations and [Settings](settings.md#resources) for additional paths.\n\nReload replaces the extension runtime, so code after `await ctx.reload()` must not reuse state from the old runtime. Only personal and explicit command-line extensions can participate in the `project_trust` event that runs before project extensions load.\n\n<a id=\"understand-the-lifecycle\"></a>\n\n## Understand the runtime lifecycle\n\nThe factory can be synchronous or asynchronous. Pi waits for an asynchronous factory before startup continues, allowing it to fetch configuration or register providers needed during startup.\n\nDo not start processes, sockets, watchers, or timers in the factory because some invocations load extensions without starting a session.\nStart long-lived resources from `session_start` or from the command or tool that needs them.\nClose session-scoped resources from an idempotent `session_shutdown` handler.\n\nA normal prompt starts a run. The run repeats turns, then checks whether more work starts another low-level agent run:\n\n```mermaid\nflowchart TD\n    Input[\"input\"] --> Before[\"before_agent_start\"]\n    Before --> AgentStart[\"agent_start\"]\n    AgentStart --> Turns[[\"turns\"]]\n    Turns --> AgentEnd[\"agent_end\"]\n    AgentEnd --> MoreRuns{\"More work?\"}\n    MoreRuns -->|yes| AgentStart\n    MoreRuns -->|no| BeforeSettle[\"agent_before_settle\"]\n    BeforeSettle --> Continue{\"Continuation requested?\"}\n    Continue -->|yes| AgentStart\n    Continue -->|no| Settled[\"agent_settled\"]\n```\n\nMore work means a retry, recovery, compaction retry, or queued work.\n\nEach turn sends one model request and executes the tool calls in the assistant response:\n\n```mermaid\nflowchart TD\n    TurnStart[\"turn_start\"] --> Request[[\"model request\"]]\n    Request --> ToolDecision{\"Tool calls?\"}\n    ToolDecision -->|yes| Tools[[\"tool execution\"]]\n    Tools --> TurnEnd[\"turn_end\"]\n    ToolDecision -->|no| TurnEnd\n    TurnEnd --> MoreTurns{\"Another turn?\"}\n    MoreTurns -->|yes| TurnStart\n    MoreTurns -->|no| AgentEnd([\"agent_end\"])\n```\n\nThe model request runs these hooks in order:\n\n1. `context`\n2. `context_with_system`\n3. `before_provider_headers`\n4. `before_provider_request`\n5. `after_provider_response`\n6. `provider_stream_event` and assistant message events\n\nTool execution runs these hooks in order for each tool call:\n\n1. `tool_execution_start`\n2. `tool_call`\n3. `tool_execution_update` while the tool executes\n4. `tool_result`\n5. `tool_execution_end`\n\nThe lifecycle involves these event groups:\n\n| Area | Events |\n|---|---|\n| Input preparation | `input`, `before_agent_start` |\n| Agent run | `agent_start`, `agent_end` |\n| Model turn | `turn_start`, `context`, `context_with_system`, `turn_end` |\n| Provider call | `before_provider_headers`, `before_provider_request`, `after_provider_response`, `provider_stream_event` |\n| Message lifecycle | `message_start`, `message_update`, `message_end` |\n| Tool execution | `tool_execution_start`, `tool_call`, `tool_execution_update`, `tool_result`, `tool_execution_end` |\n| Settlement | `agent_before_settle`, `agent_settled` |\n\nPi counts as busy while a run starts, including the compaction check and `before_agent_start`, so messages sent meanwhile go into the starting run. Message lifecycle events also cover user and tool-result messages; the model request list places only the assistant stream where it occurs in each turn.\n\nTool hooks run for each tool call, and one assistant response can start another turn by returning tool calls. Provider hooks run only when that transport exposes the corresponding stage. See the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event payloads and result types.\n\nSession startup and replacement follow a separate lifecycle:\n\n```mermaid\nflowchart TD\n    Startup[\"Process startup\"] --> Trust[\"project_trust when needed\"]\n    Trust --> SessionStart[\"session_start\"]\n    Switch[\"/new or /resume\"] --> BeforeSwitch[\"session_before_switch\"]\n    Fork[\"/fork or /clone\"] --> BeforeFork[\"session_before_fork\"]\n    BeforeSwitch --> Shutdown[\"session_shutdown\"]\n    BeforeFork --> Shutdown\n    Reload[\"reload\"] --> Shutdown\n    Shutdown --> Replacing{\"Replacing the runtime?\"}\n    Replacing -->|yes| Replacement[\"load replacement extension runtime\"]\n    Replacement --> SessionStart\n    Replacing -->|no| Stop[\"process exits\"]\n    SessionStart --> Discover[\"resources_discover\"]\n```\n\n`project_trust` runs before trusted project resources load and can run again when session replacement enters a new working directory. Retries, recovery, compaction, and queued messages can start another agent run after `agent_end`, so `agent_end` is not a final-idle signal. Reload and session replacement invalidate the old runtime. Code after `await ctx.reload()` still belongs to the old runtime and must not reuse its session-bound state.\n\n### Choose an event by intent\n\n| Goal | Event |\n|---|---|\n| Inspect, transform, or consume raw user input | `input` |\n| Change prompt sections, active tools, or add context before a run | `before_agent_start` |\n| Observe one low-level agent run | `agent_start`, `agent_end` |\n| Act after one model response and its tool calls | `turn_end` |\n| Filter conversation messages for one provider request | `context` |\n| Transform the complete transcript, including system messages | `context_with_system` |\n| Change request headers or the serialized provider payload | `before_provider_headers`, `before_provider_request` |\n| Observe provider response metadata or parsed stream events | `after_provider_response`, `provider_stream_event` |\n| Authorize or modify a tool call | `tool_call` |\n| Redact, annotate, or replace a tool result | `tool_result` |\n| Append final entries or request one last continuation | `agent_before_settle` |\n| Run notification work after automatic processing has stopped | `agent_settled` |\n| Initialize or release session-scoped resources | `session_start`, `session_shutdown` |\n\n<a id=\"agent_start--agent_end--agent_before_settle--agent_settled\"></a>\n\nUse `agent_before_settle` when an extension must append entries or request one final continuation. Use `agent_settled` for notification-only work that should run after Pi has no automatic continuation remaining.\n\n<a id=\"extensionapi-methods\"></a>\n\n## Choose an integration point\n\n| Capability | Main API |\n|---|---|\n| Observe or modify lifecycle behavior | `pi.on()` |\n| Add a model-callable operation | `pi.registerTool()` |\n| Add a `/` command | `pi.registerCommand()` |\n| Add a shortcut or CLI flag | `pi.registerShortcut()` or `pi.registerFlag()` |\n| Send user or custom messages | `pi.sendUserMessage()` or `pi.sendMessage()` |\n| Persist non-context session data | `pi.appendEntry()` |\n| Change active tools, model, or thinking level | Session control methods on `pi` |\n| Add a model provider | `pi.registerProvider()` |\n| Add an MCP server | `pi.registerMcpServer()` |\n| Route each request to a model | [`pi.registerVirtualModel()`](virtual-models.md) |\n| Add terminal rendering | Renderer registration and `ctx.ui` |\n| Communicate with another extension | `pi.events` |\n\nUse the exported declarations in [`extensions/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts) for exact event, context, tool, and result types.\n\n## Follow the extension contracts\n\n<a id=\"events\"></a>\n<a id=\"work-with-events\"></a>\n\n### Events and concurrency\n\nHandlers run in extension load and registration order. `pi.on()` returns a function that unsubscribes that registration; changes do not affect a dispatch already in progress.\nSome events notify; others transform data, replace results, or cancel an operation.\nUse each event’s declared result type rather than assuming every return value has an effect.\n\nEvents cover resource discovery, sessions, agent and message lifecycle, providers, tools, and raw input.\n\n#### Understand handler composition\n\n| Pattern | Behavior |\n|---|---|\n| Notification | Return values are ignored. Use the event for observation only. |\n| In-place mutation | Later handlers see changes to values such as `before_provider_headers.headers`, `before_agent_start.systemPromptOptions`, and `tool_call.input`. |\n| Chained transformation | Later handlers receive the value returned by earlier `input`, `context`, `context_with_system`, `message_end`, `tool_result`, and `before_provider_request` handlers. |\n| Gate | A handled input, blocked tool call, cancelled session operation, or handled `user_bash` command stops that operation's normal fallback path. |\n| Actionable boundary | Each `turn_end` or `agent_before_settle` handler sees the entries and continuation decision proposed so far. |\n| Last override | The last `cache_warming_decision` handler that returns an action wins. |\n\nNotification handlers should not perform slow work on a streaming event. Transform handlers should preserve fields they do not intentionally own, and gate handlers should fail closed when their safety decision cannot be made.\n\n`before_agent_start` exposes both the current prompt and its structured `systemPromptOptions`. `event.messageType` says what started the run: `\"user\"` for a prompt or `pi.sendUserMessage()`, with `prompt` and `images`; `\"custom\"` for `pi.sendMessage(..., { triggerTurn: true })`, with the starting `message` and no `prompt`. A returned `message` is added to either kind of run; check `messageType` to skip runs started by custom messages. Prefer changing prompt sections, selected tools, or guidelines so Pi can append a transcript delta. Returning `systemPrompt`, or setting `forceSystemPrompt`, replaces the whole prompt for that run while the transcript continues recording the structured sections. Providers receive the forced text as their leading system prompt.\n\n`message_end` can replace a finalized message while preserving its role. `tool_call` can mutate input or block execution. `tool_result` handlers compose, with each handler seeing prior changes.\n\n<a id=\"provider_stream_event\"></a>\n\n`provider_stream_event` fires for each parsed provider stream event before Pi normalizes it. The event identifies the provider, API, and model; `event.data` is the earliest structured value available to Pi, not necessarily the original HTTP bytes or SSE frame. Treat it as read-only because mutation can affect normalization. The event is notification-only and is not persisted.\n\nHandlers are awaited in stream order, so slow handlers delay stream consumption. Handler errors are reported without changing the provider response. See [`debug-provider.ts`](../examples/extensions/debug-provider.ts) for an opt-in viewer that groups raw events by assistant message.\n\n<a id=\"context_with_system\"></a>\n\n`context` transforms conversation messages without prompt and tool system messages; Pi restores that state afterward. Use `context_with_system` only when a request-local transformation must own the complete transcript, and keep a system message at index zero.\n\n`turn_end` and `agent_before_settle` are actionable boundaries. Their handlers can chain proposed `custom`, `custom_message`, `context_edit`, or `compaction` entries and return `continue: true` for one next model request. Guard continuation conditions because an unconditional continuation can loop. Use the exported event declarations for the complete validation and ordering contract.\n\n<a id=\"cache_warming_decision\"></a>\n\n`cache_warming_decision` can override an idle prompt-cache refresh with `{ action: \"warm\" }` or `{ action: \"stop\" }`. The last handler that returns an action wins.\n\nTool calls from one assistant message can run in parallel.\nDo not assume a sibling call or result exists when another tool event runs.\nUse `ctx.signal` for nested work owned by an active turn; commands and idle session events often have no operation signal.\n\nA `user_bash` handler that returns `undefined` passes the command to the next handler and then to local execution if no handler handles it. Returning `operations` or `result` stops propagation. A handler failure blocks the command rather than falling through to local execution.\n\n<a id=\"custom-tools\"></a>\n<a id=\"register-tools\"></a>\n\n### Tools\n\nA custom tool defines a name, model-facing description, TypeBox parameter schema, and `execute()` function.\nIts result requires model-facing `content` and a `details` field for rendering or state reconstruction.\nUse `details: undefined` when there are no structured details. If the tool makes nested model calls, include their `usage` in the result so session totals remain accurate.\n\nThrow from `execute()` to produce a failed tool result.\nReturning an object does not mark it as an error.\nReturn `terminate: true` only when the agent should skip its automatic follow-up after every completed tool in that batch agrees to terminate.\n\nUse sequential execution when tools share mutable in-memory state.\nFile-mutating tools should wrap the complete read-modify-write operation with `withFileMutationQueue()`.\nTruncate large model-facing results and tell the model where to read the complete output.\n\nDeclare `outputSchema` and return a matching `structuredContent` when the result is data. The model still receives `content`; programmatic callers such as codemode scripts receive `structuredContent` instead of the text. Tools without `outputSchema` are passed to scripts as their text content. To report a failure that still carries data, return the result with `isError: true` instead of throwing: the model sees an error, and scripts still receive `structuredContent`.\n\nA tool can run other tools with `ctx.executeTool(name, args, { signal, onUpdate })`. Nested calls go through argument validation and the `tool_call` and `tool_result` handlers like model-issued calls, and emit `tool_execution_start`, `tool_execution_update`, and `tool_execution_end`; all of these events carry `parentToolCallId`, and their `toolCallId` is assigned by pi as `<parent id>/<n>`. These ids do not appear as tool calls or tool results in the transcript. Nested calls do not add transcript entries: their results only reach the calling tool, which reports them itself, for example through `onUpdate` and `details`. The session keeps a bounded record of them (name, arguments, status, duration, error; never results) as `nestedCalls` on the calling tool's result message. It is used for compaction file lists and shown in HTML exports. Arguments over 8 KiB per call or 32 KiB per tool result are omitted, at most 256 calls are kept, and `complete: false` marks a record that lost anything. The `usage` of nested results, at every depth, is added to the calling tool's result `usage`, so a tool reports only its own usage, not that of the tools it called. `ctx.tools` lists the tools `ctx.executeTool()` can call. `tool_result` handlers that redact `content` should also replace `structuredContent`; replacing only `content` drops it.\n\nSee [`hello.ts`](../examples/extensions/hello.ts), [`todo.ts`](../examples/extensions/todo.ts), [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts), and [`truncated-tool.ts`](../examples/extensions/truncated-tool.ts).\n\n### Tool exposure\n\n`exposure` controls how the model reaches a tool. \"Callable\" means callable from other tools through `ctx.executeTool()` (`ctx.tools`), as the `codemode` tool's scripts do:\n\n- `direct` (default): declared to the model while active, and callable while active.\n- `model-only`: declared to the model while active, never callable. Use it for tools that orchestrate other tools or ask the user.\n- `codemode`: callable whenever registered, and listed by the `codemode` tool. Not declared to the model unless activated explicitly.\n- `deferred`: like `codemode`, but codemode tools do not list it; `tool_search` can find and activate it.\n- `hidden`: registered but unreachable. Re-register a tool with `exposure: \"hidden\"` to withdraw it, since tools cannot be unregistered.\n\n`namespace: { name, aliases, description, instructions }` groups related tools, as MCP servers do. Codemode tools list a namespace under one heading with its `description`. `instructions` holds longer usage guidance; it is not listed, and codemode scripts read it with `describeNamespace(name)`. `aliases` are other names scripts may look the namespace up by, for example the server name `docs` for `mcp__docs`; a name and the same name as a script identifier (`my-docs`, `my_docs`) match alike.\n\nRegistering a `direct` or `model-only` tool activates it; the other exposures are not activated on registration. The active set (`pi.getActiveTools()`, `pi.setActiveTools()`) is the set of tools declared to the model. `pi.getAllTools()` reports each tool's `exposure`, `namespace`, `annotations`, and `reachesTools`.\n\n`annotations` are hints about what a tool does, with the meaning of MCP tool annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. MCP tools carry the hints their server declares. Missing hints take the MCP defaults: a tool is not read-only, and may be destructive and reach an open world. The hints are not verified, but a permission extension can use them to decide which calls to confirm. This confirms the calls Codex asks approval for:\n\n```typescript\npi.on(\"tool_call\", async (event, ctx) => {\n  const hints = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;\n  const needsApproval =\n    hints?.destructiveHint === true ||\n    (!hints?.readOnlyHint && ((hints?.destructiveHint ?? true) || (hints?.openWorldHint ?? true)));\n  if (needsApproval && !(await ctx.ui.confirm(\"Allow tool call?\", event.toolName))) {\n    return { block: true, reason: `${event.toolName} was not approved` };\n  }\n});\n```\n\nA tool that orchestrates other tools can adjust what the model sees while it is active with `prepareLoadout(loadout)`. It runs whenever the active tools change and receives the declared tools, the callable tools, and every registered tool with its exposure, namespace, and prompt guidelines. It returns replacement `descriptions` for declared tools (including its own) and `hiddenDeclarations`: active tools whose declarations requests leave out while they stay active and callable. The default system prompt leaves hidden tools out of its tool list and rules, and names no tool in the skills hint when the file reader is hidden, so the orchestrating tool should present their guidelines itself. `codemode` uses only this hook, `exposure`, `reachesTools`, and `ctx.executeTool()`, so another tool can implement the same behavior under a different name.\n\nA tool that reaches other tools declares which ones a call may reach with `reachesTools(input)`: `\"all\"`, or the words of the input that may name them, where a word refers to every tool or namespace whose name it contains. `codemode` returns `\"all\"` for scripts that search or list tools and the words of the script otherwise; `tool_search` always returns `\"all\"`. Extensions that register tools late use it from their `tool_call` handler to finish registering the tools a call needs before it runs, as the MCP extension does for servers that are still connecting. The MCP extension also activates `codemode` or `tool_search` for MCP tools only when the tool of that name declares `reachesTools`.\n\n### Activate tools dynamically\n\nRegister every tool first, keep optional tools inactive, and use `pi.setActiveTools()` from a loader tool to select the desired active tools. Names must already be registered; unknown names are ignored.\n\nPi records the initial prompt and tool set in the transcript's first system message, then appends tool and prompt changes before the next model request. Providers that cannot represent the transition receive a complete transcript checkpoint, which can invalidate the cached prefix.\n\n### Tool rendering\n\nA tool's `renderCall` and `renderResult` draw its calls in the interactive transcript and in HTML exports. `pi.registerToolRenderer((toolName, next) => renderers)` chooses renderers for calls to any tool, including tools that are not registered yet, such as MCP tools in a resumed session before their server connected. `next()` returns what the remaining resolvers (in extension load order), then the registered tool, would use, so `next() ?? mine` only fills in.\n\n### MCP servers\n\n`pi.registerMcpServer(name, config)` adds an MCP server for the current session. `config` has the shape of an `mcpServers` entry in [`mcp.json`](mcp.md): `command`, `args`, `env`, and `cwd` for stdio servers, `url`, `headers`, and `oauth` for HTTP servers, plus `exposure`, `toolExposure`, `description`, `enabled`, and `timeout`.\n\n```typescript\npi.registerMcpServer(\"jira\", { url: \"https://mcp.example.com/jira\", exposure: \"codemode\" });\npi.unregisterMcpServer(\"jira\");\n```\n\nServers registered while the extension loads connect when the session starts, together with the `mcp.json` servers; servers registered later connect right away, and `pi.unregisterMcpServer()` closes the connection and makes the server's tools unreachable. Registrations are not saved: register again on every load, for example based on the extension's own settings. A server in `mcp.json` with the same name takes precedence, and `/mcp` shows the override. Registering the same name again replaces the extension's earlier registration; names registered by another extension, invalid names, and invalid configs throw.\n\nThe built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#other-mcp-extensions)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `pi.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.\n\n<a id=\"extensioncontext\"></a>\n<a id=\"extensioncommandcontext\"></a>\n<a id=\"use-extension-context\"></a>\n\n### Context and session changes\n\n`ExtensionContext` provides the working directory, mode, UI, session manager, model runtime, abort signal, context usage, and controls for compaction and shutdown.\nUse `ctx.modelRegistry.streamSimple()` for provider-neutral nested model calls.\n\nCommand handlers receive `ExtensionCommandContext`, which adds operations for waiting until idle, reloading, tree navigation, and session replacement.\nThese operations are command-only because calling them from lifecycle handlers can deadlock the runtime.\n\nSession replacement invalidates the old context. Capture only plain data before switching, then use the fresh context supplied to `withSession` for session-bound work.\n\n<a id=\"state-management\"></a>\n<a id=\"persist-state\"></a>\n\n### State\n\nChoose storage based on how state participates in the conversation:\n\n| State | Storage |\n|---|---|\n| Tool state that follows the active branch | Tool-result `details` |\n| Durable data excluded from model context | `pi.appendEntry()` |\n| Custom content stored and sent to the model | `pi.sendMessage()` |\n| Data outside one session | External storage |\n\nReconstruct branch-sensitive state from `ctx.sessionManager.getBranch()` during `session_start`.\nDo not rebuild it from every file entry because abandoned branches represent alternative histories.\nRegister an entry or message renderer when custom stored content should appear in the transcript.\n\n<a id=\"custom-ui\"></a>\n<a id=\"mode-behavior\"></a>\n<a id=\"interact-with-the-user\"></a>\n<a id=\"account-for-each-mode\"></a>\n\n### UI and modes\n\n`ctx.ui` provides dialogs, notifications, status text, widgets, titles, editor access, and custom components.\nUse `ctx.ui.custom()` only when the interaction needs its own rendering and input.\nSee [Terminal UI](tui.md) for component, focus, overlay, theme, and performance guidance.\n\nExtensions load in interactive, RPC, JSON, and print modes.\nInteractive mode provides the complete terminal UI.\nRPC can forward supported dialogs and notifications through the [RPC Extension UI protocol](rpc-extension-ui.md), but not custom terminal components; JSON and print modes have no UI.\nGuard terminal-only behavior with `ctx.mode === \"tui\"` and use `ctx.hasUI` for interactions supported by interactive and RPC clients.\n\nKeep tool and event behavior independent from rendering so non-interactive modes remain functional.\n\n<a id=\"error-handling\"></a>\n<a id=\"handle-errors-and-shutdown\"></a>\n\n### Errors and cleanup\n\nPi reports handler errors and continues where possible. A `tool_call` handler failure blocks the tool as a fail-safe; a tool execution failure becomes an error result for the model.\n\nRelease resources in `session_shutdown` even when normal operation attempted cleanup.\nKeep cleanup idempotent because cancellation, reload, session replacement, and process exit can converge on the same path.\nUse `ctx.shutdown()` to request an orderly process shutdown.\n\n<a id=\"examples-reference\"></a>\n<a id=\"use-examples-as-the-implementation-reference\"></a>\n\n## Examples and reference\n\nThe checked [extension examples](../examples/extensions/) are the implementation reference. Start with the smallest example matching the behavior you need:\n\n| Goal | Start with | Why |\n|---|---|---|\n| Confirm or block tool calls | [`permission-gate.ts`](../examples/extensions/permission-gate.ts) | Combines `tool_call`, mode-aware UI, and fail-closed behavior. |\n| Modify prompt sections for a run | [`prompt-customizer.ts`](../examples/extensions/prompt-customizer.ts) | Uses structured prompt options instead of replacing the complete prompt. |\n| Keep branch-aware tool state | [`todo.ts`](../examples/extensions/todo.ts) | Reconstructs state from persisted entries and renders it in the TUI. |\n| Register tools after startup | [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts) | Shares registration logic between lifecycle and command handlers. |\n| Run cancellable model work from a command | [`qna.ts`](../examples/extensions/qna.ts) | Combines nested model calls, cancellation, and custom UI. |\n| Replace the main editor | [`modal-editor.ts`](../examples/extensions/modal-editor.ts) | Extends `CustomEditor` while preserving application controls. |\n| Run finalization at shutdown | [`auto-commit-on-exit.ts`](../examples/extensions/auto-commit-on-exit.ts) | Uses the session shutdown hook for finalization work. |\n| Route built-in tools to another machine | [`ssh.ts`](../examples/extensions/ssh.ts) | Reuses tool operation interfaces instead of recreating tool contracts. |\n| Add OAuth and provider-specific streaming | [`custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/) | Shows a complete provider integration while delegating supported protocols. |\n\nUse [Custom Providers](custom-provider.md) for model-service integrations, [Terminal UI](tui.md) for custom components, and [Pi Packages](packages.md) to install or distribute extensions with other resources. Use the exported declarations for exact signatures, payloads, and result schemas; this guide focuses on lifecycle choices, composition, and durable implementation patterns.\n\n<!-- File: packages/coding-agent/docs/how-pi-works.md (Tokens: ~597 | Category: anthropic-skill) -->\n# How Pi Works\n\nPi coordinates model requests, tool execution, context assembly, and session storage. A session is Pi's record of a conversation, including messages, tool calls and results, model changes, compactions, and other events.\n\nMessages and events in a session form a tree. Each path through that tree is a branch. The branch ending at the current entry is the active branch and supplies the history for the next model request.\n\n## Agent loop\n\nA submitted message is added to the active branch. Pi builds a model request from the system prompt, active branch, available tools, and model settings, then sends it through the selected provider.\n\nThe provider streams an assistant response, which can contain text and tool calls. Pi records the response, executes each tool call, and records the results. That completes one turn. If tool results or queued messages require another model request, Pi starts another turn. Otherwise, the run ends.\n\nSteering messages enter after the current assistant turn. Follow-up messages enter after the agent has finished its pending work. Aborting stops the current run and returns queued messages to the editor.\n\n## Context\n\nThe active branch supplies conversation history. Pi converts its session entries into model-compatible user, assistant, and tool-result messages.\n\nPi builds the system prompt from its base instructions and discovered context files. The request also carries tool definitions and skill descriptions.\n\nFull skill instructions are loaded on demand. Extensions can add instructions or transform context.\n\nPrompt templates expand editor input before it becomes a user message. Selected files, images, pasted text, and shell output can become message content.\n\n## Sessions\n\nPersistent sessions are JSONL files. Each tree entry has an ID and refers to its parent. The current entry identifies the active branch.\n\nContinuing from an earlier entry creates another branch in the same file. Forking and cloning copy selected history into a new session file.\n\nModel context is reconstructed from the active branch. Compaction inserts a summary entry that replaces older messages in subsequent model requests. The original entries remain in the session tree.\n\n## Interfaces\n\nInteractive mode renders session and agent events in the terminal. Print mode runs a prompt and writes the final response. JSON mode writes agent events as JSONL.\n\nRPC mode accepts JSONL commands on stdin and writes responses and events to stdout. The TypeScript SDK creates and controls agent sessions in process.\n\nAll interfaces use the same agent and session mechanisms.\n\n## Extensions and resources\n\nExtensions are TypeScript modules loaded into the Pi process. Their factory functions register tools, commands, shortcuts, providers, event handlers, renderers, and terminal UI.\n\nSkills provide on-demand instructions and supporting files. Prompt templates provide reusable message text. Themes provide terminal colors. Pi packages distribute these resources through npm or git.\n\n## Trust and permissions\n\nPi resolves project trust before loading project settings and resources. After the trust decision and project-resource loading, Pi loads context files. Enabled tools use the operating-system permissions of the Pi process. Extensions execute inside that process.\n\n<!-- File: packages/coding-agent/docs/index.md (Tokens: ~555 | Category: anthropic-skill) -->\n# Pi\n\nPi is an extensible AI agent that works from your terminal. Give it a goal and a working folder, and it can inspect files, run commands, edit content, and work through multi-step tasks.\n\nUse Pi for software development, research notes, writing projects, data files, or hobby work. You can use Pi as is, prompt it to adapt itself to your workflow, or build other applications powered by Pi using the SDK.\n\n## Start using Pi\n\nNew to Pi? Follow the [Quickstart](quickstart.md) to install Pi, connect a model, and complete your first task.\n\nIf Pi is already installed, choose what you want to do:\n\n- [Use Pi interactively](usage.md) to add files, run commands, direct ongoing work, and export results.\n- [Choose a model](models.md) or connect a subscription, API key, local model, or compatible endpoint.\n- [Continue or branch a session](sessions.md) to resume work or explore another approach without losing history.\n- [Configure Pi](configuration.md) for your preferences, working folders, instructions, and reusable resources.\n- [Understand how Pi works](how-pi-works.md), including tools, context, sessions, and the agent loop.\n\n## Customize Pi\n\nPi can reuse prompts, load specialized instructions, add executable integrations, change its terminal interface, connect model services, and distribute these resources as packages.\nUse the [Quickstart customization chooser](quickstart.md#choose-how-to-customize-pi) to select the smallest mechanism that meets your need.\n\n## Automate or embed Pi\n\n- Use [print mode](cli.md#invocation-and-output) for one-off and scripted tasks.\n- Use [JSON event stream mode](json.md) to consume structured events from one run.\n- Use [RPC mode](rpc.md) to control a separate Pi process.\n- Use the [TypeScript SDK](sdk.md) to run Pi inside an application.\n\n## Find reference and setup information\n\nUse the reference pages to look up [CLI options](cli.md), [settings](settings.md), [providers](providers.md), [keybindings](keybindings.md), and [environment variables](environment-variables.md).\n\nFor platform-specific help, see [Terminal Setup](terminal-setup.md), [Windows](windows.md), [tmux](tmux.md), [Termux on Android](termux.md), or [Containerization](containerization.md).\n\n## Work safely\n\nPi's tools and extensions run with the permissions of the Pi process. Project trust controls which project resources Pi loads, but it does not sandbox tool calls. Review [Security](security.md) before using untrusted files, repositories, extensions, or unattended automation.\n\n<!-- File: packages/coding-agent/docs/json.md (Tokens: ~2689 | Category: anthropic-skill) -->\n# JSON Event Stream\n\nJSON mode emits structured progress for one invocation:\n\n```bash\npi --mode json \"Review this repository\"\n```\n\nPi writes one session header followed by session events, then exits after the supplied prompts finish. RPC mode emits the same session-event shapes but has no session header because it is a bidirectional, long-lived protocol. See [RPC Mode](rpc.md).\n\nThis page is the canonical reference for events shared by JSON and RPC mode. Message values use the [shared message types](message-types.md).\n\n## Framing and process I/O\n\nThe stream uses strict JSONL framing. Each record is one JSON object terminated by LF (`\\n`). Split records only on LF and strip an optional preceding carriage return. Unicode line and paragraph separators are valid inside JSON strings and are not record boundaries.\n\nNode.js `readline` is not suitable for this stream because it also recognizes those Unicode separators. Use a byte or UTF-8 stream decoder and split on LF.\n\nRead stdout continuously. A reader that stops consuming records can stall Pi when the pipe buffer fills. Stdout is reserved for JSONL; diagnostics and application logging go to stderr.\n\n## Session header\n\nThe first JSON-mode record is the current [session header](session-format.md#sessionheader):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path\"}\n```\n\nRPC mode does not emit this record. Use [`get_state`](rpc-commands.md#get_state) for its current session ID and file.\n\n## Event sequence\n\nA basic run produces records like these:\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"user\",\"content\":\"Review this repository\",\"timestamp\":1733234401000}}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],\"stopReason\":\"pending\",\"...\":\"...\"}}\n{\"type\":\"message_update\",\"usage\":{\"...\":\"...\"},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"}}\n{\"type\":\"turn_end\",\"message\":{\"role\":\"assistant\",\"...\":\"...\"},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[{\"...\":\"...\"}],\"willRetry\":false}\n{\"type\":\"agent_settled\",\"aborted\":false}\n```\n\n`agent_end` closes one low-level agent run. Automatic retry, overflow recovery, compaction retry, steering, or follow-up work can still continue. `agent_settled` means Pi has no remaining automatic work for that session-level run.\n\n## Agent and turn events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `agent_start` | None | A low-level agent run started. |\n| `agent_end` | `messages`, `willRetry` | That low-level run ended. `messages` contains messages generated by the run. |\n| `agent_settled` | `aborted` | Pi will not continue automatically through retries, compaction recovery, or queued messages. `aborted` is `true` when the run ended because it was aborted. |\n| `turn_start` | None | One assistant turn started. |\n| `turn_end` | `message`, `toolResults` | One assistant response and its resulting tool calls finished. |\n\nA turn is one assistant response plus any tool calls and tool results produced by that response.\n\n## Message events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `message_start` | `message` | A message started. |\n| `message_update` | `usage`, `assistantMessageEvent` | An assistant message emitted a content-block update. |\n| `message_end` | `message` | A message completed. This is the authoritative final message. |\n\n### Reconstruct streaming messages\n\nWire `message_update` records are delta-only. They omit the SDK event's cumulative `message` field and every `assistantMessageEvent.partial` snapshot so stream size remains linear.\n\nThe nested event is one of:\n\n| Type | Fields in addition to `type` | Meaning |\n|---|---|---|\n| `start` | None | The provider stream started; its cumulative `partial` field is removed on the wire. |\n| `text_start` | `contentIndex` | A text block started. |\n| `text_delta` | `contentIndex`, `delta` | Append text to the block. |\n| `text_end` | `contentIndex`, `content` | The text block ended with authoritative content. |\n| `thinking_start` | `contentIndex` | A thinking block started. |\n| `thinking_delta` | `contentIndex`, `delta` | Append thinking text to the block. |\n| `thinking_end` | `contentIndex`, `content` | The thinking block ended with authoritative content. |\n| `toolcall_start` | `contentIndex`, `id`, `toolName` | A tool-call block started. |\n| `toolcall_delta` | `contentIndex`, `delta` | Append serialized argument data. |\n| `toolcall_end` | `contentIndex`, `toolCall` | The tool call ended with the complete `ToolCall`. |\n| `done` | `reason`, `message` | The provider stream completed successfully. |\n| `error` | `reason`, `error` | The provider stream ended with an error or abort message. |\n\nThe normal agent loop translates provider-level `start`, `done`, and `error` into `message_start` and `message_end` session events rather than emitting them as `message_update`. They remain admitted by the exported `JsonAgentSessionEvent` transformation for callers that construct a matching session event.\n\nUse `contentIndex` to identify the content block. Buffer `delta` fields for a live display, but replace reconstructed data with the completed content in `text_end`, `thinking_end`, or `toolcall_end`. Replace the whole partial message with `message_end.message` when it arrives.\n\nThe top-level `usage` is the latest cumulative provider-reported usage for the assistant response. It can remain zero until completion when a provider does not report usage while streaming.\n\n```json\n{\"type\":\"message_update\",\"usage\":{\"input\":100,\"output\":1,\"cacheRead\":0,\"cacheWrite\":0,\"totalTokens\":101,\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0,\"total\":0}},\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello \"}}\n```\n\n## Tool execution events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `tool_execution_start` | `toolCallId`, `toolName`, `args` | Tool execution started. |\n| `tool_execution_update` | `toolCallId`, `toolName`, `args`, `partialResult` | The tool reported a partial result. |\n| `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError`, `durationMs` | Tool execution finished. `durationMs` is how long the tool's `execute()` took, measured with a monotonic clock; absent when the tool did not run. |\n\nUse `toolCallId` to correlate the lifecycle. `partialResult` is the latest partial result supplied by the tool. Whether it replaces or extends an earlier update depends on that tool's result contract.\n\n```json\n{\"type\":\"tool_execution_start\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"}}\n{\"type\":\"tool_execution_update\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"args\":{\"command\":\"ls -la\"},\"partialResult\":{\"content\":[{\"type\":\"text\",\"text\":\"partial output\"}],\"details\":{}}}\n{\"type\":\"tool_execution_end\",\"toolCallId\":\"call_abc123\",\"toolName\":\"bash\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"complete output\"}],\"details\":{}},\"isError\":false}\n```\n\n## Queue and state events\n\n| Event | Fields | Meaning |\n|---|---|---|\n| `queue_update` | `steering`, `followUp` | The pending steering or follow-up queue changed. Both fields contain the complete current queue. |\n| `entry_appended` | `entry` | An extension appended a custom session entry through `pi.appendEntry()`. |\n| `session_info_changed` | `name` | The session display name changed. An absent `name` means it was cleared. |\n| `thinking_level_changed` | `level` | The active thinking level changed. |\n\nThe `entry` value uses a persisted [session entry type](session-format.md#entry-types).\n\n## Compaction events\n\n`compaction_start` reports why compaction began:\n\n```json\n{\"type\":\"compaction_start\",\"reason\":\"threshold\"}\n```\n\n`reason` is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n`compaction_end` contains the result when compaction succeeds:\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\"...\": \"...\"},\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\nIf compaction was aborted, `result` is absent and `aborted` is true. If it failed, `result` is absent, `aborted` is false, and `errorMessage` describes the failure. Successful overflow recovery sets `willRetry` to true before Pi retries the prompt.\n\nSee [Compaction and Branch Summaries](compaction.md) for result semantics.\n\n## Retry events\n\nAssistant-turn retry emits:\n\n```json\n{\"type\":\"auto_retry_start\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"529 overloaded\"}\n{\"type\":\"auto_retry_end\",\"success\":true,\"attempt\":2}\n```\n\nOn final failure, `auto_retry_end` has `success: false` and a `finalError` string.\n\nCompaction and branch-summary retry emit:\n\n```json\n{\"type\":\"summarization_retry_scheduled\",\"attempt\":1,\"maxAttempts\":3,\"delayMs\":2000,\"errorMessage\":\"terminated\"}\n{\"type\":\"summarization_retry_attempt_start\",\"source\":\"compaction\",\"reason\":\"threshold\"}\n{\"type\":\"summarization_retry_finished\"}\n```\n\nFor a branch summary, `source` is `\"branchSummary\"` and `reason` is absent. The `reason` on a compaction retry is `\"manual\"`, `\"threshold\"`, or `\"overflow\"`.\n\n## RPC-only events\n\nA direct RPC [`bash`](rpc-commands.md#bash) command emits one `bash_execution_update` for each output chunk. Its optional `id` matches the command ID. The final command response can contain truncated output, but these events stream all output:\n\n```json\n{\"type\":\"bash_execution_update\",\"id\":\"req-1\",\"delta\":\"total 48\\n\"}\n```\n\nRPC also adds `extension_error` when an extension handler throws:\n\n```json\n{\"type\":\"extension_error\",\"extensionPath\":\"/path/to/extension.ts\",\"event\":\"tool_call\",\"error\":\"Error message\"}\n```\n\nExtension UI records are a separate RPC subprotocol, not `AgentSessionEvent` values. See [RPC Extension UI](rpc-extension-ui.md).\n\n## TypeScript types\n\nThe SDK's `AgentSessionEvent` contains cumulative streaming snapshots for in-process consumers. JSON and RPC transform only `message_update`:\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAssistantMessageEvent<T> = T extends { type: \"toolcall_start\"; partial: unknown }\n  ? WithoutPartial<T> & { id: string; toolName: string }\n  : WithoutPartial<T>;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      usage: Usage;\n      assistantMessageEvent: JsonAssistantMessageEvent<AssistantMessageEvent>;\n    };\n```\n\nUse the exported `JsonAgentSessionEvent` type from `@earendil-works/pi-coding-agent`. Its implementation is in [`json-event.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/json-event.ts).\n\n## Example\n\nPrint completed messages from a one-shot run:\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```\n\n<!-- File: packages/coding-agent/docs/keybindings.md (Tokens: ~2912 | Category: anthropic-skill) -->\n# Keybindings Reference\n\nPi exposes named actions, such as `app.session.new`, that can be assigned keybindings. You can change default assignments or bind unassigned actions in Pi's [user configuration](configuration.md#agent-directory).\n\nRun `/hotkeys` to see the active shortcuts for the main editor and application.\n\n## Assign keybindings\n\nCreate `<agent-dir>/keybindings.json`. The agent directory defaults to `~/.pi/agent` and is described in [Agent directory](configuration.md#agent-directory).\n\nMap each action identifier to one key or a list of keys:\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/keybindings.schema.json\",\n  \"app.session.new\": \"ctrl+shift+n\",\n  \"app.session.tree\": [\"ctrl+shift+t\", \"alt+shift+t\"]\n}\n```\n\nA configured value replaces the default for that action. Use an empty list to disable an action's keybindings:\n\n```json\n{\n  \"tui.altScreen.pageUp\": []\n}\n```\n\nAfter editing the file, run `/reload` to apply the changes to the active session.\n\n## Key syntax\n\nWrite a key as `modifier+key`. Modifiers are `ctrl`, `shift`, `alt`, and `super`. You can combine modifiers. Valid keys are:\n\n- **Letters:** `a-z`\n- **Digits:** `0-9`\n- **Special:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Function:** `f1`-`f12`\n- **Symbols:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nExamples: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, and `ctrl+1`.\n\n`super` bindings require a terminal that reports the modifier separately, typically through the Kitty keyboard protocol. They may not work in terminals without that support.\n\n## Actions\n\n### Terminal UI\n\n#### Cursor movement\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.cursorUp` | `up` | Move cursor up, browsing older history at the top |\n| `tui.editor.cursorDown` | `down` | Move cursor down, browsing newer history at the bottom |\n| `tui.editor.historyPrevious` | None | Select the previous prompt history entry |\n| `tui.editor.historyNext` | None | Select the next prompt history entry |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Move cursor left |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Move cursor right |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Move cursor word left |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Move cursor word right |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+a` | Move to line start |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+e` | Move to line end |\n| `tui.editor.jumpForward` | `ctrl+]` | Jump forward to character |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Jump backward to character |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Scroll up by page |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Scroll down by page |\n\nThe dedicated history actions browse prompt history regardless of cursor position and take precedence over application actions using the same key.\n\n#### Text editing\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.editor.deleteCharBackward` | `backspace` | Delete character backward |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Delete character forward |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Delete word backward |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Delete word forward |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Delete to line start |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Delete to line end |\n| `tui.editor.yank` | `ctrl+y` | Paste most recently deleted text |\n| `tui.editor.yankPop` | `alt+y` | Cycle through deleted text after yank |\n| `tui.editor.undo` | `ctrl+-` (`ctrl+z` on Windows; `alt+z` on WSL) | Undo last edit |\n\n#### Input and selection\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insert new line |\n| `tui.input.submit` | `enter` | Submit input |\n| `tui.input.tab` | `tab` | Tab or autocomplete |\n| `tui.input.copy` | `ctrl+c` | Copy selection |\n| `tui.select.up` | `up` | Move selection up |\n| `tui.select.down` | `down` | Move selection down |\n| `tui.select.pageUp` | `pageUp` | Page up in list |\n| `tui.select.pageDown` | `pageDown` | Page down in list |\n| `tui.select.confirm` | `enter` | Confirm selection |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Cancel selection |\n\n#### Fullscreen\n\nIn fullscreen mode, these actions control the transcript and take precedence over editor actions using the same key.\n\n| Keybinding id | Default | Description |\n|---|---|---|\n| `tui.altScreen.pageUp` | `pageUp` | Scroll the transcript up by one page |\n| `tui.altScreen.pageDown` | `pageDown` | Scroll the transcript down by one page |\n| `tui.altScreen.halfPageUp` | None | Scroll the transcript up by half a page |\n| `tui.altScreen.halfPageDown` | None | Scroll the transcript down by half a page |\n| `tui.altScreen.lineUp` | None | Scroll the transcript up by one line |\n| `tui.altScreen.lineDown` | None | Scroll the transcript down by one line |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up`, `ctrl+up` (`ctrl+up` only on Windows and WSL) | Jump to the previous marked message |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down`, `ctrl+down` (`ctrl+down` only on Windows and WSL) | Jump to the next marked message |\n| `tui.altScreen.search` | `ctrl+shift+f` (`ctrl+f` on Windows and WSL) | Search the rendered transcript |\n| `tui.altScreen.searchNext` | `enter`, `ctrl+g` | Select the next search match while searching |\n| `tui.altScreen.searchPrevious` | `shift+enter`, `ctrl+shift+g` | Select the previous search match while searching |\n| `tui.altScreen.searchClose` | `escape` | Close transcript search |\n| `tui.altScreen.top` | `ctrl+home` | Scroll to the beginning of the transcript |\n| `tui.altScreen.bottom` | `ctrl+end` | Scroll to the transcript end and follow new output |\n\n### Application\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Cancel / abort |\n| `app.clear` | `ctrl+c` | Clear editor (first) / exit (second) |\n| `app.exit` | `ctrl+d` | Exit (when editor empty) |\n| `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background |\n| `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard |\n\nOn native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior.\n\n### Sessions\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.session.new` | None | Start a new session (`/new`) |\n| `app.session.tree` | None | Open session tree navigator (`/tree`) |\n| `app.session.fork` | None | Fork current session (`/fork`) |\n| `app.session.resume` | None | Open session resume picker (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Toggle path display |\n| `app.session.toggleSort` | `ctrl+s` | Toggle sort mode |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Toggle named-only filter |\n| `app.session.rename` | `ctrl+r` | Rename session |\n| `app.session.delete` | `ctrl+d` | Delete session |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Delete session when query is empty |\n\n### Models and Thinking\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Open model selector |\n| `app.model.cycleForward` | `ctrl+p` | Cycle to next model |\n| `app.model.cycleBackward` | `shift+ctrl+p` (`alt+p` on Windows and WSL) | Cycle to previous model |\n| `app.models.save` | `ctrl+s` | Save the selected default model or scoped model configuration to settings |\n| `app.thinking.cycle` | `shift+tab` | Cycle thinking level |\n| `app.thinking.save` | `ctrl+s` | Save current thinking level to settings |\n| `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks |\n\n### Display and Message Queue\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Collapse or expand tool output |\n| `app.message.copy` | `ctrl+x` | Copy the selected message in `/tree`; in fullscreen mode, copy the active selection when `fullscreenCopyOnSelect` is `false`; otherwise copy the last assistant message. On OAuth sign-in screens, copy the sign-in URL |\n| `app.message.followUp` | `alt+enter` (`ctrl+q` on Windows and WSL) | Queue follow-up message |\n| `app.message.dequeue` | `alt+up` (`alt+q` on Windows and WSL) | Restore queued messages to editor |\n\n### Tree Navigation\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Fold current branch segment, or jump to the previous segment start |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Unfold current branch segment, or jump to the next segment start or branch end |\n| `app.tree.editLabel` | `shift+l` | Edit the label on the selected tree node |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Toggle label timestamps in the tree |\n| `app.tree.filter.default` | `ctrl+d` | Set tree filter to default view |\n| `app.tree.filter.noTools` | `ctrl+t` | Toggle tree filter that hides tool results |\n| `app.tree.filter.userOnly` | `ctrl+u` | Toggle tree filter that shows only user messages |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Toggle tree filter that shows only labeled entries |\n| `app.tree.filter.all` | `ctrl+a` | Toggle tree filter that shows all entries |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Cycle tree filter forward |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Cycle tree filter backward |\n\n### Scoped Models Selector\n\nUsed inside the scoped models selector (opened via `/scoped-models`).\n\n| Keybinding id | Default | Description |\n|--------|---------|-------------|\n| `app.models.enableAll` | `ctrl+a` | Enable all models (or all matching the current search) |\n| `app.models.clearAll` | `ctrl+x` | Clear all models (or all matching the current search) |\n| `app.models.toggleProvider` | `ctrl+p` | Toggle all models for the current provider |\n| `app.models.reorderUp` | `alt+up` | Move the selected model up in the cycle order |\n| `app.models.reorderDown` | `alt+down` | Move the selected model down in the cycle order |\n\n<!-- File: packages/coding-agent/docs/llama-cpp.md (Tokens: ~1845 | Category: anthropic-skill) -->\n# Local Models with llama.cpp\n\nPi supports the [llama.cpp](https://github.com/ggml-org/llama.cpp) router server. The router discovers multiple GGUF models and loads or unloads them on demand.\n\nUse a current llama.cpp build with router support. Follow the [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) or install a [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) for your platform.\n\n## Start the router\n\nStart `llama-server` without `--model` or `-m`. Passing a model starts single-model mode instead of router mode.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\nImportant options:\n\n- `--models-dir ~/models` discovers local GGUF files.\n- `--no-models-autoload` keeps loading explicit through `/llama`.\n- `--jinja` enables compatible chat templates and tool calling.\n- `-ngl 999` offloads as many layers as possible to the GPU.\n- `-c 32768` sets the context window for each loaded model. Omit it to use the model's native context, which may require substantially more memory.\n\nA single-file model can sit directly in the model directory. Put multimodal and multi-shard models in separate subdirectories:\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\nRestart the router after manually adding files. For per-model context sizes and other options, use [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets). For settings tuned to a specific NVIDIA GPU and model, see [Run Pi with NVIDIA RTX and llama.cpp](nvidia-rtx-llama-cpp.md).\n\n## Configure Pi\n\nStart Pi and configure the provider:\n\n```text\n/login llama.cpp\n```\n\nEnter the router URL and optional API key. The default URL is `http://127.0.0.1:8080`.\n\nIf you start the router with `--no-models-autoload`, `/login llama.cpp` only stores the connection. Run `/llama` to load a model, then `/model` to select the loaded model for the current session.\n\nEnvironment variables can configure the same values without `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nIf the server uses an API key, start `llama-server` with the matching `--api-key` value. Keep `--host 127.0.0.1` for local-only access.\n\n## Manage models\n\nRun:\n\n```text\n/llama\n```\n\n- Select an unloaded model to load it.\n- Select a loaded model to unload it.\n- Select **Download model…**, search Hugging Face, then choose a repository and quantization. Exact `owner/repository[:quant]` values also work.\n- Press Escape during a load or download to confirm cancellation.\n\nHugging Face search uses `HF_TOKEN` when set, then checks `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token`, and `~/.cache/huggingface/token`. Search also works without authentication, subject to lower rate limits. Pi warns before downloading gated repositories and links to their access page. The llama.cpp server performs the download, so its process must also have `HF_TOKEN` when the selected repository requires access.\n\nIf other models are loaded, Pi asks whether to unload them first or keep them loaded. Pi does not silently unload models and never deletes model files. The router may be shared with other clients, so `/llama` always displays the router's current state.\n\nLoaded and sleeping models appear in `/model`. Sleeping models wake automatically when selected. With router autoload enabled, unloaded preset models also appear and load when selected. With `--no-models-autoload`, load a model through `/llama` before selecting it.\n\nIf the router disconnects, `/llama` shows **Retry** and **Close**. Retry reconnects and refreshes model state without replaying the interrupted operation.\n\n## Classification\n\nClassifier models answer typed `choice`, `bool`, and `score` questions about JSON state, like TypeSafe's Jev models. The model reaches them from [`codemode`](cli.md#enable-codemode) scripts, and extensions through `ctx.modelRegistry.classify()`; see [Classifier models](models.md#use-classifier-models). Pi lists llama.cpp models as classifiers in two ways:\n\n- **Decision models** such as [Julia-1, Laya, Kev, lev, and OpenJev](https://huggingface.co/collections/ggml-org/decision-models-6abf80cca3c83f127060a769) answer natively through llama.cpp's `/v1/systemone` endpoint. They appear only as classifiers, with the `typesafe-system-one` API, and not in `/model`.\n- **Chat models** are also listed as classifiers with the same ID and the `llama-cpp-classify` API, which reads answers from next-token probabilities as described below.\n\nllama.cpp 0.6.0 and later report decision models in the router's model list: their `architecture.output_modalities` contains `decisions`. The router reads this from the GGUF metadata without loading the model, so Pi recognizes unloaded and sleeping decision models too. Older llama.cpp builds do not report it, and Pi lists their decision models as chat models.\n\n### Chat models as classifiers\n\nThe model does not generate an answer. Each question becomes one chat prompt: the state, every question of the request, the state again, and then the question with its answers under single-token labels. Labels are letters for a choice (up to 62 options), `Yes`/`No` for a bool, and digits for a score (up to 10 levels). The second copy of the state is read with the questions in view, which improved accuracy on JevBench with small models. Pi reads the probabilities of the labels as the next token and normalizes them. A choice returns every option's probability and a confidence of `(n * peak - 1) / (n - 1)`; a score returns the expected level.\n\n- Raw label probabilities are usually overconfident. The per-request `temperature` option divides the label logits before normalizing; values above 1 soften the distribution. It changes no answer.\n- Questions run one after another. Everything before the final question is the same for all questions of a request, so the server's prompt cache evaluates it once. The state appears twice, so it needs twice its size in context.\n- Small models may follow instructions written inside the state. The prompt tells the model to judge the state as data, but that is not a guarantee.\n- Hybrid models such as Qwen3.5 cannot rewind a partially cached prompt without context checkpoints. If each question reprocesses the whole state, start the router with `--ctx-checkpoints 32 --checkpoint-min-step 0`.\n\n## Troubleshooting\n\nCheck that the router is reachable:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **No models in `/llama`:** Check `--models-dir`, the directory layout, and restart the router.\n- **Model missing from `/model` with `--no-models-autoload`:** Load it with `/llama` first.\n- **Load fails or uses too much memory:** Lower `-c` or unload another model.\n- **Server is not in router mode:** Start it without `--model`, `-m`, or `-hf`.\n\nTo remove the `llama.cpp` provider and `/llama`, disable `llama.cpp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:llama.cpp\"]` in [settings](settings.md#resources).\n\n<!-- File: packages/coding-agent/docs/mcp.md (Tokens: ~4680 | Category: anthropic-skill) -->\n# MCP Servers\n\nPi connects to [Model Context Protocol](https://modelcontextprotocol.io) servers over stdio or streamable HTTP and makes their tools and resources available to the model.\n\n## Quick setup\n\nAdd a local stdio server, check the connection, then start Pi:\n\n```bash\npi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .\npi mcp list\npi\n```\n\nFor a remote server:\n\n```bash\npi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN\npi mcp list\n```\n\nThese commands add user-level servers by default. Add `--local` or `-l` to write the project configuration instead:\n\n```bash\npi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp\n```\n\nUse `/mcp` inside an interactive session to inspect connections, sign in, reconnect, change exposure, or enable and disable servers. Run `/reload` after adding, removing, or changing a server outside the session.\n\n## Configure servers\n\nPi reads user-level servers from `~/.pi/agent/mcp.json` and project servers from `.pi/mcp.json`. Project configuration is read only after [project trust](security.md#understand-project-trust) is granted. A project entry replaces a user-level entry with the same name.\n\nA project entry without `command`, `url`, or `type` overrides only `enabled`, `exposure`, and `toolExposure` of the user-level server with the same name and keeps the rest, including `env`, `headers`, and `auth`. For example, this turns off a user-level server in one project:\n\n```json\n{\n  \"mcpServers\": {\n    \"internal-tools\": { \"enabled\": false }\n  }\n}\n```\n\nThe format matches other MCP clients:\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \".\"]\n    },\n    \"docs\": {\n      \"url\": \"https://example.com/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer ${DOCS_TOKEN}\" },\n      \"description\": \"Search and read the product documentation\"\n    }\n  }\n}\n```\n\nStdio servers use `command`, `args`, `env`, and `cwd`. Relative `cwd` values resolve against the session directory. A leading `~/` in `command`, an argument, or `cwd` names the home directory.\n\nHTTP servers use `url`, `headers`, and `oauth` (see [Authenticate with OAuth](#authenticate-with-oauth)). The legacy SSE transport is not supported.\n\nBoth server types support:\n\n- `timeout`: per-request timeout in seconds (default 60). Progress notifications reset it.\n- `enabled: false`: keep the entry without connecting to it.\n- `exposure` and `toolExposure`: control how tools reach the model (see [Control tool exposure](#control-tool-exposure)).\n- `description`: what the server offers, in a sentence. It lists the server in the system prompt (see [Control tool exposure](#control-tool-exposure)), tool search ranks the server's tools by it, and codemode's `describeNamespace()` returns it. Without it, the first line of the server instructions is used once the server connects.\n\nKeep personal servers and servers with credentials in the user-level file. Use the project file only for servers the project requires, and only in trusted projects.\n\n### Configuration rules\n\n- Server names may contain only letters, digits, `_`, and `-`. Tools are named `mcp__<server>__<tool>`, with every character other than letters, digits, and `_` replaced by `_`; tools of a server whose names then collide all get a hash suffix. Server names that differ only in `-` and `_` count as the same server: a second one is rejected, and a `mcp.json` server overrides a registered one.\n- `type` is optional. A `command` selects stdio and a `url` selects streamable HTTP. When present, `type` must be `stdio`, `http`, or `streamable-http`.\n- `sse` is rejected. Servers that document an SSE endpoint often also provide streamable HTTP, commonly at `/mcp` instead of `/sse`.\n- `command` is one executable and `args` contains its arguments. It is not a shell command string.\n- `env` and `headers` values can use environment variables such as `${GITHUB_TOKEN}`. They can also run a command with `!command`, but the command must make up the whole value, for example `\"Authorization\": \"!echo Bearer $(gh auth token)\"`.\n- Invalid entries are reported and skipped without preventing other servers from connecting.\n\n`pi mcp add` and `pi mcp remove` cover common changes from a shell. See [MCP commands](cli.md#mcp-commands) for their options.\n\n### Inspect or change a server\n\n`/mcp` lists configured servers with their state, tool count, exposure, and configuration source. Servers that need attention appear first. Select a server to inspect its tools and connection details, reconnect, sign in or out, change exposure, or enable and disable it.\n\nExposure and enabled-state changes are saved to the file that defines the server without replacing unrelated content. In a trusted project, \"Enable in this project\" and \"Disable in this project\" add a project override for a user-level server; later changes to that server are saved to the override. Disabled servers remain listed. Outside the interactive TUI, `/mcp` prints server status; `/mcp login <server>`, `/mcp logout <server>`, and `/mcp reconnect <server>` perform those actions directly.\n\nShell commands work without a session: `pi mcp add`, `pi mcp remove`, `pi mcp list`, `pi mcp login`, and `pi mcp logout`. Shell commands do not load extensions.\n\n### Diagnose connection problems\n\nRun `pi mcp list` to connect to every enabled server and print its state, tools, and errors. It exits with status 1 when an entry is invalid or an enabled server is not connected. `/mcp` shows the full connection error and the tail of stderr from a failed stdio server.\n\nPi reports configuration errors, failed connections, and required sign-ins once after startup. Server logging notifications are appended to `~/.pi/agent/mcp.log` as `<time> [<server>] <level> <logger>: <message>`. The file moves to `mcp.log.1` after it grows past 5 MB.\n\nPi connects every enabled server in the background when a session starts. A server's tools appear once it connects; the `codemode` description does not list them, so it does not change when servers connect. The first prompt waits up to 10 seconds only for servers with `direct` tools, which must be declared in its request. Other servers are waited for when they are needed: a codemode script waits for the servers it names (`mcp__<server>`) and, when it calls `searchTools()` or reads `ALL_TOOLS`, for all of them; `tool_search` and the resource tools also wait for all of them. HTTP network errors and transient statuses (408, 429, and 5xx) are retried twice. A dropped connection is shown as disconnected and reconnects on the next call. When a server announces a changed tool list, new tools are added and withdrawn tools become unreachable.\n\nStopping a stdio server closes its stdin, sends SIGTERM, then sends SIGKILL to its process group. This also stops servers launched through wrappers such as `npx` or `uvx`.\n\n## Migrate configuration from another client\n\nMove the converted entry under `mcpServers` in `mcp.json`, then run `pi mcp list` to validate it.\n\n| Client | Conversion |\n|---|---|\n| Claude Desktop, Claude Code, or Cursor | Copy the existing `mcpServers` entry. |\n| VS Code | Move an entry from the top-level `servers` object and replace `${input:...}` prompts with `${NAME}` environment variables. |\n| Codex | Convert `[mcp_servers.<name>]` TOML fields such as `command`, `args`, `env`, and `url` to JSON. |\n| OpenCode | Convert `\"type\": \"local\"` to a stdio entry, split its `command` array into `command` and `args`, rename `environment` to `env`, and replace `{env:NAME}` with `${NAME}`. Convert `\"type\": \"remote\"` to a URL entry. |\n\n## Authenticate with OAuth\n\nRemote servers that use OAuth, such as Sentry, need no credentials in `mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"sentry\": { \"url\": \"https://mcp.sentry.dev/mcp\" }\n  }\n}\n```\n\nWhen the server rejects an unauthenticated connection, `/mcp` shows that it needs sign-in. Select \"Sign in\", run `/mcp login sentry`, or run `pi mcp login sentry`. Pi opens the authorization page and waits for approval. If the browser runs on another machine, such as over SSH, paste its redirected URL into the sign-in screen. A running session uses the new credentials on its next turn.\n\nPi registers itself with the authorization server, stores tokens in `~/.pi/agent/mcp-auth.json`, and refreshes access tokens when they expire or the server rejects them. If a server later requests additional scope, Pi asks for sign-in again. Signing out deletes the stored credentials.\n\nCredentials belong to a server name and URL. Servers with the same URL under different names, such as one per account, sign in separately; servers with the same name and URL in different `mcp.json` files share one sign-in.\n\nOAuth applies to HTTP servers without an `Authorization` header. For a server that does not support dynamic client registration, configure a registered client:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"clientId\": \"my-client\", \"clientSecret\": \"${EXAMPLE_SECRET}\", \"callbackPort\": 8765 }\n    }\n  }\n}\n```\n\nThe redirect URI must match the registered URI. `callbackPort` uses `http://127.0.0.1:<port>/callback`. To use another URI, set `callbackUrl`; it must use HTTP on `localhost`, `127.0.0.1`, or `[::1]`. Pi sends it exactly as written. When `callbackUrl` omits a port, Pi uses `callbackPort` or a free port and adds it to the URI, as allowed for loopback redirects by RFC 8252. `clientSecret` is optional and can use an environment variable or command.\n\nSet `scope` to a space-separated list for servers that do not advertise their required scopes. Otherwise, Pi requests the advertised scopes. Later scope requests are added to the configured value.\n\nPi registers as `pi`. Some servers only accept registrations from known clients. Set `clientName` to send another name:\n\n```json\n{\n  \"mcpServers\": {\n    \"figma\": { \"url\": \"https://mcp.figma.com/mcp\", \"oauth\": { \"clientName\": \"Claude Code\" } }\n  }\n}\n```\n\nThe name is only sent when Pi registers a client. To register again under a new name, sign out first.\n\nSome authorization servers allow clients by their Client ID Metadata Document URL instead of registering them. Set `clientRegistration` to `cimd` to identify as Pi's document on pi.dev instead of registering:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": { \"url\": \"https://mcp.example.com/mcp\", \"oauth\": { \"clientRegistration\": \"cimd\" } }\n  }\n}\n```\n\nThe client ID is `https://pi.dev/oauth/client.json` with the redirect URI `http://127.0.0.1:<port>/callback`. If the authorization server does not send the `iss` parameter in authorization responses (RFC 9207), Pi uses a document and redirect path specific to the MCP server instead: `https://pi.dev/oauth/<id>/client.json` with `http://127.0.0.1:<port>/callback/<id>`. The authorization server must advertise Client ID Metadata Document support and public clients, or sign-in fails. `cimd` cannot be combined with `clientId` or `clientName`, and a `callbackUrl` must use `localhost` or `127.0.0.1` with the path `/callback`.\n\nPi finds the authorization server through the server's protected resource metadata (RFC 9728) and checks that the authorization server's metadata names the expected issuer (RFC 8414). Some servers advertise the wrong authorization server or none, so sign-in opens a page that does not exist. Set `authServerMetadataUrl` to the metadata document of the right authorization server:\n\n```json\n{\n  \"mcpServers\": {\n    \"example\": {\n      \"url\": \"https://mcp.example.com/mcp\",\n      \"oauth\": { \"authServerMetadataUrl\": \"https://example.okta.com/.well-known/openid-configuration\" }\n    }\n  }\n}\n```\n\nPi uses that document instead of discovery and trusts it as configured, so only point it at a document you trust. The URL must use HTTPS, except on `localhost`, `127.0.0.1`, or `[::1]`.\n\n## Control tool exposure\n\nEach server tool is registered as `mcp__<server>__<tool>`. The server's `exposure` determines how the model reaches it:\n\n| Exposure | Behavior | Typical use |\n|---|---|---|\n| `codemode` (default) | Callable from [`codemode`](cli.md#tools) scripts, but neither declared to the model nor listed in the codemode description. Scripts find tools with `searchTools()`, `describeTool()`, or `ALL_TOOLS`. | General MCP servers, especially when scripts should combine or filter calls. |\n| `deferred` | Not declared until [`tool_search`](cli.md#tools) loads a match for the next model call. | Large servers whose tools should be called directly after discovery. |\n| `direct` | Declared to the model like a built-in tool and also callable from codemode. | Small, frequently used tool sets. |\n| `hidden` | Registered but unreachable. | Servers or tools that should remain unavailable. |\n\n`codemode-deferred` is accepted as an alias for `codemode`.\n\nServers with `codemode` or `deferred` tools are listed in the `mcp_servers` section of the system prompt, with how their tools are reached and one line from the configured `description` or, once connected, from the server instructions. Pi updates the section when a prompt starts, after waiting for servers with `direct` tools. When it changed, for example because a server connected and its summary became available, Pi appends the new section to the conversation instead of changing tool declarations, so earlier messages stay cached. `describeNamespace()` and the `namespace` option of `searchTools()` accept `mcp__dev-radius`, `mcp__dev_radius`, `dev-radius`, or `dev_radius`.\n\nPi activates `codemode` when a server with `codemode` exposure connects. It activates `tool_search` for a server with `deferred` exposure. To make the model see a tool without searching, give it `direct` exposure with `toolExposure`.\n\n`toolExposure` overrides the server exposure for individual tools. Keys are exact server tool names or patterns where `*` matches any characters. Exact names win over patterns; among patterns, the first match wins. A server with `hidden` exposure can expose only selected tools:\n\n```json\n{\n  \"mcpServers\": {\n    \"github\": {\n      \"url\": \"https://api.githubcopilot.com/mcp/\",\n      \"exposure\": \"deferred\",\n      \"toolExposure\": {\n        \"search_code\": \"direct\",\n        \"get_*\": \"codemode\",\n        \"delete_*\": \"hidden\"\n      }\n    }\n  }\n}\n```\n\n`pi mcp list` marks tools whose exposure differs from their server. The Tools view in `/mcp` also shows the effective exposure.\n\nTools with `codemode` or `deferred` exposure can be reached through either indirect mechanism: codemode scripts can call them, and `tool_search` can load them. Codemode calls do not depend on the active tool set, so they remain available after `/tree`, resume, and fork. Tools loaded by `tool_search` are recorded in the transcript and remain declared on that branch.\n\n`--tools` does not remove MCP tools unless one of its entries starts with `mcp__`; `pi --tools read,codemode,'mcp__radius__*'` keeps only the tools of `radius`. `--exclude-tools` accepts the same patterns, and `--no-mcp` disables MCP for one run (see [Tools](cli.md#mcp-tools)).\n\nTo keep `codemode` active without MCP servers, add `\"defaultTools\": [\"+codemode\"]` to [settings](settings.md#tools). To prevent automatic codemode activation, set `\"autoEnableCodemode\": false` beside `mcpServers`. A project value overrides the user-level value. Pi warns once when neither `codemode` nor `tool_search` is active and non-direct tools cannot be called.\n\nText results over 20 KB reach the model with their middle removed around a `…N chars truncated…` marker. The full text is saved to a temporary file named in the result. Codemode scripts receive the complete result and can reduce it before returning output to the model.\n\nCodemode scripts receive the complete MCP `CallToolResult`, including `content`, `structuredContent`, and `isError`. A result with `isError` resolves inside scripts but is reported as an error for direct calls. `image(result.content[0])` forwards an image block. Server instructions are not part of any tool description; scripts read them with `describeNamespace(\"mcp__<server>\")`, which also returns the server's tool names.\n\n## Use resources\n\nWhen a connected server offers [resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources), Pi adds the resource tools used by Codex and OpenCode:\n\n- `list_mcp_resources` lists resources as JSON: `{ server?, resources: [{ server, uri, name, ... }], nextCursor? }`. With `server`, it lists one page; `cursor` continues with the next page. Without `server`, it lists every resource from every server.\n- `list_mcp_resource_templates` lists URI templates for resources the servers do not list directly.\n- `read_mcp_resource` reads a resource by `server` and `uri`. Text reaches the model as text and images as images. Other binary resources are saved to temporary files, and the model receives the path. Scripts receive `{ server, uri, contents }`.\n\nThese tools reach every enabled, non-hidden server with resources. Their exposure is the widest exposure among those servers: `direct`, then `codemode` or `deferred`. Resource links in tool results identify `read_mcp_resource` and the server.\n\nResources for MCP Apps, identified by `ui://` URIs or `text/html;profile=mcp-app`, are omitted because Pi does not render them. Resource icons are also omitted.\n\nReading and listing resources is retried once after a transient HTTP error (408, 429, or 5xx). Tool calls are not retried because the server may already have performed them.\n\n## Permissions\n\nEvery MCP call passes through Pi's tool pipeline. Extension `tool_call` and `tool_result` handlers, including permission gates, therefore apply to MCP tools. Calls made from codemode scripts carry the codemode call ID as `parentToolCallId`.\n\n`pi.getAllTools()` reports the annotations declared by each server: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. Permission extensions can use these hints to decide which calls require confirmation (see [Tool exposure](extensions.md#tool-exposure)). Resource tools are marked read-only.\n\n## Extensions and SDK\n\n### Add servers from extensions\n\nExtensions can add servers for the current session with `pi.registerMcpServer(name, config)`, using the same shape as an `mcpServers` entry (see [MCP servers in extensions](extensions.md#mcp-servers)). Registered servers connect like configured servers and appear in `/mcp` with the extension as their source.\n\nChanges to enabled state or exposure apply only to the current session. A file-configured server with the same name takes precedence, and `/mcp` lists the overridden registration. `pi mcp` shell commands do not load extensions and only see file-configured servers.\n\n### Replace the built-in MCP support\n\nAn installed extension that registers `/mcp`, such as `pi-mcp-adapter`, replaces the built-in MCP support for sessions. Pi then does not read `mcp.json` or connect its servers in a session, and `/mcp` belongs to the extension. Remove the extension to restore the built-in behavior. To disable built-in MCP support without a replacement, disable `mcp` under Built-in in `pi config`, or set `\"extensions\": [\"-builtin:mcp\"]` in [settings](settings.md#resources). `--no-mcp` disables it for one run.\n\nAn extension that registers `codemode` or `tool_search` similarly replaces the built-in tool with that name. Pi activates the replacement for MCP servers and waits for the servers its calls need only when it declares [`reachesTools`](extensions.md#tool-exposure). Shell-level `pi mcp` commands always use the built-in implementation.\n\n### Use MCP from the SDK\n\nSDK sessions do not load built-in extensions. Add the MCP extension, the codemode extension for `codemode` servers, and the tool-search extension for `deferred` servers to the resource loader. See [Codemode and MCP](sdk.md#codemode-mcp).\n\n<!-- File: packages/coding-agent/docs/message-types.md (Tokens: ~1715 | Category: anthropic-skill) -->\n# Message Types\n\nPi uses `AgentMessage` values in SDK state, lifecycle events, RPC responses, and persisted session message entries. This page defines those shared messages and their content blocks.\n\nMessage timestamps are Unix timestamps in milliseconds. They are different from the ISO 8601 timestamps on [session entries](session-format.md#entry-base).\n\nSource definitions:\n\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/ai/src/types.ts) defines provider-facing messages and content blocks.\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts) defines the extensible `AgentMessage` union.\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/messages.ts) adds coding-agent message roles.\n\n## Content blocks\n\n### TextContent\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n  textSignature?: string;\n}\n```\n\n`textSignature` contains provider-specific message metadata. Treat it as opaque.\n\n### ImageContent\n\n```typescript\ninterface ImageContent {\n  type: \"image\";\n  data: string;\n  mimeType: string;\n}\n```\n\n`data` is base64-encoded image data. `mimeType` identifies its media type, such as `image/png` or `image/jpeg`.\n\n### ThinkingContent\n\n```typescript\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n  thinkingSignature?: string;\n  redacted?: boolean;\n}\n```\n\nThinking signatures contain provider-specific replay data. Treat them as opaque. A redacted block can have no visible thinking text while retaining an encrypted payload in `thinkingSignature`.\n\n### ToolCall\n\n```typescript\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n  thoughtSignature?: string;\n  namespace?: string;\n}\n```\n\n`thoughtSignature` is provider-specific. `namespace` identifies an OpenAI Responses namespace for dynamically loaded or namespaced tools.\n\n## Usage\n\nAssistant messages always contain usage. Tool results can contain usage when the tool performed nested model work.\n\n```typescript\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  cacheWrite1h?: number;\n  reasoning?: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\nWhen present, `reasoning` is already included in `output`; do not add it again. `cacheWrite1h` is the subset of `cacheWrite` written with one-hour retention.\n\n## Base messages\n\n### SystemMessage\n\n```typescript\ninterface SystemMessage {\n  role: \"system\";\n  content: string | TextContent[];\n  sections?: Record<string, string | null>;\n  toolsAdded?: Tool[];\n  toolsRemoved?: ToolReference[];\n  timestamp: number;\n}\n```\n\nThe leading system message declares the initial prompt and tools. Later system messages can append instructions, replace or remove named prompt sections, and add or remove tools. Replaying them in order yields the current state.\n\n### UserMessage\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;\n}\n```\n\n### AssistantMessage\n\n```typescript\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  responseModel?: string;\n  responseId?: string;\n  providerThinkingLevel?: string;\n  thinkingLevel?: ModelThinkingLevel;\n  diagnostics?: AssistantMessageDiagnostic[];\n  usage: Usage;\n  stopReason: \"pending\" | \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\" | \"deferred\";\n  deferred?: DeferredHandle;\n  errorMessage?: string;\n  rawStopReason?: string;\n  endTurn?: boolean;\n  timestamp: number;\n}\n```\n\n`responseModel` records a concrete provider response model when it differs from the requested model. `responseId`, `providerThinkingLevel`, `thinkingLevel`, `diagnostics`, and `rawStopReason` preserve provider or runtime details.\n\n`\"pending\"` is used for a partial assistant message while it streams. The completed message in `message_end` has a terminal stop reason, and Pi does not persist `\"pending\"` assistant messages in session JSONL.\n\nA `\"deferred\"` response has a `DeferredHandle` with the provider data needed to retrieve it:\n\n```typescript\ninterface DeferredHandle {\n  provider: string;\n  modelId: string;\n  api: string;\n  id: string;\n  expiresAt?: number;\n  pollAfterMs?: number;\n  data?: JsonValue;\n}\n```\n\n### ToolResultMessage\n\n```typescript\ninterface ToolResultMessage<TDetails = any> {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: TDetails;\n  usage?: Usage;\n  nestedCalls?: NestedToolCalls;\n  isError: boolean;\n  timestamp: number;\n}\n```\n\n`details` is tool-specific. Optional `usage` reports nested model work performed by the tool and contributes to full-session statistics, but it is not part of the main model-call usage. `nestedCalls` records bounded metadata about calls this tool made to other tools:\n\n```typescript\ninterface NestedToolCalls {\n  calls: NestedToolCallRecord[];\n  complete: boolean;\n}\n\ninterface NestedToolCallRecord {\n  id: string;\n  name: string;\n  arguments?: JsonObject;\n  argumentsBytes?: number;\n  status: \"ok\" | \"error\" | \"unfinished\";\n  durationMs?: number;\n  error?: string;\n}\n```\n\n## Coding-agent messages\n\nThe coding-agent package extends `AgentMessage` with four roles.\n\n### BashExecutionMessage\n\nCreated by direct shell commands, including the RPC [`bash`](rpc-commands.md#bash) command. It is not an LLM tool result.\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;\n  timestamp: number;\n}\n```\n\nUnless `excludeFromContext` is true, Pi converts this message to user-role text before the next model request.\n\n### CustomMessage\n\nCreated when an extension sends a context message.\n\n```typescript\ninterface CustomMessage<T = unknown> {\n  role: \"custom\";\n  customType: string;\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;\n  details?: T;\n  timestamp: number;\n}\n```\n\nPi converts its content to a user message for model requests. `display` controls terminal rendering; `details` is not sent to the model.\n\n### BranchSummaryMessage\n\n```typescript\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string | null;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `branch_summary` entry.\n\n### CompactionSummaryMessage\n\n```typescript\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\nPi creates this context message from a persisted `compaction` entry.\n\n## AgentMessage union\n\nIn the coding agent, the union is equivalent to:\n\n```typescript\ntype AgentMessage =\n  | SystemMessage\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\nAt the lower-level agent package, `AgentMessage` is `Message | CustomAgentMessages[keyof CustomAgentMessages]`. Applications can add roles through TypeScript declaration merging, so consumers should tolerate unknown custom roles when they accept messages from an augmented host.\n\n<!-- File: packages/coding-agent/docs/models-configuration.md (Tokens: ~4898 | Category: anthropic-skill) -->\n# Models Configuration\n\n<!-- This file is generated from TypeScript source. Do not edit it manually; run npm run generate:references. -->\n\nThis reference lists the fields of `models.json`. See [Configure a compatible endpoint](models.md#configure-a-compatible-endpoint) for examples and [Configuration](configuration.md#agent-directory) for the file location.\n\nAdd `\"$schema\": \"https://pi.dev/schemas/models.schema.json\"` to `models.json` for editor validation and completion.\n\n## Providers\n\nEach key under `providers` is a provider ID. A built-in provider ID changes that provider. Any other ID adds a provider, which needs `baseUrl` and an `api` at provider or model level.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name. |\n| `baseUrl` | string |  | API base URL. Also applies to the provider's built-in models. |\n| `apiKey` | string |  | Literal key, \"$NAME\" environment interpolation, or \"!command\" output. |\n| `api` | string |  | API for the provider's models, such as \"openai-completions\", \"openai-responses\", \"anthropic-messages\", or \"google-generative-ai\". |\n| `oauth` | `\"radius\"` |  | OAuth login flow. Requires baseUrl. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n| `authHeader` | boolean | `false` | Whether to send the resolved API key as Authorization: Bearer. |\n| `models` | [`object[]`](#models) |  |  |\n| `modelOverrides` | [object](#model-overrides) |  |  |\n\n## Models\n\nEach `models` entry adds a model to the provider. An entry whose `id` matches an existing model replaces it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `id` | string |  | Required. Model ID sent to the API. |\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `api` | string |  | Overrides the provider api. |\n| `baseUrl` | string |  | Overrides the provider baseUrl. |\n| `reasoning` | boolean | `false` | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` | `[\"text\"]` |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number | `128000` | Context window in tokens. |\n| `maxTokens` | number | `16384` | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Model overrides\n\nEach `modelOverrides` key is the ID of a built-in or extension model. Unknown IDs are ignored. Object fields, such as `cost`, merge with the model's value field by field; other fields replace it.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string |  | Display name, also matched by --model patterns. |\n| `reasoning` | boolean |  | Whether the model supports thinking. |\n| `thinkingLevelMap` | object |  | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |\n| `input` | `(\"text\" \\| \"image\")[]` |  |  |\n| `inputLimits` | [object](#input-limits) |  |  |\n| `cost` | object |  |  |\n| `cost.input` | number |  | Input cost in USD per million tokens. |\n| `cost.output` | number |  | Output cost in USD per million tokens. |\n| `cost.cacheRead` | number |  | Cache-read cost in USD per million tokens. |\n| `cost.cacheWrite` | number |  | Cache-write cost in USD per million tokens. |\n| `cost.tiers` | `object[]` |  | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |\n| `cost.tiers[].inputTokensAbove` | number |  | Required. Use this tier when total request input exceeds this token count. |\n| `cost.tiers[].input` | number |  | Required. Input cost in USD per million tokens. |\n| `cost.tiers[].output` | number |  | Required. Output cost in USD per million tokens. |\n| `cost.tiers[].cacheRead` | number |  | Required. Cache-read cost in USD per million tokens. |\n| `cost.tiers[].cacheWrite` | number |  | Required. Cache-write cost in USD per million tokens. |\n| `promptCache` | object |  | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |\n| `contextWindow` | number |  | Context window in tokens. |\n| `maxTokens` | number |  | Maximum output tokens. |\n| `samplingParams` | object |  | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |\n| `samplingParamsByThinkingLevel` | object |  | samplingParams per thinking level, merged over samplingParams. |\n| `headers` | object |  | Request headers. Values resolve like apiKey. |\n| `compat` | [object](#compatibility) |  | Provider and model compatibility overrides. |\n\n## Input limits\n\nSee [Describe model input and caching](models.md#describe-model-input-and-caching).\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `maxRequestBytes` | integer |  | Maximum serialized provider request size in bytes. |\n| `images` | object |  |  |\n| `images.resize` | object |  | Cache-safe resize profile applied before a new image enters conversation history. |\n| `images.resize.maxWidth` | integer |  |  |\n| `images.resize.maxHeight` | integer |  |  |\n| `images.resize.maxBytes` | integer |  | Maximum base64-encoded payload size in bytes. |\n| `images.resize.jpegQuality` | integer |  |  |\n| `images.maxPerMessage` | integer |  | Maximum images accepted in one provider message. |\n| `images.maxPerRequest` | integer |  | Maximum images accepted across one provider request. |\n\n## Compatibility\n\n`compat` describes verified differences from an API's standard request and response behavior. Model values override provider values per field. The fields that apply depend on the model's `api`.\n\n### `openai-completions`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStore` | boolean |  | Whether the provider supports the store field. Default: auto-detected from URL. |\n| `supportsDeveloperRole` | boolean |  | Whether the provider supports the developer role instead of system. Default: auto-detected from URL. |\n| `supportsReasoningEffort` | boolean |  | Whether the provider supports reasoning_effort. Default: auto-detected from URL. |\n| `supportsUsageInStreaming` | boolean | `true` | Whether the provider supports stream_options.include_usage for token usage in streaming responses. |\n| `supportsFinishReason` | boolean | `true` | Whether streamed responses include finish_reason. When false, pi infers stop or toolUse when the stream ends. |\n| `maxTokensField` | `\"max_completion_tokens\" \\| \"max_tokens\"` |  | Which field to use for max tokens. Default: auto-detected from URL. |\n| `requiresToolResultName` | boolean |  | Whether tool results require the name field. Default: auto-detected from URL. |\n| `requiresAssistantAfterToolResult` | boolean |  | Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. |\n| `requiresThinkingAsText` | boolean |  | Whether thinking blocks must be converted to text blocks with &lt;thinking> delimiters. Default: auto-detected from URL. |\n| `requiresReasoningContentOnAssistantMessages` | boolean |  | Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. |\n| `thinkingFormat` | `\"openai\" \\| \"openrouter\" \\| \"deepseek\" \\| \"together\" \\| \"baseten\" \\| \"zai\" \\| \"qwen\" \\| \"chat-template\" \\| \"qwen-chat-template\" \\| \"string-thinking\" \\| \"ant-ling\"` |  | Format for reasoning or thinking parameters. When omitted, Pi auto-detects the format from the provider URL. \"openai\" uses reasoning_effort, \"openrouter\" uses reasoning.effort, \"deepseek\" uses thinking.type plus reasoning_effort when supported, \"together\" uses reasoning.enabled plus reasoning_effort when supported, \"baseten\" uses configurable chat_template_args plus reasoning_effort when supported, \"zai\" uses thinking.type, \"qwen\" uses top-level enable_thinking, \"qwen-chat-template\" uses chat_template_kwargs.enable_thinking and preserve_thinking, \"chat-template\" uses configurable chat_template_kwargs, \"string-thinking\" uses top-level thinking, and \"ant-ling\" uses reasoning.effort only when the mapped effort is non-null. |\n| `chatTemplateKwargs` | object |  | Kwargs sent as chat_template_kwargs when thinkingFormat is \"chat-template\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `chatTemplateArgs` | object |  | Arguments sent as chat_template_args when thinkingFormat is \"baseten\". Use $var with \"thinking.enabled\", \"thinking.effort\", or \"thinking.budget\" for pi-controlled values. |\n| `openRouterRouting` | object |  | OpenRouter provider routing preferences. Controls which upstream providers OpenRouter routes requests to. Sent as the provider field in the OpenRouter API request body. See https://openrouter.ai/docs/guides/routing/provider-selection. |\n| `vercelGatewayRouting` | object |  | Vercel AI Gateway routing preferences. Controls which upstream providers the gateway routes requests to. See https://vercel.com/docs/ai-gateway/models-and-providers/provider-options. |\n| `zaiToolStream` | boolean | `false` | Whether z.ai supports top-level tool_stream for streaming tool call deltas. |\n| `thinkingTokenBudgetField` | `\"thinking_token_budget\" \\| \"thinking_budget\" \\| \"thinking_budget_tokens\"` |  | Top-level request field used to cap reasoning tokens from thinkingBudgets. Reasoning and the answer share max_tokens on these endpoints. \"thinking_token_budget\" is vLLM, \"thinking_budget\" is Qwen, DashScope, or SGLang, and \"thinking_budget_tokens\" is llama.cpp. Off by default and not set on the generated catalog. |\n| `supportsThinkingTokenBudget` | boolean | `false` | Alias for thinkingTokenBudgetField: \"thinking_token_budget\" (vLLM). Prefer thinkingTokenBudgetField. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether the provider supports OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system or developer messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `supportsMidConvoToolAdditions` | boolean | `false` | Whether system messages can introduce additional tools mid-conversation. Requires supportsMidConvoSystemMessages. The generated catalog enables this for capable models. |\n| `supportsStrictMode` | boolean | `false` | Whether the provider supports the strict field in tool definitions. Generated capable models enable it explicitly. |\n| `cacheControlFormat` | `\"anthropic\"` |  | Cache control convention for prompt caching. Anthropic applies cache_control markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send session-affinity data from options.sessionId. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id, x-client-request-id, and x-session-affinity; openai-nosession sends x-client-request-id and x-session-affinity; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean |  | Whether the provider supports long prompt cache retention (prompt_cache_retention: \"24h\" or Anthropic-style cache_control.ttl: \"1h\", depending on format). Default: auto-detected from provider and URL. |\n| `vllmPriority` | number |  | vLLM scheduler priority sent as the top-level priority request field. Lower values are handled earlier and the server default is 0. Only meaningful with --scheduling-policy priority. Off by default and not set on the generated catalog. |\n\n### `openai-responses`\n\nAlso applies to `azure-openai-responses` and `openai-codex-responses`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsDeveloperRole` | boolean | `true` | Whether the provider supports the developer role instead of system. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts developer or system messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |\n| `sessionAffinityFormat` | `\"openai\" \\| \"openai-nosession\" \\| \"openrouter\"` |  | Session-affinity header format. openai sends session_id and x-client-request-id; openai-nosession sends x-client-request-id; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports long prompt cache retention. This uses prompt_cache_options.ttl: \"30m\" on GPT-5.6+ and prompt_cache_retention: \"24h\" on earlier models. |\n| `supportsStrictMode` | boolean |  | Whether the provider supports strict JSON-schema function tools. Defaults are API-specific; generated OpenAI models enable it explicitly. |\n| `supportsOpenAIGrammarTools` | boolean | `false` | Whether to emit OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |\n| `supportsAdditionalTools` | boolean | `false` | Whether the model supports message-anchored additional_tools input items. |\n| `supportsToolSearch` | boolean | `false` | Whether the model supports client-executed tool search for transcript-anchored additions. |\n| `supportsExplicitPromptCacheMode` | boolean | `false` | Whether the model accepts prompt_cache_options. Older OpenAI models reject the parameter. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether thinking level changes are sent as configuration_update input items instead of changing reasoning.effort, so a change keeps the prompt cache. Only used by the openai-responses API, and ignored with Sign in with ChatGPT on the openai provider. |\n| `supportsMaxOutputTokens` | boolean | `true` | Whether the provider accepts max_output_tokens. Some Codex-protocol gateways reject it. |\n\n### `anthropic-messages`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsEagerToolInputStreaming` | boolean | `true` | Whether the provider accepts per-tool eager_input_streaming. When false, the Anthropic provider omits tools[].eager_input_streaming and sends the legacy fine-grained-tool-streaming-2025-05-14 beta header for tool-enabled requests. |\n| `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports Anthropic long cache retention through cache_control.ttl. |\n| `sendSessionAffinityHeaders` | boolean |  | Whether to send x-session-affinity from options.sessionId when caching is enabled. Required for providers like Fireworks that use session affinity for prompt cache routing; requests to the same replica maximize cache hits. Default: true for OpenRouter endpoints, false otherwise. |\n| `sessionAffinityFormat` | `\"openrouter\"` |  | Session-affinity format. openrouter sends x-session-id; when unset, sends x-session-affinity. |\n| `supportsCacheControlOnTools` | boolean | `true` | Whether the provider supports Anthropic-style cache_control markers on tool definitions. When false, cache_control is omitted from tool parameters. Some Anthropic-compatible providers, such as Fireworks, do not support this field on tools and may reject or ignore it. |\n| `supportsTemperature` | boolean | `true` | Whether the model accepts the Anthropic temperature request field. Claude Opus 4.7+ rejects non-default values. |\n| `forceAdaptiveThinking` | boolean | `false` | Whether to force adaptive thinking (thinking.type: adaptive plus output_config.effort) regardless of model ID. Built-in models that require adaptive thinking set this in generated metadata. Custom Anthropic-compatible providers can set this to true for any model whose upstream requires the adaptive format. Set false to opt out on overridden built-in models. |\n| `allowEmptySignature` | boolean | `false` | Whether to replay empty thinking signatures instead of converting thinking to text. |\n| `supportsStrictTools` | boolean | `false` | Whether the provider supports Anthropic strict tool schemas. Generated Anthropic models enable it explicitly. |\n| `supportsMidConvoEffort` | boolean | `false` | Whether the exact model transport supports effort-only system messages and thinking binding controls. |\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system-role messages inside the conversation. When false, later system messages are folded into the top-level system prompt. |\n| `supportsMidConvoToolChanges` | boolean | `false` | Whether the exact model accepts mid-conversation tool_addition and tool_removal blocks. Requires supportsMidConvoSystemMessages. |\n| `allowedFallbackModels` | `object[]` |  | Models Anthropic accepts for server-side refusal fallback, with local pricing metadata for returned fallback responses. When absent or empty, callers must omit fallbacks; Anthropic rejects the field for models with no permitted fallback targets. |\n\n### `bedrock-converse-stream`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsStrictMode` | boolean | `false` | Whether the model supports Bedrock strict tool schemas. |\n\n### `mistral-conversations`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system messages after the conversation has started. When false, later system messages are folded into the leading system message. |\n\n### `typesafe-system-one`\n\nAlso applies to `cloudflare-workers-ai-system-one`.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `imageInput` | `\"assets\" \\| \"images\" \\| \"state-image-urls\"` |  | How the service takes classifier images. \"assets\" sends TypeSafe's assets map (PNG and JPEG) and references the images from the state, as TypeSafe and Jev on Workers AI expect. \"images\" sends Clef's images list of data URLs (PNG, JPEG, and WebP, at most four), as Clef on Workers AI expects. \"state-image-urls\" places image_url parts before the state, as OpenRouter expects; OpenRouter drops assets without an error. Auto-detected from the provider, baseUrl, and model ID. Classifier models read it from the provider-level compat; models and modelOverrides entries apply to chat models only. |\n| `sendImageOptInHeader` | boolean |  | Whether requests with images send the TypeSafe-Danger-Zone-Images: true header, which TypeSafe's own API requires for its experimental image input. Defaults to true for the typesafe-system-one API unless the model is detected as OpenRouter. |\n\n<!-- File: packages/coding-agent/docs/models.md (Tokens: ~3306 | Category: anthropic-skill) -->\n# Choose a Model\n\nFor a built-in provider, start with `/login`, then choose a model with `/model`. Use custom model configuration only when Pi does not already include the provider or endpoint you need.\n\n## Choose a connection\n\n| What you have | Recommended setup |\n|---|---|\n| A supported subscription | Sign in through `/login` |\n| A provider API key | Store it through `/login` or set its environment variable |\n| A local GGUF model | Connect Pi to the llama.cpp router |\n| An OpenAI-, Anthropic-, or Google-compatible endpoint | Add it to `models.json` |\n| A provider with a custom protocol or authentication flow | Build or install a provider extension |\n\nBrowse the [model catalog](https://pi.dev/models) for current providers, model IDs, capabilities, context limits, and pricing. Pi starts with its bundled catalog and can overlay newer catalog data from pi.dev. Cached catalog data remains available offline; run `pi update --models` to force a refresh.\n\n## Authenticate\n\nRun `/login` and select a provider. Pi stores credentials in [`auth.json`](configuration.md#agent-directory). Run `/logout` to remove stored credentials for a provider.\n\nYou can instead provide an API key through the provider's environment variable. This is useful in CI and other environments where Pi should not write credentials. [Providers](providers.md) lists the variables and provider-specific setup.\n\nWhen several credential sources are configured, Pi uses a runtime `--api-key` first, then a stored `auth.json` credential, an `apiKey` from `models.json`, and finally the provider's environment variables or ambient cloud credentials. Provider extensions can define their own authentication behavior.\n\nKeep `auth.json` and any credential commands private. Project settings and extensions can execute inside the Pi process after you trust a project. Review [Security](security.md) before loading configuration from an untrusted directory.\n\n## Select a model\n\nRun `/model` to search available models. The picker shows models whose providers have usable authentication. Press `Ctrl+S` on a model to save it as the default for new sessions.\n\nRun `/thinking` to select the thinking level for the current model. Press `Ctrl+S` there to save the startup level. Pi limits the choices to levels supported by the selected model.\n\n`Ctrl+P` cycles through available models. Use `/scoped-models` to control that cycle and save the selection, or configure model patterns through [Settings](settings.md#model-cycling).\n\nA session records model and thinking-level changes. Resuming the session restores them without changing defaults for new sessions.\n\n## Connect local models\n\nPi integrates directly with the llama.cpp router. The router discovers GGUF files and loads models on demand. Pi's `/llama` command manages the router, while `/model` selects one of its loaded models.\n\nFollow [Local Models with llama.cpp](llama-cpp.md) for server startup, model layout, downloads, and connection troubleshooting.\n\nFor Ollama, LM Studio, vLLM, SGLang, and other compatible servers, [configure a compatible endpoint](#configure-a-compatible-endpoint) in `models.json`.\n\n## Configure a compatible endpoint\n\nUse [`models.json`](configuration.md#agent-directory) when an endpoint speaks an API Pi already supports. This includes most Ollama, LM Studio, vLLM, SGLang, and proxy deployments.\n\n```json\n{\n  \"$schema\": \"https://pi.dev/schemas/models.schema.json\",\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\nThe dummy key makes the model available to Pi; Ollama ignores it. For an authenticated endpoint, `apiKey` and header values can use `$NAME` or `${NAME}` environment interpolation, a literal value, or a leading `!command`. `$$` and `$!` produce a literal `$` and `!`. Commands in `models.json` run at request time and are not cached by Pi.\n\nOpening `/model` reloads the file. A `models` entry adds or replaces a model with the same ID on that provider. Use `modelOverrides` to change metadata for an existing built-in or extension-provided model without replacing the provider's model list. Unknown override IDs are ignored.\n\nTo route a built-in provider through a proxy, set only its `baseUrl`. Its models and authentication stay unchanged:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": { \"baseUrl\": \"https://proxy.example.com/v1\" }\n  }\n}\n```\n\n[Models Configuration](models-configuration.md) lists every field.\n\n### Describe model input and caching\n\nUse `inputLimits.images.resize` to control how Pi encodes new image attachments, `read` results, and tool-result images before storing them in conversation history:\n\n```json\n{\n  \"id\": \"vision-model\",\n  \"input\": [\"text\", \"image\"],\n  \"inputLimits\": {\n    \"images\": {\n      \"resize\": {\n        \"maxWidth\": 1568,\n        \"maxHeight\": 1568,\n        \"maxBytes\": 524288,\n        \"jpegQuality\": 75\n      }\n    }\n  }\n}\n```\n\n`maxBytes` limits the base64-encoded payload. Omitted resize fields use conservative defaults of 2000 by 2000 pixels, 4.5 MiB encoded, and JPEG quality 80. Images are encoded once; changing models does not rewrite historical images. The catalog can also describe hard request limits with `inputLimits.maxRequestBytes`, `images.maxPerMessage`, and `images.maxPerRequest`, but Pi does not yet rewrite or reject history based on them.\n\n<a id=\"prompt-cache-lifetimes\"></a>\n\nUse `promptCache` to declare the provider's best-effort cache lifetime in seconds for the `short` or `long` retention tier:\n\n```json\n{ \"id\": \"claude-sonnet-5\", \"promptCache\": { \"short\": 300, \"long\": 3600 } }\n```\n\nChoose the conservative end of any published range. A model without a lifetime for the active tier is not eligible for cache warming. A `modelOverrides` entry can set `inputLimits` or `promptCache` for a built-in or extension model, including a model accessed through a validated proxy. See [`cacheWarming`](settings.md#model-and-thinking).\n\n### Configure sampling by thinking level\n\nOpenAI-compatible APIs support free-form `samplingParams` model defaults and `samplingParamsByThinkingLevel` overrides. The latter uses Pi thinking-level keys (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`), not provider values from `thinkingLevelMap`:\n\n```json\n{\n  \"id\": \"qwen-thinking-model\",\n  \"reasoning\": true,\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95\n  },\n  \"samplingParamsByThinkingLevel\": {\n    \"off\": {\n      \"temperature\": 0.7,\n      \"top_p\": 0.8\n    },\n    \"high\": {\n      \"top_k\": 20\n    }\n  }\n}\n```\n\nPi first clamps unsupported thinking levels, then merges model `samplingParams`, the effective level's override, and request-level `samplingParams` in that order. Later values win per key. Missing levels inherit the model defaults. `modelOverrides` merges per-level entries per key with the base model. These fields apply only to `openai-completions`, `openai-responses`, and `azure-openai-responses`; other APIs ignore them.\n\nCompatibility settings should describe verified differences in the endpoint's request or response behavior. Do not enable them based only on an endpoint advertising OpenAI or Anthropic compatibility.\n\n## Use classifier models\n\nClassifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef, Clef Flash, and Clef Omni models from Workers AI and OpenRouter, and OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions):\n\n| Provider | Model IDs | Authentication |\n|---|---|---|\n| `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |\n| `openrouter` | `typesafe/jev-1.13`, `typesafe/jev-1.14`, `~typesafe/jev-latest`, `cloudflare/clef`, `cloudflare/clef-flash`, `cloudflare/clef-omni` | `OPENROUTER_API_KEY` or `/login` |\n| `cloudflare-workers-ai` | `typesafe/jev`, `typesafe/jev-1.14.0`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`, `@cf/cloudflare/clef-omni` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |\n| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |\n| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |\n| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |\n\nChat models on a [llama.cpp router](llama-cpp.md#classification) are also listed as classifier models.\n\nOpenAI's Decisions API needs an API key. Sign in with ChatGPT credentials do not work with it, so `gpt-6-luna` is not listed as available while `openai` is logged in through `/login`, even when `OPENAI_API_KEY` is set; log out of `openai` to use the key. The API rejects inputs above 922K tokens, but requests that run longer than about five seconds, currently above roughly 600K input tokens, fail with a gateway timeout.\n\nClassifier models do not appear in `/model`. The model reaches them through the [`codemode`](cli.md#enable-codemode) tool, which is off unless an MCP server turned it on. Enable it with `\"defaultTools\": [\"+codemode\"]` in [settings](settings.md#tools). Scripts then list classifier models with `models.getAvailableOfType(\"classifier\")` and call `models.classify(model, { state, questions })`:\n\n```js\nconst jev = await models.getModelOfType(\"classifier\", \"typesafe\", \"jev-latest\");\nconst result = await models.classify(jev, {\n  state: { message: \"The change works, thanks.\" },\n  questions: {\n    approved: {\n      type: \"bool\",\n      instructions: \"Does the user approve of the result?\",\n      criteria: { true: \"Approval\", false: \"No approval\" },\n    },\n  },\n});\nreturn result.answers;\n```\n\n[Codemode](codemode.md#classify) describes the question and answer types.\n\nClassifier models whose `input` includes `\"image\"` also judge images passed in `images`; other classifier models return an error for them. Image input works with GPT-6 Luna, with Jev from `typesafe` and `cloudflare-workers-ai`, with Clef from `cloudflare-workers-ai`, and with the OpenRouter models whose catalog lists image input. Jev takes PNG and JPEG images, and image input is experimental on TypeSafe's own API. Clef takes PNG, JPEG, and WebP images, at most four per request. Jev from `vercel-ai-gateway` and `opencode` takes text only.\n\nEach System One service takes images in its own request format, set by the model's `compat.imageInput`. Built-in models set it, so overriding a provider's `baseUrl` keeps it. A custom provider that proxies another service's classifiers sets it in its provider-level `compat` in `models.json` (see [System One compatibility](models-configuration.md#typesafe-system-one)). OpenRouter in particular answers requests in TypeSafe's format without an error but drops the images.\n\nWhen the service reports token counts, as all System One services do, `result.usage` carries them with their cost. Pi adds the usage of a script's classifier calls to the `codemode` tool result, so it counts toward the session cost in the footer and `/session`. The cost uses the model's catalog price; models without one, such as TypeSafe's direct `jev-latest`, report tokens at no cost.\n\nExtensions call classifiers through `ctx.modelRegistry.classify()`, without codemode. [Virtual models](virtual-models.md#route-requests) can use them to route requests; see the `jev-router.ts` example.\n\n## Use image models\n\nImage models generate images from a prompt and optional input images. Pi lists OpenRouter's image models, such as `google/gemini-2.5-flash-image` and `black-forest-labs/flux.2-pro`, under the `openrouter` provider; they use the same `OPENROUTER_API_KEY` or `/login` credential as its chat models.\n\nLike classifier models, image models do not appear in `/model`; the model reaches them through the [`codemode`](cli.md#enable-codemode) tool. Scripts list them with `models.getAvailableOfType(\"image\")` and call `models.generateImages(model, { input })`. The result's `output` holds base64 image blocks, which `image()` attaches to the `codemode` result so the model sees them:\n\n```js\nconst painter = await models.getModelOfType(\"image\", \"openrouter\", \"google/gemini-2.5-flash-image\");\nconst result = await models.generateImages(painter, {\n  input: [{ type: \"text\", text: \"A red fox in the snow, watercolor\" }],\n});\nif (result.stopReason !== \"stop\") return result.errorMessage;\nfor (const block of result.output) if (block.type === \"image\") image(block);\n```\n\n`input` can also contain `{ type: \"image\", data, mimeType }` blocks to edit or use as references. Pi adds the usage of a script's image calls to the `codemode` tool result, like classifier calls. Generated images are not saved to disk. [Codemode](codemode.md#generate-images) describes the full API.\n\nExtensions generate images through `ctx.modelRegistry.generateImages()`, without codemode.\n\n## Add a custom provider\n\nUse an extension when the provider needs custom streaming, model discovery, or authentication behavior. See [Custom Providers](custom-provider.md) for the extension workflow.\n\n## Troubleshooting\n\n### A model does not appear\n\nConfirm that its provider has usable authentication. Custom models can load from `models.json` but remain unavailable in `/model` until Pi can resolve credentials. For llama.cpp, only models currently loaded by the router appear.\n\n### Authentication works in one shell only\n\nCheck whether the key came from an environment variable rather than `auth.json`. Environment variables must be present in the process that starts Pi.\n\n### Sign-in opens a browser on a remote machine\n\nComplete the provider's headless authentication flow when available. Some providers let you paste the final redirect URL or authorization code back into Pi. See [Authenticate interactively](providers.md#authenticate-interactively).\n\n### A compatible endpoint rejects requests\n\nCheck its API type and compatibility settings in `models.json`. The upstream server must support the corresponding request fields and behavior.\n\n<!-- File: packages/coding-agent/docs/nvidia-rtx-llama-cpp.md (Tokens: ~1220 | Category: anthropic-skill) -->\n# Run Pi with NVIDIA RTX and llama.cpp\n\n[NVIDIA Local AI Inference Recipes](https://nvidia.github.io/local-ai-inference-recipes/index.html) provides llama.cpp settings tuned for specific NVIDIA hardware and models. Each recipe gives a single-model `llama-server` command. Pi works with the llama.cpp router, which can serve and manage several models, so this guide converts the recipe command into a router preset.\n\nThe examples below use Windows PowerShell. See [Local Models with llama.cpp](llama-cpp.md) for Pi's complete router and model-management reference.\n\n## Before you begin\n\n1. Open the [recipe catalog](https://nvidia.github.io/local-ai-inference-recipes/index.html), select your hardware, operating system, and model, then choose **llama.cpp**. Check the prerequisites and memory requirements. Keep the recipe's launch command for the conversion below.\n2. Install llama.cpp with the recipe's setup instructions. The recipe's settings depend on its build, for example CUDA support or speculative decoding options.\n3. Run `llama-server --help` and check that it lists `--models-preset`. Without it, the build cannot load the recipe as a router preset. Use a newer build that still meets the recipe's requirements.\n4. Install Pi with the [Quickstart](quickstart.md). On native Windows, also see [Run Pi on Windows](windows.md).\n\n## Convert the recipe to a preset\n\nA preset lists the settings for one or more models. Each section name becomes the model ID shown in Pi. Save the preset next to `llama-server`, or pass its full path when starting the router.\n\nConvert each option from the recipe command into one preset line:\n\n- Remove the leading dashes: `--ctx-size 65536` becomes `ctx-size = 65536`.\n- Write options without a value as `true`: `--jinja` becomes `jinja = true`.\n- Remove quotes and PowerShell line-continuation backticks around values.\n- Leave out `--host` and `--port`. They belong on the router command line.\n- Add `load-on-startup = true` so the router loads the model when it starts.\n- Replace short model-file options with their long preset keys:\n\n| Recipe option | Preset key |\n|---|---|\n| `-hf` | `hf-repo` |\n| `-hff` | `hf-file` |\n| `-hfrd` | `hf-repo-draft` |\n| `--spec-draft-model` | `spec-draft-model` |\n\nFor example, this recipe command:\n\n```powershell\nllama-server.exe -hf \"unsloth/Qwen3.8-27B-GGUF\" `\n  -hff \"Qwen3.8-27B-UD-Q4_K_M.gguf\" `\n  -hfrd \"z-lab/Qwen3.8-27B-DFlash2-GGUF\" `\n  --spec-draft-model \"Qwen3.8-27B-DFlash2-Q4_K_M.gguf\" `\n  --no-mmproj `\n  --host 127.0.0.1 `\n  --port 9931 `\n  --batch-size 4096 `\n  --ubatch-size 1024 `\n  --ctx-size 65536 `\n  --parallel 1 `\n  -ngl 999 `\n  --jinja `\n  --chat-template-kwargs '{\"preserve_thinking\":true}' `\n  --spec-type draft-dflash `\n  --spec-draft-n-max 7 `\n  --spec-draft-backend-sampling\n```\n\nbecomes `qwen3.8-27b.ini`:\n\n```ini\nversion = 1\n\n[qwen3.8-27b]\nload-on-startup = true\nhf-repo = unsloth/Qwen3.8-27B-GGUF\nhf-file = Qwen3.8-27B-UD-Q4_K_M.gguf\nhf-repo-draft = z-lab/Qwen3.8-27B-DFlash2-GGUF\nspec-draft-model = Qwen3.8-27B-DFlash2-Q4_K_M.gguf\nno-mmproj = true\nbatch-size = 4096\nubatch-size = 1024\nctx-size = 65536\nparallel = 1\nngl = 999\njinja = true\nchat-template-kwargs = {\"preserve_thinking\":true}\nspec-type = draft-dflash\nspec-draft-n-max = 7\nspec-draft-backend-sampling = true\n```\n\n## Start the router\n\nOpen PowerShell in the llama.cpp folder and run:\n\n```powershell\n.\\llama-server.exe --models-preset qwen3.8-27b.ini --host 127.0.0.1 --port 8080\n```\n\nIf the recipe requires a Hugging Face token, set `$env:HF_TOKEN` in the same PowerShell window before starting the router. On its first start, the router downloads the model files.\n\nUse the [router health checks](llama-cpp.md#troubleshooting) to confirm that the server is ready. In Windows PowerShell, use `curl.exe` rather than `curl`, which PowerShell maps to a different command. The model is ready when `/models` lists `qwen3.8-27b` with the status `loaded`.\n\n## Use the model in Pi\n\nFollow [Configure Pi](llama-cpp.md#configure-pi) and connect to `http://127.0.0.1:8080`. The preset section name is the model ID, so select `qwen3.8-27b` in `/model` for this example.\n\nNVIDIA recipe memory settings assume that only the selected model uses the GPU. If `/llama` shows other loaded models, unload them before using the recipe model.\n\n<!-- File: packages/coding-agent/docs/packages.md (Tokens: ~1307 | Category: anthropic-skill) -->\n# Pi Packages\n\nPi packages install and distribute extensions, skills, prompt templates, and themes as one unit. Use a package when a customization should be shared through npm or git, or when several resources belong together.\n\nA package is an ordinary directory or npm package. It can expose conventional resource directories, declare explicit paths under the `pi` key in `package.json`, and carry its own runtime dependencies.\n\n## Install and manage packages\n\nInstall from npm, git, or a local path:\n\n```bash\npi install npm:@example/pi-tools@1.0.0\npi install git:github.com/example/pi-tools@v1\npi install ./local-package\n```\n\n`pi list` shows configured packages. Use `pi remove <source>` to remove one and `pi update --extensions` to reconcile package installations. See [Command Line](cli.md#package-commands) for every package command and option.\n\nPersonal installs are written to `~/.pi/agent/settings.json`. Add `--local` or `-l` to write the package declaration to `.pi/settings.json`. Pi reads declarations from that file only after project trust is granted.\n\nProject packages are installed and loaded only after project trust is resolved. Packages can execute extension code and can include skills that instruct the model to run programs. Review third-party package source before installing it. Review project package declarations before granting project trust.\n\nUse `--extension` or `-e` to try a package for one invocation without adding it to settings:\n\n```bash\npi -e npm:@example/pi-tools\n```\n\n## Choose a source\n\n| Source | Example | Behavior |\n|---|---|---|\n| npm | `npm:@example/pi-tools@1.0.0` | Installed under the Pi npm directory |\n| git | `git:github.com/example/pi-tools@v1` | Cloned and reconciled to the selected ref |\n| URL | `https://github.com/example/pi-tools` | Treated as a git source |\n| Local | `./pi-tools` | Loaded from the resolved path without copying |\n\nVersioned npm specifications are pinned. Git tags and commits are also pinned; package updates reconcile the checkout but do not move a configured ref.\n\nRelative local paths resolve from the settings file that contains them. A file path loads one extension. A directory follows normal package discovery rules.\n\n## Create a package\n\nThe simplest package uses conventional directories:\n\n```text\nmy-pi-package/\n├── package.json\n├── extensions/\n├── skills/\n├── prompts/\n└── themes/\n```\n\nWithout a `pi` manifest, Pi discovers TypeScript and JavaScript extensions, skill directories, Markdown prompts, and JSON themes from those directories.\n\nUse an explicit manifest when resources live elsewhere or need filtering:\n\n```json\n{\n  \"name\": \"my-pi-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./src/extension.ts\"],\n    \"skills\": [\"./resources/skills\"],\n    \"prompts\": [\"./resources/prompts/*.md\"],\n    \"themes\": [\"./resources/themes/*.json\"]\n  }\n}\n```\n\nPaths are relative to the package root. Arrays accept glob patterns and exclusions. List dot-prefixed or symlinked resource roots directly when traversal through a glob would not discover them.\n\nThe `pi-package` keyword makes an npm package eligible for discovery in the [Pi package gallery](https://pi.dev/packages). Optional `pi.image` and `pi.video` fields add gallery previews.\n\n## Declare dependencies\n\nPut runtime packages imported by extensions in `dependencies`. Pi installs package dependencies when it installs an npm or git source.\n\nPi supplies these packages to extensions and skills:\n\n- `@earendil-works/pi-ai`\n- `@earendil-works/pi-agent-core`\n- `@earendil-works/pi-coding-agent`\n- `@earendil-works/pi-tui`\n- `typebox`\n\nDeclare the host-provided packages listed above in `peerDependencies` with a `\"*\"` range and do not bundle them. Pi suppresses automatic peer installation for managed npm packages and git packages installed with npm, pnpm, or Bun. Local packages are not installed or modified, so their dependency tree remains the package author's responsibility.\n\nDo not list host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping in compiled ESM and create duplicate classes, registries, and initialization work. Pi reports an extension warning when it detects this manifest configuration. Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths.\n\nInstalled packages load with separate module roots. Do not rely on two packages sharing one dependency instance or one package resolving another package’s undeclared dependency.\n\n## Select package resources\n\nThe object form in settings narrows which resources load from a package:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"npm:@example/pi-tools\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"]\n    }\n  ]\n}\n```\n\nFor each resource type:\n\n- Omit the property to load everything allowed by the package.\n- Use `[]` to load none of that type.\n- Use `!pattern` to exclude glob matches.\n- Use `+path` to include one exact allowed path.\n- Use `-path` to exclude one exact path.\n\nFilters narrow the package manifest. They do not expose resources that the package itself did not declare.\n\nRun `pi config` to enable or disable discovered resources and pi's built-in extensions. It starts with personal configuration; press Tab to switch scope, or run `pi config --local` to start with project overrides.\n\n## Understand scope and identity\n\nThe same package can appear in personal and project settings. A project entry normally replaces the personal entry. With `autoload: false`, the project entry instead acts as a filtering delta over the personal package.\n\nPi identifies npm packages by package name, git packages by repository URL without the ref, and local packages by resolved absolute path. This prevents the same package from loading twice through equivalent declarations.\n\nUse [Extensions](extensions.md), [Skills](skills.md), [Prompt Templates](prompt-templates.md), and [Themes](themes.md) to design each resource before packaging it.\n\n<!-- File: packages/coding-agent/docs/prompt-templates.md (Tokens: ~532 | Category: anthropic-skill) -->\n# Prompt Templates\n\nPrompt templates turn Markdown files into reusable `/` commands. Use one when you want to reuse the same prompt without adding executable behavior or a larger set of supporting instructions.\n\nA template can accept arguments and appear in command completion. Pi can load templates from personal configuration, project configuration, an explicit path, or a Pi package. Project configuration loads only after project trust is granted.\n\n## Create a template\n\nCreate `~/.pi/agent/prompts/review.md`:\n\n```markdown\n---\ndescription: Review staged git changes\nargument-hint: \"[focus]\"\n---\nReview the staged changes. Focus on ${1:-correctness, security, and error handling}.\n```\n\nThe filename becomes the command name, so this template is available as `/review`. The `description` appears in command completion. If it is omitted, Pi uses the first non-empty line.\n\n`argument-hint` is optional. Use `<angle brackets>` for required arguments and `[square brackets]` for optional arguments.\n\nRun `/reload` after adding or changing a template in an active session.\n\n<a id=\"invoke-a-template\"></a>\n\n## Use a template\n\nType the template command in the editor:\n\n```text\n/review\n/review concurrency\n```\n\nPi expands the template before the resulting text enters the agent. Extensions receive the raw input first through the `input` event unless an extension command with the same name handles it.\n\nTemplates support these substitutions:\n\n| Syntax | Result |\n|---|---|\n| `$1`, `$2`, … | One positional argument |\n| `$@` or `$ARGUMENTS` | All arguments joined with spaces |\n| `${1:-default}` | First argument, or a default value |\n| `${@:-default}` | All arguments, or a default value |\n| `${@:N}` | Arguments starting at position `N` |\n| `${@:N:L}` | `L` arguments starting at position `N` |\n\nArguments follow shell-like quoting, so `/review \"API compatibility\"` supplies one argument containing a space.\n\n<a id=\"choose-where-it-loads\"></a>\n\n## Add it to Pi\n\nPlace the template in your user or project prompt directory. Conventional prompt directories load direct `.md` children only.\n\nSettings and packages can select nested Markdown files; a package manifest can narrow discovery with explicit paths and globs. See [Settings](settings.md#resources) and [Pi Packages](packages.md) for these options.\n\nProject templates become commands in the editor after trust is granted. Review their content before trusting an unfamiliar project. See [Security](security.md#understand-project-trust).\n\n<!-- ... +49 more modular skills available via /api/skills/earendil-works/pi?path={path} -->\n</yakaai_skills>"}