Update pi-agent skill
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
name: pi-agent
|
||||
description: Build with and use Pi, the minimal terminal coding harness. Use for installing Pi, configuring providers/models/settings/environment variables, creating Pi skills/extensions/packages/themes/prompt templates, embedding Pi through the SDK, integrating over RPC or JSON event streams, parsing sessions, running local models through the llama.cpp router, developing custom Pi providers and TUI components, or using ecosystem packages such as pi-subagents (delegation/orchestration), pi-mcp-adapter (MCP servers), pi-interview (interactive forms), and pi-web-access (web search, fetching, video understanding).
|
||||
license: MIT
|
||||
compatibility: Requires Node.js/npm for Pi CLI and SDK usage. Pi package name is @earendil-works/pi-coding-agent.
|
||||
compatibility: Requires Node.js >= 22.19 and npm for Pi CLI and SDK usage. Pi package name is @earendil-works/pi-coding-agent.
|
||||
metadata:
|
||||
version: "1.2"
|
||||
version: "1.3"
|
||||
skill-author: K-Dense Inc.
|
||||
---
|
||||
|
||||
@@ -73,10 +73,11 @@ pi --mode rpc --no-session
|
||||
pi --provider anthropic --model claude-sonnet-4-5
|
||||
pi --model sonnet:high "Solve this complex problem"
|
||||
pi --tools read,grep,find,ls -p "Review this repository"
|
||||
pi --tui-mode fullscreen
|
||||
pi install npm:pi-subagents
|
||||
pi update --all
|
||||
```
|
||||
|
||||
## Source Coverage
|
||||
|
||||
These references summarize the Pi documentation at `https://pi.dev/docs/latest` and every docs page found under it as of this skill version, plus the package pages for `pi-subagents`, `pi-mcp-adapter`, `pi-interview`, and `pi-web-access` at `https://pi.dev/packages/`. Package details were cross-checked against the published npm READMEs (`pi-web-access` 0.14.0, `pi-mcp-adapter` 2.15.0, `pi-subagents` 0.37.0, `pi-interview` 0.9.0). When exact API behavior matters, prefer the cited reference page and inspect installed TypeScript definitions under `node_modules/@earendil-works/pi-coding-agent/dist/` and `node_modules/@earendil-works/pi-ai/dist/`.
|
||||
These references summarize the Pi documentation at `https://pi.dev/docs/latest` and every docs page found under it, as of Pi **0.84.2** (docs source: `packages/coding-agent/docs/` in `https://github.com/earendil-works/pi`, formerly `pi-mono`). They also cover the package pages for `pi-subagents`, `pi-mcp-adapter`, `pi-interview`, and `pi-web-access` at `https://pi.dev/packages/`, cross-checked against the published npm READMEs and package docs (`pi-web-access` 0.22.0, `pi-mcp-adapter` 2.25.0, `pi-subagents` 0.49.0, `pi-interview` 0.11.0). When exact API behavior matters, prefer the cited reference page and inspect installed TypeScript definitions under `node_modules/@earendil-works/pi-coding-agent/dist/` and `node_modules/@earendil-works/pi-ai/dist/`.
|
||||
|
||||
@@ -15,7 +15,7 @@ Both use fresh routing session IDs and, where the provider supports it, disable
|
||||
|
||||
Triggers when `contextTokens > contextWindow - reserveTokens`. Defaults: `reserveTokens` 16384, `keepRecentTokens` 20000, configured under `compaction` in global or project settings. `/compact [instructions]` works even with auto-compaction disabled.
|
||||
|
||||
Steps: walk backwards from the newest message accumulating token estimates until `keepRecentTokens` is reached (the cut point) → collect messages from the previous kept boundary (or session start) to the cut point → summarize with the structured format, passing any previous summary as iterative context → append a `CompactionEntry` → reload context as summary plus messages from `firstKeptEntryId`.
|
||||
Steps: walk backwards from the newest message accumulating token estimates until `keepRecentTokens` is reached (the cut point) → collect messages from the previous kept boundary (or session start) to the cut point → summarize with the structured format, passing any previous summary as iterative context → append a `CompactionEntry` → rebuild the context for the next request as summary plus messages from `firstKeptEntryId`.
|
||||
|
||||
On repeated compactions the summarized span starts at the previous compaction's kept boundary (`firstKeptEntryId`), not at the compaction entry, falling back to the entry after the previous compaction when that kept entry is not on the path. This re-includes messages that survived the earlier pass. `tokensBefore` is recalculated from the rebuilt context before writing the new entry.
|
||||
|
||||
@@ -27,7 +27,7 @@ A turn starts with a user message and includes all assistant responses and tool
|
||||
|
||||
## Branch Summarization
|
||||
|
||||
On `/tree` navigation to a different branch: find the deepest common ancestor, walk from the old leaf back to it, include messages up to the token budget newest-first, summarize, and append a `BranchSummaryEntry` at the navigation point.
|
||||
On `/tree` navigation to a different branch: find the deepest common ancestor, walk from the old leaf back to it, include messages up to the token budget newest-first, summarize, and append a `BranchSummaryEntry` at the navigation point — the summary lands on the destination branch's new leaf, not on the branch being left.
|
||||
|
||||
Both mechanisms extract file operations from the tool calls being summarized **and** from previous compaction/branch-summary `details`, so read/modified file tracking accumulates across passes.
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Use an **async extension factory** for dynamic model discovery so models are reg
|
||||
|
||||
`anthropic-messages`, `openai-completions`, `openai-responses`, `azure-openai-responses`, `openai-codex-responses`, `mistral-conversations`, `google-generative-ai`, `google-vertex`, `bedrock-converse-stream`.
|
||||
|
||||
Most OpenAI-compatible providers work with `openai-completions`; use model-level `thinkingLevelMap` for thinking levels and `compat` for quirks (full flag list in `references/models.md`). `xhigh` and `max` are opt-in and require non-null map entries. Mistral moved from `openai-completions` to `mistral-conversations` — use the latter for native Mistral models.
|
||||
Most OpenAI-compatible providers work with `openai-completions`; use model-level `thinkingLevelMap` for thinking levels and `compat` for quirks (full flag list in `references/models.md`). `xhigh` and `max` are opt-in and require non-null map entries. Mistral moved from `openai-completions` to `mistral-conversations` (native Mistral Chat Completions streaming) — use the latter for native Mistral models.
|
||||
|
||||
## Auth Header and Secrets
|
||||
|
||||
@@ -79,24 +79,24 @@ Most OpenAI-compatible providers work with `openai-completions`; use model-level
|
||||
oauth: {
|
||||
name: "Corporate AI (SSO)",
|
||||
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>,
|
||||
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials>,
|
||||
async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>,
|
||||
getApiKey(credentials: OAuthCredentials): string,
|
||||
}
|
||||
```
|
||||
|
||||
`OAuthLoginCallbacks`: `onAuth({ url })` (open in browser), `onDeviceCode({ userCode, verificationUri, intervalSeconds?, expiresInSeconds? })`, `onProgress?(message)`, `onPrompt({ message }): Promise<string>`, `onSelect({ message, options: { id, label }[] }): Promise<string | undefined>`.
|
||||
|
||||
`OAuthCredentials` is `{ refresh, access, expires }` (expiry in ms), persisted in `~/.pi/agent/auth.json`. Users authenticate with `/login <provider-name>`.
|
||||
`OAuthCredentials` is `{ refresh, access, expires }` (expiry in ms), persisted in `~/.pi/agent/auth.json`. Users authenticate with `/login <provider-name>`. `refreshToken` receives an `AbortSignal` — pass it to blocking I/O and call `signal.throwIfAborted()` early.
|
||||
|
||||
## Custom Streaming
|
||||
|
||||
Implement `streamSimple(model, context, options?)` returning an `AssistantMessageEventStream` from `createAssistantMessageEventStream()`. Initialize an `AssistantMessage` (`role`, `content: []`, `api`, `provider`, `model`, zeroed `usage`, `stopReason: "stop"`, `timestamp`), then:
|
||||
Implement `streamSimple(model, context, options?)` returning an `AssistantMessageEventStream` from `createAssistantMessageEventStream()`. Initialize an `AssistantMessage` (`role`, `content: []`, `api`, `provider`, `model`, zeroed `usage`, `stopReason: "pending"`, `timestamp`), then:
|
||||
|
||||
1. `stream.push({ type: "start", partial: output })`
|
||||
2. Content events, tracking `contentIndex` per block: `text_start`, `text_delta`, `text_end`, `thinking_start`, `thinking_delta`, `thinking_end`, `toolcall_start`, `toolcall_delta`, `toolcall_end`
|
||||
3. `stream.push({ type: "done", reason, message })` or `{ type: "error", reason, error }`, then `stream.end()`
|
||||
|
||||
Every event carries `partial` with the current `AssistantMessage` state — mutate `output.content` as data arrives and pass `output`. Tool calls accumulate JSON deltas, parse into `{ id, name, arguments }`, and finish with `toolcall_end` carrying the full `toolCall`. Update usage from the API response and call `calculateCost(model, output.usage)`. Register with `streamSimple` on the provider config.
|
||||
`stopReason: "pending"` marks the partial message; set a terminal reason before pushing `done` (throw for `"error"`/`"aborted"`). Every event carries `partial` with the current `AssistantMessage` state — mutate `output.content` as data arrives and pass `output`. Tool calls accumulate JSON deltas, parse into `{ id, name, arguments }`, and finish with `toolcall_end` carrying the full `toolCall`. Update usage from the API response and call `calculateCost(model, output.usage)`. Register with `streamSimple` on the provider config.
|
||||
|
||||
Reference implementations in `packages/ai/src/providers/`: `anthropic.ts`, `mistral.ts`, `openai-completions.ts`, `openai-responses.ts`, `google.ts`, `amazon-bedrock.ts`.
|
||||
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
Source: https://pi.dev/docs/latest/environment-variables
|
||||
|
||||
Pi uses environment variables three ways: variables that configure the Pi process, a marker Pi sets so child processes know they run inside Pi, and session metadata injected into commands run by the LLM-callable bash tool. Provider API-key variables live in `references/providers.md`.
|
||||
Pi uses environment variables three ways: variables that configure the Pi process, markers Pi sets so child processes know they run inside Pi, and session metadata injected into commands run by the LLM-callable bash tool. Provider API-key variables live in `references/providers.md`.
|
||||
|
||||
## Process Marker
|
||||
## Process Markers
|
||||
|
||||
The CLI and RPC entry points set `PI_CODING_AGENT=true`. Child processes inherit it. It is not session-specific and is **not** set automatically when Pi is embedded through the SDK.
|
||||
The CLI and RPC entry points set two markers:
|
||||
|
||||
- `AI_AGENT=pi` — generic marker letting tooling identify Pi as the launching agent.
|
||||
- `PI_CODING_AGENT=true` — Pi-specific marker for detecting that a process runs inside Pi.
|
||||
|
||||
Child processes inherit both. Neither is session-specific, and neither is set automatically when Pi is embedded through the SDK.
|
||||
|
||||
## Bash Tool Session Environment
|
||||
|
||||
@@ -43,6 +48,7 @@ Custom bash tools built with `createBashTool()` expose the same variables by def
|
||||
| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |
|
||||
| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |
|
||||
| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor (IME positioning) |
|
||||
| `PI_TUI_ESC_TIMEOUT` | Milliseconds to wait after a lone ESC before treating it as Escape; defaults to `100` over SSH and `10` otherwise. Increase when Alt-key input is misread as Escape |
|
||||
| `VISUAL`, `EDITOR` | External editor fallback when the `externalEditor` setting is unset |
|
||||
| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ Session replacement (`/new`, `/resume`): `session_before_switch` (cancellable)
|
||||
- `before_provider_headers` — mutate `event.headers` in place; a string adds/overrides, `null` deletes. Fires once per request; retries reuse the headers.
|
||||
- `before_provider_request` — inspect or replace `event.payload`; handlers run in load order and `undefined` keeps it unchanged. Payload-level system-instruction rewrites are not reflected by `ctx.getSystemPrompt()`.
|
||||
- `after_provider_response` — `event.status` and normalized `event.headers` before the stream body is consumed.
|
||||
- `tool_call` — `event.input` is mutable and mutations affect execution (no re-validation); return `{ block: true, reason? }` to block. Narrow with `isToolCallEventType("bash", event)`, or `isToolCallEventType<"my_tool", MyToolInput>(...)` for custom tools.
|
||||
- `tool_call` — `event.input` is mutable and mutations affect execution (no re-validation); return `{ block: true, reason?, terminate? }` to block. `terminate` applies only to a blocked call, and the agent stops early only when every finalized result in the batch is terminating. Narrow with `isToolCallEventType("bash", event)`, or `isToolCallEventType<"my_tool", MyToolInput>(...)` for custom tools.
|
||||
- `tool_result` — middleware-style chain; return partial patches (`content`, `details`, `isError`, `usage`). Use `isBashToolResult(event)` for typed bash details and `ctx.signal` for nested async work.
|
||||
- `message_end` — return `{ message }` to replace the finalized message; the replacement must keep the same `role`.
|
||||
- `user_bash` — intercept `!`/`!!`: return `{ operations }` (optionally wrapping `createLocalBashOperations()`) or `{ result }`.
|
||||
@@ -82,7 +82,9 @@ In parallel tool mode, sibling tool calls are preflighted sequentially then exec
|
||||
|
||||
## ExtensionContext
|
||||
|
||||
`ctx.ui` (see Custom UI), `ctx.mode` (`"tui" | "rpc" | "json" | "print"`), `ctx.hasUI` (true in TUI and RPC), `ctx.cwd`, `ctx.signal` (agent abort signal; usually `undefined` outside active turns), `ctx.isProjectTrusted()`, `ctx.sessionManager` (read-only: `getEntries`, `getBranch`, `buildContextEntries`, `getLeafId`, …), `ctx.modelRegistry` (`getProvider(id)`, `getProviderAuth(id)`, `find(...)`), `ctx.model`, `ctx.thinkingLevel`, `ctx.isIdle()`, `ctx.abort()`, `ctx.hasPendingMessages()`, `ctx.shutdown()`, `ctx.getContextUsage()`, `ctx.compact({ customInstructions, onComplete, onError })`, `ctx.getSystemPrompt()`.
|
||||
`ctx.ui` (see Custom UI), `ctx.mode` (`"tui" | "rpc" | "json" | "print"`), `ctx.hasUI` (true in TUI and RPC), `ctx.cwd`, `ctx.signal` (agent abort signal; usually `undefined` outside active turns), `ctx.isProjectTrusted()`, `ctx.sessionManager` (read-only: `getEntries`, `getBranch`, `buildContextEntries`, `getLeafId`, …), `ctx.modelRegistry` (`getProvider(id)`, `getProviderAuth(id)`, `find(...)`), `ctx.model`, `ctx.thinkingLevel`, `ctx.scopedModels`, `ctx.isIdle()`, `ctx.abort()`, `ctx.hasPendingMessages()`, `ctx.shutdown()`, `ctx.getContextUsage()`, `ctx.compact({ customInstructions, onComplete, onError })`, `ctx.getSystemPrompt()`.
|
||||
|
||||
`ctx.scopedModels` is the read-only list of models scoped to the session — the same set `/scoped-models` shows, resolved at session start from `--models` and the `enabledModels` setting (minimatch against `provider/modelId` or a bare `modelId`). It is empty when no scoping is configured, meaning every available model is usable. Entries are `{ model, thinkingLevel? }`, with `thinkingLevel` set only when a pattern pinned it (e.g. `anthropic/*:high`). Use it for a model picker that mirrors the built-in one instead of enumerating `ctx.modelRegistry.getAvailable()`.
|
||||
|
||||
Use the exported `CONFIG_DIR_NAME` instead of hardcoding `.pi` — rebranded distributions use a different name.
|
||||
|
||||
@@ -96,14 +98,27 @@ Command handlers additionally get session-control methods that would deadlock fr
|
||||
|
||||
Tools: `registerTool(definition)` (works during load and at runtime — new tools are callable without `/reload`), `getActiveTools()`, `getAllTools()` (returns `name`, `description`, `parameters`, `promptGuidelines`, `sourceInfo`), `setActiveTools(names)`.
|
||||
|
||||
Messages and session: `sendMessage(message, { deliverAs: "steer" | "followUp" | "nextTurn", triggerTurn })`, `sendUserMessage(content, { deliverAs })` (required while streaming), `appendEntry(customType, data)`, `setSessionName`, `getSessionName`, `setLabel(entryId, label)`.
|
||||
Messages and session: `sendMessage(message, { deliverAs: "steer" | "followUp" | "nextTurn", triggerTurn })`, `sendUserMessage(content, { deliverAs, expandPromptTemplates })` (`deliverAs` required while streaming; `expandPromptTemplates` defaults to `false` and opts into extension-command dispatch plus skill/prompt-template expansion), `appendEntry(customType, data)`, `setSessionName`, `getSessionName`, `setLabel(entryId, label)`.
|
||||
|
||||
Commands and input: `registerCommand(name, { description, handler, getArgumentCompletions })` (duplicate names get `:1`/`:2` suffixes in load order), `getCommands()` (extension → prompt → skill order, each with `sourceInfo.scope`/`origin`), `registerShortcut(key, options)`, `registerFlag(name, options)` + `getFlag(name)`.
|
||||
|
||||
Rendering: `registerMessageRenderer(customType, renderer)` (custom messages, in LLM context), `registerEntryRenderer(customType, renderer)` (custom entries, TUI only).
|
||||
Rendering: `registerMessageRenderer(customType, renderer)` (custom messages, in LLM context), `registerEntryRenderer(customType, renderer)` (custom entries, TUI only), `registerMarkdownTransformer(transformer)`.
|
||||
|
||||
`registerMarkdownTransformer` transforms the Markdown of normal user text, assistant text, and thinking blocks before Pi's built-in renderer runs. Transformers run in extension load order, each receiving the previous transformer's output plus a context of `messageType` (`"user" | "assistant" | "assistant-thinking"`), `isStreaming` (true only for partial assistant updates), and `availableWidth` (exact terminal columns):
|
||||
|
||||
```typescript
|
||||
pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
|
||||
if (isStreaming || messageType === "assistant-thinking") return markdown;
|
||||
return markdown.replaceAll("-->", "→");
|
||||
});
|
||||
```
|
||||
|
||||
A throwing transformer keeps the Markdown produced so far and continues with the next one. The hook is display-only — the session and model context keep the original message. It fires for new user messages, assistant streaming updates, restored session messages, and terminal width changes, so keep transformers synchronous and cheap.
|
||||
|
||||
Model and provider: `setModel(model)` (returns `false` without an API key), `getThinkingLevel()`, `setThinkingLevel(level)`, `registerProvider(nameOrProvider, config?)`, `unregisterProvider(name)`. Calls after the load phase take effect immediately. Dynamic providers can implement `refreshModels`, and a complete pi-ai `Provider` from `createProvider(...)` can be registered as the composition base with `models.json` overrides layered above.
|
||||
|
||||
`refreshModels` receives the canonical credential/stored-catalog/network/signal context: `context.stored` is the persisted provider snapshot, and persistence goes through generation-checked `context.publish({ persist: entry })` (`persist: null` deletes the snapshot). Live servers such as llama.cpp can return models without persisting. `context.signal` is always a concrete signal and provider callbacks must pass it to blocking I/O; public `ModelRuntime.refresh()` / `ModelRegistry.refresh()` accept an optional signal and are unbounded when it is omitted, so extensions choose their own deadlines. Cancellation stops the caller waiting even if a provider ignores the signal. OAuth `refreshToken(credentials, signal)` now takes the signal as a second argument.
|
||||
|
||||
Other: `exec(command, args, { signal, timeout })` → `{ stdout, stderr, code, killed }`, `on(event, handler)`, `events` (inter-extension bus).
|
||||
|
||||
## Custom Tools
|
||||
|
||||
@@ -10,6 +10,16 @@ pi --mode json "Your prompt"
|
||||
|
||||
## Event Types
|
||||
|
||||
Wire events use `JsonAgentSessionEvent`, which matches `AgentSessionEvent` except that streaming message updates omit cumulative snapshots:
|
||||
|
||||
```typescript
|
||||
type WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, "partial"> : T;
|
||||
|
||||
type JsonAgentSessionEvent =
|
||||
| Exclude<AgentSessionEvent, { type: "message_update" }>
|
||||
| { type: "message_update"; usage: Usage; assistantMessageEvent: WithoutPartial<AssistantMessageEvent> };
|
||||
```
|
||||
|
||||
`AgentSessionEvent` is `AgentEvent` plus session-level events:
|
||||
|
||||
- `queue_update` — `{ steering: readonly string[], followUp: readonly string[] }`, emitted whenever either queue changes
|
||||
@@ -25,7 +35,7 @@ Base `AgentEvent` types:
|
||||
|
||||
- `agent_start`, `agent_end` (`messages`)
|
||||
- `turn_start`, `turn_end` (`message`, `toolResults`)
|
||||
- `message_start` (`message`), `message_update` (`message`, `assistantMessageEvent`), `message_end` (`message`)
|
||||
- `message_start` (`message`), `message_update` (`usage`, `assistantMessageEvent`), `message_end` (`message`)
|
||||
- `tool_execution_start` (`toolCallId`, `toolName`, `args`), `tool_execution_update` (+ `partialResult`), `tool_execution_end` (`result`, `isError`)
|
||||
|
||||
## Output Format
|
||||
@@ -42,12 +52,14 @@ Then events as they occur:
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[]}}
|
||||
{"type":"message_update","message":{},"assistantMessageEvent":{"type":"text_delta","delta":"Hello"}}
|
||||
{"type":"message_update","usage":{},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello"}}
|
||||
{"type":"message_end","message":{}}
|
||||
{"type":"turn_end","message":{},"toolResults":[]}
|
||||
{"type":"agent_end","messages":[]}
|
||||
```
|
||||
|
||||
`message_update` records are delta-only: they omit both the cumulative `message` field and `assistantMessageEvent.partial` to keep stream size linear. The top-level `usage` field carries the latest cumulative provider-reported usage and may stay zero when a provider only reports usage at completion. Assemble live text, thinking, or tool-call arguments from `contentIndex` and `delta`; `message_end` holds the final authoritative message.
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
|
||||
@@ -6,7 +6,7 @@ All shortcuts are customizable in `~/.pi/agent/keybindings.json`, which uses the
|
||||
|
||||
## Key Format
|
||||
|
||||
`modifier+key` where modifiers are `ctrl`, `shift`, `alt` (combinable, e.g. `ctrl+shift+x`, `alt+ctrl+1`). Keys:
|
||||
`modifier+key` where modifiers are `ctrl`, `shift`, `alt`, `super` (combinable, e.g. `ctrl+shift+x`, `alt+ctrl+1`, `super+k`, `ctrl+super+k`). `super` bindings need a terminal that reports the modifier separately, typically via the Kitty keyboard protocol. Keys:
|
||||
|
||||
- Letters `a-z`, digits `0-9`
|
||||
- Special: `escape`/`esc`, `enter`/`return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`
|
||||
@@ -15,7 +15,9 @@ All shortcuts are customizable in `~/.pi/agent/keybindings.json`, which uses the
|
||||
|
||||
## Actions
|
||||
|
||||
**`tui.editor.*` cursor** — `cursorUp` (up), `cursorDown` (down), `cursorLeft` (left, ctrl+b), `cursorRight` (right, ctrl+f), `cursorWordLeft` (alt+left, ctrl+left, alt+b), `cursorWordRight` (alt+right, ctrl+right, alt+f), `cursorLineStart` (home, ctrl+a), `cursorLineEnd` (end, ctrl+e), `jumpForward` (ctrl+]), `jumpBackward` (ctrl+alt+]), `pageUp`, `pageDown`.
|
||||
**`tui.editor.*` cursor** — `cursorUp` (up; browses older history at the top), `cursorDown` (down; browses newer history at the bottom), `historyPrevious` / `historyNext` (no defaults), `cursorLeft` (left, ctrl+b), `cursorRight` (right, ctrl+f), `cursorWordLeft` (alt+left, ctrl+left, alt+b), `cursorWordRight` (alt+right, ctrl+right, alt+f), `cursorLineStart` (home, ctrl+home, ctrl+a), `cursorLineEnd` (end, ctrl+end, ctrl+e), `jumpForward` (ctrl+]), `jumpBackward` (ctrl+alt+]), `pageUp` (pageUp, ctrl+pageUp), `pageDown` (pageDown, ctrl+pageDown).
|
||||
|
||||
The dedicated `historyPrevious`/`historyNext` actions always change history entries regardless of cursor position in a multiline prompt, and explicit history bindings take precedence over application actions while the main editor is focused — binding `tui.editor.historyPrevious` to `ctrl+p` overrides model cycling in that context without changing `Ctrl+P` in selectors.
|
||||
|
||||
**`tui.editor.*` deletion** — `deleteCharBackward` (backspace), `deleteCharForward` (delete, ctrl+d), `deleteWordBackward` (ctrl+w, alt+backspace), `deleteWordForward` (alt+d, alt+delete), `deleteToLineStart` (ctrl+u), `deleteToLineEnd` (ctrl+k).
|
||||
|
||||
@@ -25,7 +27,11 @@ All shortcuts are customizable in `~/.pi/agent/keybindings.json`, which uses the
|
||||
|
||||
**`tui.select.*`** — `up`, `down`, `pageUp`, `pageDown`, `confirm` (enter), `cancel` (escape, ctrl+c).
|
||||
|
||||
**`app.*` application** — `interrupt` (escape), `clear` (ctrl+c), `exit` (ctrl+d when editor empty), `suspend` (ctrl+z; none on Windows), `editor.external` (ctrl+g), `clipboard.pasteImage` (ctrl+v; alt+v on Windows).
|
||||
**`tui.altScreen.*` fullscreen viewport** (only in `--tui-mode fullscreen`) — `pageUp` (pageUp), `pageDown` (pageDown), `halfPageUp` / `halfPageDown` / `lineUp` / `lineDown` (no defaults), `previousPrompt` (ctrl+shift+up), `nextPrompt` (ctrl+shift+down), `search` (ctrl+shift+f), `searchNext` (enter, ctrl+g), `searchPrevious` (shift+enter, ctrl+shift+g), `searchClose` (escape), `top` (home), `bottom` (end).
|
||||
|
||||
These target the primary transcript scroll region and take precedence over editor bindings, so in fullscreen mode unmodified `home`/`end`/`pageUp`/`pageDown` drive the transcript while their `ctrl` variants still drive the editor; outside fullscreen both variants drive the editor. Rebind normally to change the routing (`"tui.altScreen.pageUp": "ctrl+pageUp"`), or set `[]` to disable a transcript shortcut. Mouse-wheel and two-finger input scroll the region under the pointer, OSC 8 hyperlinks open on click, and primary-button drag selects text and copies it (holding at an edge auto-scrolls).
|
||||
|
||||
**`app.*` application** — `interrupt` (escape), `clear` (ctrl+c; clears the editor first, exits on a second press), `exit` (ctrl+d when editor empty), `suspend` (ctrl+z; none on Windows), `editor.external` (ctrl+g), `clipboard.pasteImage` (ctrl+v; alt+v on Windows — pastes images or text).
|
||||
|
||||
**`app.session.*`** — `new`, `tree`, `fork`, `resume` (no defaults), `togglePath` (ctrl+p), `toggleSort` (ctrl+s), `toggleNamedFilter` (ctrl+n), `rename` (ctrl+r), `delete` (ctrl+d), `deleteNoninvasive` (ctrl+backspace).
|
||||
|
||||
@@ -41,8 +47,8 @@ All shortcuts are customizable in `~/.pi/agent/keybindings.json`, which uses the
|
||||
|
||||
```json
|
||||
{
|
||||
"tui.editor.cursorUp": ["up", "ctrl+p"],
|
||||
"tui.editor.cursorDown": ["down", "ctrl+n"],
|
||||
"tui.editor.historyPrevious": "ctrl+p",
|
||||
"tui.editor.historyNext": "ctrl+n",
|
||||
"tui.editor.deleteWordBackward": ["ctrl+w", "alt+backspace"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -46,7 +46,7 @@ For `google-generative-ai` custom models, `baseUrl` is required (for example `ht
|
||||
|
||||
## Model Fields
|
||||
|
||||
Required: `id`. Optional: `name` (defaults to `id`), `api`, `reasoning` (`false`), `thinkingLevelMap`, `input` (`["text"]` or `["text","image"]`), `contextWindow` (`128000`), `maxTokens` (`16384`), `cost` (zeros), `compat` (merged with provider `compat`).
|
||||
Required: `id`. Optional: `name` (defaults to `id`), `api`, `reasoning` (`false`), `thinkingLevelMap`, `input` (`["text"]` or `["text","image"]`), `contextWindow` (`128000`), `maxTokens` (`16384`), `samplingParams`, `cost` (zeros), `compat` (merged with provider `compat`).
|
||||
|
||||
`/model`, `--list-models`, and the footer display entries by model `id`; `name` is used for `--model` pattern matching and secondary detail text.
|
||||
|
||||
@@ -61,6 +61,17 @@ Required: `id`. Optional: `name` (defaults to `id`), `api`, `reasoning` (`false`
|
||||
}
|
||||
```
|
||||
|
||||
## Sampling Parameters
|
||||
|
||||
`samplingParams` is a free-form object merged verbatim into every request body for that model, after the fields Pi sets itself — so its keys win, including over Pi's own `temperature`. Use it for sampling controls Pi does not model, such as llama.cpp `min_p` or vLLM `top_k`:
|
||||
|
||||
```json
|
||||
{ "id": "deepseek-v4-flash",
|
||||
"samplingParams": { "temperature": 1.0, "top_p": 0.95, "top_k": 0, "min_p": 0.0 } }
|
||||
```
|
||||
|
||||
Only OpenAI-compatible APIs apply it (`openai-completions`, `openai-responses`, `azure-openai-responses`); other APIs ignore it. Treat it as the single source of sampling truth for a model. In `modelOverrides`, `samplingParams` merges per key with the base model's value.
|
||||
|
||||
## Thinking Level Map
|
||||
|
||||
Keys are Pi levels: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Maps may have holes. Values are tristate: omitted means standard levels through `high` use the provider default mapping while `xhigh`/`max` are unsupported; a string is sent to the provider; `null` marks the level unsupported so it is hidden/skipped/clamped away.
|
||||
@@ -82,7 +93,7 @@ Base-URL-only overrides keep all built-in models and existing auth:
|
||||
|
||||
If `models` is included, built-in models are kept and custom models are upserted by `id` — a matching `id` replaces the built-in, a new `id` is added.
|
||||
|
||||
`modelOverrides` customizes built-in and matching extension-registered models without replacing the provider list. Supported per-model fields: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (partial), `contextWindow`, `maxTokens`, `headers`, `compat`. Unknown model IDs are ignored; provider-level `baseUrl`/`headers` can be combined with it. If `models` is also defined, custom models merge after built-in overrides.
|
||||
`modelOverrides` customizes built-in and matching extension-registered models without replacing the provider list. Supported per-model fields: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (partial), `contextWindow`, `maxTokens`, `samplingParams` (merged per key), `headers`, `compat`. Unknown model IDs are ignored; provider-level `baseUrl`/`headers` can be combined with it. If `models` is also defined, custom models merge after built-in overrides.
|
||||
|
||||
Example — opt a direct OpenAI GPT-5.6 model into the 1.05M context window (they default to `272000` to stay in the short-context pricing tier):
|
||||
|
||||
@@ -96,8 +107,8 @@ Example — opt a direct OpenAI GPT-5.6 model into the 1.05M context window (the
|
||||
|
||||
## OpenAI Compatibility Flags
|
||||
|
||||
`supportsStore`, `supportsDeveloperRole`, `supportsReasoningEffort`, `supportsUsageInStreaming` (default `true`), `maxTokensField` (`max_completion_tokens` | `max_tokens`), `requiresToolResultName`, `requiresAssistantAfterToolResult`, `requiresThinkingAsText`, `requiresReasoningContentOnAssistantMessages`, `thinkingFormat` (`reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, `qwen-chat-template`), `chatTemplateKwargs`, `cacheControlFormat` (`anthropic`), `sendSessionAffinityHeaders` (default `false`), `sessionAffinityFormat` (`openai`, `openai-nosession`, `openrouter`), `supportsStrictMode`, `supportsOpenAIGrammarTools` (default `false`; the built-in catalog enables it for GPT-5+ on OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode, Cloudflare AI Gateway), `deferredToolsMode` (`"kimi"`), `supportsLongCacheRetention` (default `true`; `prompt_cache_retention: "24h"`), `openRouterRouting`, `vercelGatewayRouting`.
|
||||
`supportsStore`, `supportsDeveloperRole`, `supportsReasoningEffort`, `supportsUsageInStreaming` (default `true`), `supportsFinishReason` (default `true`; `false` makes Pi infer `stop`/`toolUse` when the stream ends without one), `maxTokensField` (`max_completion_tokens` | `max_tokens`), `requiresToolResultName`, `requiresAssistantAfterToolResult`, `requiresThinkingAsText`, `requiresReasoningContentOnAssistantMessages`, `thinkingFormat` (`reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template`, `qwen-chat-template`), `chatTemplateKwargs`, `chatTemplateArgs` (`chat_template_args` values for `thinkingFormat: "baseten"`), `cacheControlFormat` (`anthropic`), `sendSessionAffinityHeaders` (default `false`), `sessionAffinityFormat` (`openai`, `openai-nosession`, `openrouter`), `supportsStrictMode`, `supportsOpenAIGrammarTools` (default `false`; the built-in catalog enables it for GPT-5+ on OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode, Cloudflare AI Gateway), `deferredToolsMode` (`"kimi"`), `supportsLongCacheRetention` (default `true`; `prompt_cache_retention: "24h"`), `openRouterRouting`, `vercelGatewayRouting`.
|
||||
|
||||
Thinking-format notes: `openrouter` uses `reasoning: { effort }`; `together` uses `reasoning: { enabled }` plus `reasoning_effort` when `supportsReasoningEffort`; `qwen` uses top-level `enable_thinking`; `qwen-chat-template` targets local Qwen servers needing `chat_template_kwargs.enable_thinking` and `preserve_thinking`; `chat-template` plus `chatTemplateKwargs` targets vLLM/Hugging Face templates, e.g. `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x. `$var` accepts `thinking.enabled` or `thinking.effort`.
|
||||
Thinking-format notes: `openrouter` uses `reasoning: { effort }`; `together` uses `reasoning: { enabled }` plus `reasoning_effort` when `supportsReasoningEffort`; `qwen` uses top-level `enable_thinking`; `qwen-chat-template` targets local Qwen servers needing `chat_template_kwargs.enable_thinking` and `preserve_thinking`; `chat-template` plus `chatTemplateKwargs` targets vLLM/Hugging Face templates, e.g. `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x. `baseten` plus `chatTemplateArgs` targets providers exposing toggle controls through `chat_template_args`, optionally with top-level `reasoning_effort`. `$var` accepts `thinking.enabled` or `thinking.effort`.
|
||||
|
||||
`openRouterRouting` is sent as-is in the OpenRouter `provider` field (`only`, `order`, `ignore`, `sort`, `max_price`, `quantizations`, `zdr`, …). `vercelGatewayRouting` takes `only`/`order`.
|
||||
|
||||
@@ -32,4 +32,6 @@ Authenticate with `/login` for subscription providers or set API keys such as `A
|
||||
|
||||
## Ecosystem
|
||||
|
||||
Package gallery at `https://pi.dev/packages` lists community extensions tagged `pi-package`. Source: `https://github.com/earendil-works/pi-mono` (docs live under `packages/coding-agent/docs/`).
|
||||
Package gallery at `https://pi.dev/packages` lists community extensions tagged `pi-package`. Source: `https://github.com/earendil-works/pi` (formerly `pi-mono`; docs live under `packages/coding-agent/docs/`, and doc pages still link the old repo name, which redirects).
|
||||
|
||||
Pi requires Node.js >= 22.19.0. The published version this skill was written against is `0.84.2` (`https://pi.dev/api/latest-version` reports the current one).
|
||||
|
||||
@@ -21,7 +21,9 @@ await interview({
|
||||
});
|
||||
```
|
||||
|
||||
Lifecycle: the tool starts a local server and opens a Glimpse window (macOS) or browser tab → the user answers at their own pace with auto-save and timeout reset on any activity → the session ends by Submit (`⌘+Enter`), timeout (warning overlay with an option to stay), or Escape twice → the window closes and the agent receives responses, or `null` if cancelled.
|
||||
Lifecycle: the tool starts a local server and opens a Glimpse window (macOS), an Orca tab, or a browser tab → the user answers at their own pace with auto-save and timeout reset on any activity → the session ends by Submit (`⌘+Enter`), timeout (warning overlay with an option to stay), or Escape twice → the window closes and the agent receives responses, or `null` if cancelled.
|
||||
|
||||
Remote and Moshi sessions: when the session looks remote (ssh/mosh env, or an active remote login on the host), the tool skips or supplements the local window and prints the form URL with access hints — a Moshi tip when the moshi-hook gateway is running (tap the preview button in the terminal title bar and pick the interview server), and an exact `ssh -L` command for plain SSH (mosh cannot forward ports). The server binds low ports (8377+, scanning forward on collision) and answers tokenless loopback opens with a landing page that hops to the form, so Moshi's browser preview reaches it in one tap. Requests with a non-loopback `Host` header are rejected.
|
||||
|
||||
With multiple concurrent interviews, only the first auto-opens; the rest are queued and surfaced as URLs in tool output, plus a top-right toast with a dropdown to open queued sessions. Submitting the active interview redirects the window to the next queued one. A status bar shows project path, git branch, and session ID.
|
||||
|
||||
@@ -82,6 +84,8 @@ interface Response {
|
||||
"snapshotDir": "~/.pi/interview-snapshots/",
|
||||
"autoSaveOnSubmit": true,
|
||||
"generateModel": "anthropic/claude-haiku-4-5",
|
||||
"launcher": "browser",
|
||||
"browser": "Firefox",
|
||||
"glimpseFloating": false,
|
||||
"theme": {
|
||||
"mode": "auto",
|
||||
@@ -96,6 +100,14 @@ interface Response {
|
||||
|
||||
Timeout precedence: function parameter > settings > default 600s. A fixed `port` keeps the URL stable across sessions. `generateModel` drives the generate/review option actions, defaulting to the agent's current model then a cheap available model; if an explicitly configured model fails and the session uses a different one, it retries once with the session model. `glimpseFloating` keeps the native macOS window above others (browser fallback unaffected).
|
||||
|
||||
`launcher` chooses where the form opens; omit it for the default (Glimpse on a local macOS session with `glimpseui` installed, otherwise a browser tab):
|
||||
|
||||
- `"glimpse"` — native macOS Glimpse window; requires a local macOS session with `glimpseui`, and reports why the window could not open instead of falling back to a browser.
|
||||
- `"browser"` — browser tab even when Glimpse is installed.
|
||||
- `"orca"` — a browser tab in the current [Orca](https://github.com/stablyai/orca)-managed worktree, or Orca's focused worktree when the cwd is outside one; the tab is focused when that worktree is visible, otherwise staged in its tab bar. Needs `orca` on `PATH`.
|
||||
|
||||
`browser` names the application used for browser tabs (`"Firefox"`, `"Brave Browser"`, …). It applies to `launcher: "browser"` and to an omitted `launcher` when Glimpse is unavailable; it has no effect under `"glimpse"` or `"orca"`.
|
||||
|
||||
Themes: built-ins are `default` (monospace) and `tufte` (serif); modes are `dark` (default), `light`, and `auto` (follows the OS, user override persists in localStorage). Custom themes are CSS files overriding variables such as `--bg-body`, `--bg-card`, `--bg-elevated`, `--bg-selected`, `--fg`, `--fg-muted`, `--accent`, `--border`, `--success`, `--warning`, `--error`, `--focus-ring`.
|
||||
|
||||
## Keyboard
|
||||
|
||||
@@ -24,15 +24,25 @@ Precedence, lowest to highest:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@latest"] }
|
||||
"chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@1.6.0"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Host-specific configs are detected but **not** loaded automatically. Opt in with `settings.hostConfigDiscovery: "on"` (or `pi-mcp-adapter init --discover-host-configs`); `"off"` is the default and `"prompt"` detects without activating. Host configs sit below every shared and Pi-owned source.
|
||||
Host-specific configs are detected but **not** loaded automatically, and the normal `/mcp` panel does not scan them while `settings.hostConfigDiscovery` is `"off"` (the default). Opt in with `"on"` (or `pi-mcp-adapter init --discover-host-configs`); `"prompt"` detects without activating. Host configs sit below every shared and Pi-owned source.
|
||||
|
||||
Import specific host formats explicitly with `"imports": ["cursor", "claude-code", "claude-desktop", "opencode", "vscode", "windsurf", "codex"]`.
|
||||
|
||||
### Agent Plugins
|
||||
|
||||
List [Agent Plugins](https://agent-plugins.org/) package directories in `settings.agentPluginPaths` to load their MCP servers:
|
||||
|
||||
```json
|
||||
{ "settings": { "agentPluginPaths": ["./plugins/acme-tools"] }, "mcpServers": {} }
|
||||
```
|
||||
|
||||
Each directory needs a valid Agent Plugins 1.0 `plugin.json`; a root `mcp.json` there contributes `mcpServers` entries prefixed `<plugin>__<server>`. The loader uses the transport declared by each server `type` and skips invalid entries without blocking others. For stdio plugin servers, `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` expand only in `args`, `env`, and `cwd`; both are set for the child process and plugin data is stored under the Pi agent directory. Native Pi MCP config remains `.mcp.json`, `~/.config/mcp/mcp.json`, and the Pi-owned overrides.
|
||||
|
||||
`/mcp disable <server>` / `/mcp enable <server>` persist only the `disabled` field into `.pi/mcp.json` (never rewriting the source file or copying credentials); run `/reload` to apply. The manual equivalent is `{ "disabled": true }` in any MCP config.
|
||||
|
||||
### SDK Configuration
|
||||
@@ -46,6 +56,8 @@ A supplied `config` is a complete isolated snapshot — never merged with files,
|
||||
|
||||
OAuth credentials are stored in the OS credential store keyed by configured server name, with URL binding so credentials cannot be reused for a different server URL. `settings.oauthDir` / `MCP_OAUTH_DIR` are legacy plaintext `tokens.json` import locations only.
|
||||
|
||||
Cooperating Pi extensions can reuse URL-bound tokens through the public `pi-mcp-adapter/oauth` subpath (`getMcpOAuthTokensForUrl(server, url)`, `updateMcpOAuthTokensForUrl(server, url, tokens)`, plus a status helper). The async read path applies the adapter's refresh logic first. It exposes token read/update only — never client registration secrets, PKCE verifiers, or OAuth state — and keeps secure-store storage, URL binding, refresh persistence, chunk handling, legacy import, and fail-closed credential-store errors.
|
||||
|
||||
## Server Options
|
||||
|
||||
| Field | Description |
|
||||
@@ -55,20 +67,28 @@ OAuth credentials are stored in the OS credential store keyed by configured serv
|
||||
| `env`, `cwd` | Interpolation supported; an `env` value starting with `!` runs a command at connect (`!!` escapes) |
|
||||
| `url`, `headers` | StreamableHTTP with SSE fallback; interpolation supported, missing URL vars fail before any request |
|
||||
| `auth` | `"bearer"` or `"oauth"` |
|
||||
| `oauth.*` | `grantType` (`authorization_code` default, or `client_credentials`), `clientId`, `clientSecret`, `scope`, `redirectUri` (exact localhost callback for pre-registered clients), `clientName`, `clientUri` |
|
||||
| `oauth.*` | `grantType` (`authorization_code` default, or `client_credentials`), `clientId` (MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; Dynamic Client Registration is the fallback when omitted), `clientSecret`, `scope`, `redirectUri` (exact localhost callback for pre-registered clients), `clientName`, `clientUri` (defaults to the host manifest's `piConfig.clientUri`, omitted under a rebranded host), `logoUri` (absolute `http(s)` URL, RFC 7591 `logo_uri`), `authorizationParams` (extra authorization-URL parameters; flow-owned ones such as `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `response_type`, `resource` cannot be overridden), `skipIssuerMetadataValidation` |
|
||||
| `bearerToken` / `bearerTokenEnv` | Token or env var name; interpolation and `!command` supported |
|
||||
| `lifecycle` | `"lazy"` (default), `"eager"`, `"keep-alive"`, `"lazy-keep-alive"` |
|
||||
| `idleTimeout` | Minutes before idle disconnect (overrides global) |
|
||||
| `requestTimeoutMs` | Per-server request timeout; omitted or `<= 0` uses the MCP SDK default |
|
||||
| `protocolVersion` | `"legacy"` (default), `"auto"`, or `"2026-07-28"` — modern negotiation is opt-in |
|
||||
| `exposeResources` | Expose MCP resources as tools (default `true`) |
|
||||
| `directTools` | `true`, `string[]`, or `false` |
|
||||
| `toolPrefix` | Per-server override of the global `toolPrefix` |
|
||||
| `includeTools` / `excludeTools` | Names or glob patterns; `excludeTools` applies after `includeTools` |
|
||||
| `searchKeywords` | `{ "tool-or-glob": ["keyword", …] }` extra keywords boosting `mcp({ search })` ranking; never shown to the model |
|
||||
| `approveTools` | `true` or glob array requiring approval before calls (overrides the global setting) |
|
||||
| `debug` | Show server stderr (default `false`) |
|
||||
| `trace` | Metadata-only JSONL protocol tracing for this server |
|
||||
| `disabled` | Keep visible in config/status but block connections, auth, tools, resources (only literal `true`) |
|
||||
|
||||
Secret values in `headers`, `bearerToken`, `oauth.clientSecret`, and stdio `env` may use a leading `!command` resolved at connect/auth time: stdin and stderr suppressed, stdout capped at 1 MiB and trimmed, 10-second limit, non-empty output required. Commands never run during discovery, merging, previewing, hashing, or rendering config.
|
||||
|
||||
`oauth.skipIssuerMetadataValidation: true` disables the RFC 8414 issuer echo check for one server. It weakens OAuth mix-up protection — use it only for a known-misconfigured internal server while its metadata is being fixed, never for public or untrusted servers.
|
||||
|
||||
**Protocol version negotiation** — the default `"legacy"` uses the classic MCP initialize sequence with no `server/discover` or 2026 headers, preserving compatibility with deployed 2025-era servers. `"auto"` probes for MCP 2026-07-28 and conservatively falls back to the classic handshake on legacy evidence; set it for Cloudflare Workers `createMcpHandler` and other MCP SDK v2 stateless servers. For stdio servers, `"auto"` probes with a short-lived sibling process before starting the session process, so each fresh connection adds one spawn and may wait out the request timeout; explicit Unix sockets probe in place. HTTP auto negotiation uses the real Streamable HTTP connection and falls back to legacy SSE only on definitive rejection (404/405/406/415) — never on auth failures, cancellation, timeouts, or server errors. `"2026-07-28"` pins that revision with no legacy or SSE fallback. Strict OAuth issuer validation applies in every mode. Adapter-level roots support, standard MCP logging presentation, and protocol cache-hint config are not yet implemented.
|
||||
|
||||
**Lifecycle modes** — `lazy`: connect on first tool call, disconnect after idle, cached metadata keeps search working. `eager`: connect at startup, no auto-reconnect, no idle timeout unless set. `keep-alive`: connect at startup with health checks and auto-reconnect. `lazy-keep-alive`: connect on first use, then stay resident with auto-reconnect. Any enabled `eager`/`keep-alive` server also triggers initialization at extension load, supporting hosts that never emit `session_start`.
|
||||
|
||||
**rmcp-mux** — point `socket` at an [`rmcp-mux`](https://github.com/VetCoders/rmcp-mux) service socket to share one stdio server across Pi sessions. The adapter owns only its client socket; the mux owns the upstream process, routing, restart policy, and socket permissions. A socket is an explicit trusted local endpoint.
|
||||
@@ -79,11 +99,36 @@ Secret values in `headers`, `bearerToken`, `oauth.clientSecret`, and stdio `env`
|
||||
{ "settings": { "toolPrefix": "server", "idleTimeout": 10, "requestTimeoutMs": 30000, "trace": { "enabled": true } } }
|
||||
```
|
||||
|
||||
`toolPrefix` (`"server"` default, `"short"` strips a `-mcp` suffix, `"none"`, `"mcp"` prefixes `mcp__`), `idleTimeout` (minutes, default 10, `0` disables), `requestTimeoutMs`, `showStatusIcon` (default `true`), `hostConfigDiscovery`, `oauthDir`, `directTools` (global default, default `false`), `disableProxyTool`, `autoAuth` (default `false`), `sampling` (default `true` when UI approval is available; honors `modelPreferences.hints`), `samplingAutoApprove` (required for sampling in non-UI sessions), `elicitation` (default `true` with UI), `outputGuard`, `trace` (`{ enabled, file, maxBytes: 262144, maxEvents: 10000 }`; the per-session JSONL defaults to `.pi/mcp-traces/` and never records payloads, prompts, arguments/results, auth data, or URLs).
|
||||
`toolPrefix` (`"server"` default, `"short"` strips a `-mcp` suffix, `"none"`, `"mcp"` prefixes `mcp__`; per-server `toolPrefix` overrides it), `idleTimeout` (minutes, default 10, `0` disables), `requestTimeoutMs`, `showStatusIcon` (default `true`), `mcpFooterStatus` (`"full"` default, `"compact"`, `"off"`), `toolResultRendering` (`"compact"` default self-rendered rows, or `"boxed"` for the legacy Pi tool row), `collapsedResultLines` (`1`–`3`; defaults `1` compact / `3` boxed), `notifyOnStartupConnect` (default `true`; `false` suppresses routine connect notices but keeps errors and auth warnings), `hostConfigDiscovery`, `agentPluginPaths`, `approveTools`, `oauthDir`, `directTools` (global default, default `false`), `freezeDirectTools` (default `false`), `scriptMode` (default `true`; registers the MCP-only `mcpScript` plain-JavaScript tool), `disableProxyTool`, `autoAuth` (default `false`), `sampling` (default `true` when UI approval is available; honors `modelPreferences.hints`), `samplingAutoApprove` (required for sampling in non-UI sessions), `elicitation` (default `true` with UI), `outputGuard`, `trace` (`{ enabled, file, maxBytes: 262144, maxEvents: 10000 }`; the per-session JSONL defaults to `.pi/mcp-traces/` and never records payloads, prompts, arguments/results, auth data, or URLs).
|
||||
|
||||
Per-server `idleTimeout`, `requestTimeoutMs`, and `approveTools` override the global values.
|
||||
|
||||
### Tool Approval
|
||||
|
||||
`approveTools` keeps a tool visible but gates the call — useful for destructive or high-cost actions where hiding the tool would hurt planning:
|
||||
|
||||
```json
|
||||
{ "settings": { "approveTools": ["github_delete_*", "notion_update_*"] },
|
||||
"mcpServers": { "github": { "approveTools": ["delete_*", "merge_pull_request"] }, "docs": { "approveTools": false } } }
|
||||
```
|
||||
|
||||
A matching call from the proxy tool, a direct MCP tool, `mcpScript`, a resource call, or an MCP UI iframe prompts **Allow once** / **Allow for session** / **Deny**; session approvals live in memory only, and headless sessions fail closed with an `approval_required` result. `excludeTools` still removes tools entirely — `approveTools` only gates visible ones.
|
||||
|
||||
Permission extensions can broker decisions by listening on `MCP_TOOL_APPROVAL_REQUEST_EVENT` (`pi-mcp-adapter:tool-approval-request`) and claiming the request synchronously with `request.claim(async () => "allow_once" | "allow_for_session" | "deny" | "abstain")`. The request carries `serverName`, `originalToolName`, `prefixedToolName`, `args`, `origin`, and an optional `signal`; the first synchronous claim wins, `allow_for_session` updates the same in-memory cache as the dialog, and `abstain`/no claim keeps the fallback behavior. Brokered approval runs for every uncached MCP call regardless of `approveTools` config.
|
||||
|
||||
### Search Keywords
|
||||
|
||||
Search matches literally, so per-server `searchKeywords` adds vocabulary for tools whose names and descriptions use different words:
|
||||
|
||||
```json
|
||||
{ "mcpServers": { "github": { "searchKeywords": { "search_code": ["grep"], "*": ["gh"] } } } }
|
||||
```
|
||||
|
||||
Keys match a tool's original name, prefixed name, or a glob (`*` covers every tool on the server), and all matching entries combine. Keywords are weighted like description text with an extra boost on exact phrase matches. They affect ranked and regex search only (including `tools.search` in `mcpScript`) and never appear in tool schemas, `describe` output, direct-tool registration, or the metadata cache — so keyword search works offline from cached metadata.
|
||||
|
||||
## Output Guard
|
||||
|
||||
On by default: inline text is capped at **50 KiB / 2000 lines** (matching Pi's `bash` guard), with the full text spilled to a temp file whose path is included so the agent can `read`/`grep` it. Image blocks pass through unchanged. In proxy mode `details.mcpResult` stays raw when its JSON is ≤ 16 KiB; larger results become a compact summary with the raw JSON spilled to a temp file (direct tools never carry `mcpResult`). Tune with `{ maxBytes, maxLines, detailsMaxBytes }`; disable with `"outputGuard": false` or `MCP_OUTPUT_GUARD=0`. Temp files are mode `0600` under the system temp dir and are not cleaned up automatically.
|
||||
On by default: inline text is capped at **50 KiB / 2000 lines** (matching Pi's `bash` guard), with the full text spilled to a temp file whose path is included so the agent can `read`/`grep` it. Image blocks pass through unchanged. Binary resource blobs up to **10 MiB** are decoded to private temp files and replaced with file references, bounded to 100 MiB and 10,000 files per session and removed at session teardown. In proxy mode `details.mcpResult` stays raw when its JSON is ≤ 16 KiB; larger results become a compact summary with the raw JSON spilled to a temp file (direct tools never carry `mcpResult`). Tune with `{ maxBytes, maxLines, detailsMaxBytes }`; disable with `"outputGuard": false` or `MCP_OUTPUT_GUARD=0`. Temp files are mode `0600` under the system temp dir and are not cleaned up automatically.
|
||||
|
||||
## Direct Tools
|
||||
|
||||
@@ -95,12 +140,14 @@ On by default: inline text is capped at **50 KiB / 2000 lines** (matching Pi's `
|
||||
|
||||
Direct tools register from the metadata cache (`~/.pi/agent/mcp-cache.json`, or `$PI_CODING_AGENT_DIR/mcp-cache.json`), so no startup connections are needed. The first session after adding `directTools` falls back to proxy-only while the cache populates, then hot-loads. Servers advertising list-change notifications refresh the current session. Force a refresh with `/mcp reconnect <server>`.
|
||||
|
||||
Set `settings.freezeDirectTools: true` when prompt-cache stability matters more than hot-loading: the initial sync still runs, but later automatic reconnects, lazy-connects, and list-change notifications leave the registered tool surface unchanged. Deliberate refreshes via `mcp({ connect: "server" })` or `/mcp reconnect <server>` still update it.
|
||||
|
||||
## Proxy Tool API
|
||||
|
||||
```javascript
|
||||
mcp({ }) // status / list servers
|
||||
mcp({ server: "name" }) // server details (+ instructions preview)
|
||||
mcp({ search: "screenshot navigate" }) // search tools (OR'd words)
|
||||
mcp({ search: "screenshot navigate", limit: 12, offset: 0 }) // ranked tool search
|
||||
mcp({ describe: "tool_name" })
|
||||
mcp({ instructions: "name" }) // full server instructions
|
||||
mcp({ tool: "chrome_devtools_take_screenshot", args: { format: "png" } })
|
||||
@@ -110,13 +157,13 @@ mcp({ action: "auth-start", server: "name" })
|
||||
mcp({ action: "auth-complete", server: "name", args: { redirectUrl: "http://localhost:19876/callback?code=...&state=..." } })
|
||||
```
|
||||
|
||||
`args` accepts a JSON object or a JSON string. Search covers MCP tools **and** Pi extension tools (prefixed `[pi tool]`, listed first). Names fuzzy-match on hyphens and underscores. Server `instructions` surface at three levels: a truncated head in the proxy tool description, a longer preview in `mcp({ server })`, and the full text via `mcp({ instructions })` — captured at connect time and cached.
|
||||
`args` accepts a JSON object or a JSON string. Search covers MCP tools **and** Pi extension tools (prefixed `[pi tool]`, listed first). Space-separated words are ranked by weighted matches across name, server, description, and any configured `searchKeywords`, then paginated (`limit` defaults to 12; follow `details.nextOffset`). `regex: true` still works but paginates without ranking. Names fuzzy-match on hyphens and underscores, and an unresolvable `describe`/`tool` name returns top suggestions so the agent can fix a typo in the same turn. With `includeSchemas`, search and describe render common JSON Schema parameters as compact TypeScript shapes like `{ query: string; limit?: number; }`. For HTTP servers, a failed connect runs a one-request shape probe that turns opaque transport errors into hints such as `endpoint returned HTML (200) — this URL does not appear to speak MCP`. Server `instructions` surface at three levels: a truncated head in the proxy tool description, a longer preview in `mcp({ server })`, and the full text via `mcp({ instructions })` — captured at connect time and cached.
|
||||
|
||||
Remote/headless OAuth uses `auth-start` then `auth-complete` (pass `redirectUrl`, or just `args: { code }`). Persistent OAuth requires an available OS credential store — on headless Linux, an unlocked Secret Service/libsecret keyring; the adapter fails closed rather than storing plaintext.
|
||||
Remote/headless OAuth: `/mcp-auth <server>` first shows a clickable authorization URL. Open it in your local browser, approve, then select **Yes** in Pi to open the callback input — the browser's localhost callback page will usually fail to load (localhost is your workstation), so copy the full URL from its address bar and paste it into Pi. When the browser can reach Pi's callback directly, the authorization screen closes on its own instead. The same flow is available through the proxy tool (`auth-start` then `auth-complete` with `redirectUrl` or `args: { code }`) for non-interactive clients. Persistent OAuth requires an available OS credential store — on headless Linux, an unlocked Secret Service/libsecret keyring; the adapter fails closed rather than storing plaintext. On Linux, when credential access fails because Pi inherited a revoked session keyring, the adapter attempts recovery through `keyctl session - node <packaged helper>` (requires `keyctl` and `node` on `PATH`) so re-authentication can write fresh credentials without killing a long-lived tmux server.
|
||||
|
||||
## Commands
|
||||
|
||||
`/mcp` (interactive panel: status, tools, direct/proxy toggles, reconnect, `ctrl+a` or Enter for OAuth), `/mcp setup`, `/mcp tools`, `/mcp prompts`, `/mcp reconnect [server]`, `/mcp disable <server>`, `/mcp enable <server>`, `/mcp logout <server>`, `/mcp-auth [server]`.
|
||||
`/mcp` (interactive panel: status, tools, direct/proxy toggles, reconnect, `ctrl+a` or Enter for OAuth, Save on `ctrl+s` — remappable via the `mcp.panel.save` keybinding), `/mcp setup` (imports, a minimal `.mcp.json`, curated known servers — DeepWiki, Context7, Notion, GitHub, Chrome DevTools — RepoPrompt quick-add, config-path inspection), `/mcp tools`, `/mcp prompts`, `/mcp reconnect [server]`, `/mcp disable <server>`, `/mcp enable <server>`, `/mcp logout <server>`, `/mcp-auth [server]`.
|
||||
|
||||
## Prompts, Elicitation, UI
|
||||
|
||||
@@ -124,7 +171,7 @@ MCP prompt templates register as slash commands `/mcp__<server>__<prompt>`, refr
|
||||
|
||||
Elicitation forms use Pi's `select()`/`input()` dialogs with validation and a review step; explicit refusal maps to MCP `decline`, dismissal to `cancel`. URL mode is TUI-only, always shows requesting server/host/URL, and requires consent; `-32042` URL-required tool errors are handled — retry the original call after completing the browser step.
|
||||
|
||||
MCP UI resources open in a native macOS window via Glimpse (`pi install npm:glimpseui`) or fall back to the browser. `MCP_UI_VIEWER=browser|glimpse|none` forces or suppresses the viewer (`none` still runs the tool and returns inline results). UIs talk back — message types `prompt`, `intent`, `notify`, `message`, plus custom types forwarded as intents — retrieved with `mcp({ action: "ui-messages" })` (each with `type`, `sessionId`, `serverName`, `toolName`, `timestamp`). Calling the same tool again pushes a new result into the open window instead of replacing it. Tool consent gates whether UIs may call MCP tools (never / once-per-server / always). Browser controls: Cmd/Ctrl+Enter completes, Escape cancels.
|
||||
MCP UI resources open in a native macOS window via Glimpse (`pi install npm:glimpseui`) or fall back to the browser. `MCP_UI_VIEWER=browser|glimpse|none` forces or suppresses the viewer (`none` still runs the tool and returns inline results). UIs talk back — message types `prompt`, `intent`, `notify`, `message`, plus custom types forwarded as intents — retrieved with `mcp({ action: "ui-messages" })` (each with `type`, `sessionId`, `serverName`, `toolName`, `timestamp`). Calling the same tool again pushes a new result into the open window instead of replacing it. Tool consent gates whether UIs may call MCP tools (never / once-per-server / always), and `_meta.ui.visibility` controls audience — app-only tools stay out of the model tool list, model-only tools cannot be called from the UI iframe. Browser controls: Cmd/Ctrl+Enter completes, Escape cancels.
|
||||
|
||||
## Status Snapshots
|
||||
|
||||
@@ -137,8 +184,8 @@ Includes `totalTools`, `totalResources`, `connectedCount`, `disabledCount`, and
|
||||
|
||||
## Behavior Notes and Limitations
|
||||
|
||||
npx-based servers resolve to direct binaries, skipping the ~143 MB npm parent process. Advertised `outputSchema` supports JSON Schema draft-07 and 2020-12 (unstamped schemas use the SDK's 2020-12 default), and returned `structuredContent` is validated for both proxy and direct calls. Results render compactly (first three wrapped lines plus a Ctrl+O expand hint) while the model still receives the full result.
|
||||
npx-based servers resolve to direct binaries, skipping the ~143 MB npm parent process. Advertised `outputSchema` supports JSON Schema draft-07 and 2020-12 (unstamped schemas use the SDK's 2020-12 default), and returned `structuredContent` is validated for both proxy and direct calls. Results use compact self-rendered rows by default — collapsed success output shows the call title and the first result line plus a `Ctrl+O to expand` hint — while the model still receives the full result. Set `toolResultRendering: "boxed"` for the legacy row, or `collapsedResultLines` to `2`/`3` for more collapsed text.
|
||||
|
||||
Limitations: no cross-session server sharing (each Pi session runs its own server processes, unless using rmcp-mux); MCP sampling is text-only (context inclusion, tools, stop sequences, audio, and images are rejected); inline images follow Pi's image display settings.
|
||||
Limitations: no cross-session server sharing (each Pi session runs its own server processes, unless using rmcp-mux); MCP sampling is text-only (context inclusion, tools, stop sequences, audio, and images are rejected); inline images follow Pi's image display settings; Pi still owns one separator row before self-rendered tool output, so compact mode reduces but cannot eliminate the gap.
|
||||
|
||||
Subagents (`pi-subagents`) receive direct MCP tools only when listed with an `mcp:` prefix in their `tools:` frontmatter — a global `directTools: true` is not enough. See `references/pi-subagents.md`.
|
||||
|
||||
@@ -1,74 +1,138 @@
|
||||
# pi-subagents Package
|
||||
|
||||
Source: https://pi.dev/packages/pi-subagents
|
||||
Source: https://pi.dev/packages/pi-subagents (docs: `https://github.com/nicobailon/pi-subagents/tree/main/docs`)
|
||||
|
||||
Delegate tasks to focused child agents with sequential chains, parallel groups, dynamic fanout, worktree isolation, acceptance gates, and background runs.
|
||||
Delegate work to focused child Pi sessions: code review, scouting, implementation, parallel audits, saved workflows, background jobs. Installing the extension does not start anything automatically — it gives Pi a `subagent` delegation tool.
|
||||
|
||||
```bash
|
||||
pi install npm:pi-subagents
|
||||
```
|
||||
|
||||
Users normally ask in plain language ("Use reviewer to review this diff", "Run parallel reviewers for correctness, tests, and complexity"). Pi decides whether to call the tool, which agent to use, and how to compose the work.
|
||||
|
||||
## Built-in Agents
|
||||
|
||||
| Agent | Purpose |
|
||||
|---|---|
|
||||
| `scout` | Fast local codebase recon: relevant files, entry points, data flow, risks, where to start |
|
||||
| `researcher` | Web/docs research with sources; needs `pi-web-access` for `web_search`/`fetch_content`/`get_search_content` |
|
||||
| `planner` | Concrete implementation plan from existing context; reads and plans, does not edit |
|
||||
| `worker` | Implementation, including approved oracle handoffs; escalates unapproved decisions |
|
||||
| `reviewer` | Code review and small fixes against task/plan, tests, edge cases, simplicity |
|
||||
| `context-builder` | Stronger setup pass: gathers code context and writes handoff material (`context.md`, `meta-prompt.md`) |
|
||||
| `oracle` (alias `advisor`) | Second opinion before acting; challenges assumptions, no edits |
|
||||
| `delegate` | Lightweight general delegate close to parent behavior (uses append prompt mode) |
|
||||
| `delegate` | Lightweight general delegate close to parent behavior (append prompt mode) |
|
||||
|
||||
Builtins inherit the current Pi default model unless `subagents.defaultModel` or an override says otherwise. Packaged `planner`, `worker`, `oracle`, and `advisor` default to `context: "fork"`; others default to `fresh`. Builtins opt into project-instruction inheritance so they follow repo rules.
|
||||
Builtins load at the lowest priority and inherit the current Pi default model unless `subagents.defaultModel` or an override says otherwise. Packaged `worker`, `oracle`, and `advisor` default to `context: "fork"`; others default to `fresh`. Builtins opt into project-instruction inheritance so they follow repo rules.
|
||||
|
||||
Recommended implementation loop: **clarify → scout → worker → fresh reviewers → worker**.
|
||||
|
||||
## Execution: workflowScript
|
||||
|
||||
All model-facing execution goes through `workflowScript` — an ordinary JavaScript statement body with an explicit `return`. The legacy `/chain`, `/parallel`, `/run-chain`, and `/chain-prompts` commands are no longer registered, and `.chain.md`/`.chain.json` files exist only as durable legacy chains.
|
||||
|
||||
```javascript
|
||||
// One child
|
||||
subagent({ workflowScript: `return runs.run("main", { agent: "scout", task: "Analyze the auth flow" })` })
|
||||
|
||||
// Sequential
|
||||
subagent({ workflowScript: `
|
||||
const scan = await runs.run("scan", { agent: "scout", task: "Analyze auth" });
|
||||
return (await runs.run("implement", { agent: "worker", task: "Implement from: " + scan.output })).output;
|
||||
` })
|
||||
|
||||
// Parallel
|
||||
subagent({ workflowScript: `
|
||||
const reviews = await runs.all([
|
||||
{ key: "correctness", agent: "reviewer", task: "Review correctness" },
|
||||
{ key: "tests", agent: "reviewer", task: "Review tests" }
|
||||
]);
|
||||
return reviews.map(r => r.output);
|
||||
` })
|
||||
```
|
||||
|
||||
Globals inside the script: `runs.run(key, opts)`, `runs.all([items])`, `runs.ref`, `state.get/set` (durable mission JSON state), and `prompts.render(ref, vars?)`. For long task text containing Markdown fences or shell blocks, build the string from quoted lines joined with `\n` rather than a raw template literal.
|
||||
|
||||
Workflows default to background execution; pass `async: false` for a watched foreground run with a live in-chat card (`chatProgress` forces `auto`/`off`/`live-card`). Foreground workflows default to a 30-minute timeout; async workflows have no default top-level timeout.
|
||||
|
||||
`prompts.render` needs an explicit scope: `package:<name>`, `user:<name>`, or `project:<name>`, each naming a top-level `<name>.md`. Frontmatter is stripped, scalar `{{name}}` placeholders are substituted, and unknown placeholders stay unchanged. Rendering returns text only — pass it explicitly as `task`.
|
||||
|
||||
### Key Tool Parameters
|
||||
|
||||
`agent`, `action`, `topic`, `chainName`, `config`, `context` (`fresh`/`fork` — an explicit value overrides every workflow child; otherwise each child uses its own `defaultContext`), `missionId`, `mission` (object or `false`), `handoffPath`, `view` (`fleet`/`transcript`), `lines` (default 80, max 500), `agentScope`, `async`, `chatProgress`, `timeoutMs`/`maxRuntimeMs`, `toolTimeoutMs`, `turnBudget`, `toolBudget`, `usageBudget`, `cwd`, `maxOutput` (200 KB / 5000 lines), `artifacts`, `includeProgress`, `share`, `sessionDir`, `acceptance`, `gate`, plus per-item `output`, `outputMode`, `skill`, `model`, `worktree`, `resume`.
|
||||
|
||||
Budgets: `turnBudget` is `{ maxTurns, graceTurns }` (warn at `maxTurns`, terminate at the next assistant boundary after the grace window); `toolBudget` is `{ soft?, hard, block? }` (block defaults to `read`/`grep`/`find`/`ls`; `"*"` blocks everything, final assistant text never blocked); `usageBudget` is root-only `{ tokens?: { soft?, hard }, costUsd?: { soft?, hard } }` where soft limits are status-only and hard limits prevent later child launches without stopping running ones. **Do not** set turn, hard tool, or tight usage budgets on mutation-capable children (implementation workers, fix workers, reviewers with edit authority) — none of those measure whether a delivery slice is buildable, and a default tool budget blocks read/search tools rather than mutations. Bound writers with a narrow task and an outer `timeoutMs` instead, and request a checkpoint via `steer` before the deadline.
|
||||
|
||||
`context: "fork"` fails fast when the parent session is not persisted, the leaf is missing, or the branched session cannot be created — it never silently downgrades to `fresh`. Forking strips signed Anthropic `thinking`/`redacted_thinking` blocks from the child session and forces thinking `off` when the child's effective primary or fallback model resolves to the Anthropic provider or `anthropic-messages` API (unresolved models are treated conservatively). Use `fresh` when an Anthropic child needs thinking.
|
||||
|
||||
`outputMode: "file-only"` returns a compact pointer (`Output saved to: /abs/report.md (48.2 KB, 2847 lines)…`) instead of inline text; failed runs and save errors still return inline output for debugging. A read-only child does not need filesystem access for `output` — it returns the artifact in its final response and the runtime persists it.
|
||||
|
||||
### Retained Children
|
||||
|
||||
Completed workflow children from the current parent session stay addressable. `{ action: "children.list" }` lists up to the last 10 with run ids; a later workflow continues one by passing `resume` instead of `agent`:
|
||||
|
||||
```javascript
|
||||
subagent({ workflowScript: `
|
||||
let writer = await runs.run("implement", { agent: "worker", task: "Implement the accepted contract" });
|
||||
for (const pass of [1, 2]) {
|
||||
const task = await prompts.render("project:writer-followup", { pass, previous: writer.output });
|
||||
writer = await runs.run("followup-" + pass, { resume: writer.runId, task });
|
||||
}
|
||||
return writer;
|
||||
` })
|
||||
```
|
||||
|
||||
Each resume can return a new retained run id, so loops must continue from the latest `runId`. `resume` and `agent` are mutually exclusive, the revived child keeps its stored agent/model/tool contract, and `gate` is rejected on resume items. Top-level `{ action: "resume" }` stays detached and returns a background receipt — use it for a simple challenge outside a script; use `runs.run({ resume })` only when the script must await the revived output. `steer` with `mode: "follow_up"` only queues text for the next `resume`; it does not revive a completed child.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
/run <agent> [task] # single agent; --bg detached, --fork branched child session
|
||||
/chain scout "scan" -> planner "plan" # sequential
|
||||
/chain scout "scan" -> (reviewer "A" | reviewer "B")[concurrency=2,failFast,worktree] -> writer "fix"
|
||||
/parallel scanner "security" -> reviewer "style"
|
||||
/run-chain <chainName> -- <task> # saved .chain.md / .chain.json
|
||||
/subagent-cost # parent + child token usage and cost
|
||||
/subagents [agent] [model|thinking|prompt|details]
|
||||
/run <agent> [task] [--bg] [--fork] # one child
|
||||
/subagents-fleet # live fleet inspector
|
||||
/subagents-stop [run-id] # stop a top-level async run
|
||||
/subagents-detach [run-id] # leave a foreground run running
|
||||
/subagents-doctor # read-only setup diagnostics
|
||||
/subagents-models [agent] # runtime-loaded model mapping
|
||||
/subagents-guide [topic] # packaged docs for the installed version
|
||||
/subagents-refine <agent> # project-local refinement overlay
|
||||
/subagents-models [agent] # live runtime model mapping
|
||||
/subagents-watchdog [status|on|off|recommend-model|model ...|session model ...|check]
|
||||
/subagents-fleet # live fleet inspector (inspection only)
|
||||
/subagents-stop [run-id]
|
||||
/subagents-profiles | /subagents-load-profile <name> | /subagents-check-profile <name>
|
||||
/subagents-refresh-provider-models <provider> [--force] | /subagents-generate-profiles <provider>
|
||||
/subagents-refresh-provider-models <provider> [--force]
|
||||
/subagents-generate-profiles <provider> | /subagents-load-profile <name> | /subagents-check-profile <name>
|
||||
/prompt-workflow <template> [args] # run a subagent prompt template
|
||||
```
|
||||
|
||||
Steps use `->`; a shared task uses one `--` before the task (`/chain scout planner -- analyze auth`). Inline parallel groups wrap two or more agents in `( ... )` separated by ` | `, only as a complete step, and only when the step opens with `(`. Dynamic fanout is not available inline — use the tool API or a `.chain.json`.
|
||||
Per-run overrides use bracket syntax on the agent name: `/run reviewer[model=anthropic/claude-sonnet-4:high] "Review this diff"`.
|
||||
|
||||
Per-step config appends `[key=value,...]` to the agent name: `output`, `outputMode` (`inline`/`file-only`), `reads` (`a.md+b.md`), `model`, `skills` (`a+b`), `progress`, plus chain-only `as`, `label`, `phase`, `cwd`, `count`, `outputSchema`, `acceptance`. Values must not contain spaces or commas. `output=false`, `reads=false`, `skills=false` disable explicitly. `/run` and `/parallel` ignore chain-only keys.
|
||||
Packaged prompt shortcuts: `/parallel-review`, `/review-loop`, `/parallel-research`, `/gather-context-and-clarify`, `/parallel-cleanup` (add `autofix` to `/parallel-review` or `/parallel-cleanup` to apply only the synthesized fixes worth doing now).
|
||||
|
||||
Add `--bg` for background and `--fork` to branch each child from the parent's current leaf (combinable in either order).
|
||||
`/subagents-guide` and `{ action: "guide", topic }` read the packaged docs for the installed version. Topics: `overview`, `workflows`, `agents`, `missions`, `observability`, `tool-reference`, `configuration`, `models`, `watchdog`, `extension-api`.
|
||||
|
||||
Packaged prompt shortcuts: `/parallel-review`, `/review-loop`, `/parallel-research`, `/parallel-context-build`, `/parallel-handoff-plan`, `/gather-context-and-clarify`, `/parallel-cleanup` (add `autofix` to `/parallel-review` or `/parallel-cleanup` to apply the synthesized fixes).
|
||||
|
||||
## Programmatic API (`subagent` tool)
|
||||
## Management, Status, and Control Actions
|
||||
|
||||
```javascript
|
||||
{ agent: "worker", task: "refactor auth" }
|
||||
{ tasks: [{ agent: "scout", task: "audit frontend" }, { agent: "reviewer", task: "audit backend" }], count: 3 }
|
||||
{ chain: [{ agent: "scout", task: "Gather context" }, { agent: "planner" }, { agent: "worker" }] }
|
||||
{ chain: [...], async: true, clarify: true }
|
||||
{ action: "list" | "get" | "create" | "update" | "delete" | "enable" | "disable" | "status" | "interrupt" | "resume" | "stop" | "doctor" }
|
||||
{ action: "status", id: "<run-id>", view: "fleet" | "transcript", index: 0 }
|
||||
{ action: "resume", id: "<run-id>", message: "follow-up" }
|
||||
{ action: "watchdog.recommend-model" }
|
||||
{ action: "watchdog.configure", model: "recommended", scope: "session" | "user" | "project" }
|
||||
{ action: "list" | "get" | "create" | "update" | "delete" | "eject" | "enable" | "disable" | "reset" }
|
||||
{ action: "children.list" }
|
||||
{ action: "refine" | "refine.show" | "refine.rollback", agent: "reviewer" }
|
||||
{ action: "status" } // all active runs
|
||||
{ action: "status", view: "fleet" }
|
||||
{ action: "status", id: "<run-id>", view: "transcript", index: 0, lines: 80 }
|
||||
{ action: "interrupt" | "stop", id: "<run-id>" }
|
||||
{ action: "resume", id: "<run-id>", index: 1, message: "follow-up" }
|
||||
{ action: "steer", id: "<run-id>", mode: "steer" | "follow_up" | "auto", message: "guidance" }
|
||||
{ action: "grant-spawn-budget", additional: 10 }
|
||||
{ action: "doctor" }
|
||||
{ action: "watchdog.recommend-model" } | { action: "watchdog.configure", model: "recommended", scope: "session" | "user" | "project" }
|
||||
{ action: "mission.create" | "mission.list" | "mission.show" | "mission.update"
|
||||
| "mission.resolve-decision" | "mission.attach-run" | "mission.close" }
|
||||
{ action: "schedule.create" | "schedule.list" | "schedule.show" | "schedule.history"
|
||||
| "schedule.pause" | "schedule.resume" | "schedule.run" | "schedule.run-due" | "schedule.delete" }
|
||||
{ action: "inspector.open" | "inspector.status" | "inspector.close", id, index, focus }
|
||||
{ action: "project.open" | "project.status" | "project.close", cwd, message }
|
||||
```
|
||||
|
||||
Key parameters: `output` (file or `false`), `outputMode`, `skill` (string/array/`false`), `model`, `concurrency`, `worktree`, `context` (`fresh`/`fork`), `chainDir`, `clarify` (tool calls launch directly unless set), `async`, `cwd`, `timeoutMs`/`maxRuntimeMs`, `turnBudget`, `maxOutput`, `acceptance`.
|
||||
`create` uses `config.scope` (not `agentScope`); `config.package` registers the runtime name as `{package}.{name}`; `config.aliases` accepts a comma-separated string, array, or `false`. Clear optional string fields with `false` or `""`. `eject` copies a bundled builtin or package agent verbatim into the user/project agent dir as an editable shadow; `reset` deletes the scope's custom file and/or override entry, restoring the bundled default (it refuses when no bundled default exists — use `delete` for purely custom agents). These accept `agentScope: "user" | "project"` and operate on one scope at a time; a project-scope disable survives a user-scope enable.
|
||||
|
||||
`subagent_wait` blocks on background work: `{ all: true }`, `{ id }`, `{ timeoutMs }`. Background runs are detached — prefer returning control and letting Pi deliver the completion notification, and use `subagent_wait` only when the current turn must have results before it ends. Headless sessions auto-drain current-session work at `agent_end` as a safeguard.
|
||||
`status` resolves exact foreground ids, top-level async ids, and nested run ids before prefix matching. `stop` is stronger than `interrupt`: it is not a resumable pause, rejects foreground and nested targets, and stopped runs must be restarted as new runs. `resume` revives a paused, completed, or failed child from its stored session file by starting a *new* child process, taking an exclusive cross-process lease on the canonical session file. `steer` waits up to three seconds for correlated acceptance and returns a request id with `delivered`/`scheduled`/`pending`/`partial`/`recovered`/`failed` plus `deliveryStatus: "delivered" | "queued"`; the FIFO holds 20 messages and the persisted `steering` ledger retains 20 requests. `append-step`, `approve-checkpoint`, and `reject-checkpoint` require `legacyChainControls: true`.
|
||||
|
||||
Dynamic fanout (`.chain.json` or tool API only): a step with `expand: { from: { output: "name", path: "/items" }, item: "target", key: "/path", maxItems: N }` plus `parallel: { agent, label, task: "Review {target.path}", outputSchema }` and `collect: { as: "reviews" }`. The source must be structured output (`as` + `outputSchema`); prose is never parsed, `maxItems` is required, and nested fanout is unsupported.
|
||||
`subagent_wait` blocks on background work: `{ all: true }`, `{ id }`, `{ timeoutMs }`. Background runs are detached — prefer returning control and letting Pi deliver the completion notification, and use `subagent_wait` only when the current turn must have results before it ends. `{ id, nonBlocking: true }` resolves the prefix once, returns a subscription token immediately, and wakes the session on completion/failure/attention/timeout. Headless sessions auto-drain current-session work at `agent_end` as a safeguard.
|
||||
|
||||
## Agent Definition Files
|
||||
|
||||
@@ -79,6 +143,7 @@ Markdown with YAML frontmatter. Precedence low → high: builtin (`~/.pi/agent/e
|
||||
name: scout
|
||||
package: code-analysis # registers as code-analysis.scout
|
||||
description: Fast codebase recon
|
||||
aliases: explorer, code-scout
|
||||
tools: read, grep, find, ls, bash, mcp:chrome-devtools
|
||||
extensions: # omitted = all; empty = none; list = allowlist
|
||||
subagentOnlyExtensions: ./tools/child-only-search.ts
|
||||
@@ -96,13 +161,15 @@ defaultReads: context.md
|
||||
defaultProgress: true
|
||||
async: true
|
||||
timeoutMs: 900000
|
||||
toolTimeoutMs: 600000
|
||||
turnBudget: {"maxTurns":20,"graceTurns":2}
|
||||
acceptance: {"level":"none","reason":"lightweight lookup"}
|
||||
acceptanceRole: read-only # or writer
|
||||
completionGuard: false # false for non-implementation validators
|
||||
interactive: true # parsed, not enforced in v1
|
||||
interactive: true # parsed, not enforced
|
||||
maxSubagentDepth: 1
|
||||
memory: { scope: project, path: security-reviewer }
|
||||
permission: { write: allow, edit: ask }
|
||||
---
|
||||
Your system prompt goes here.
|
||||
```
|
||||
@@ -111,38 +178,15 @@ Scalar list fields (`tools`, `defaultReads`, `skills`, `skillPath`, `fallbackMod
|
||||
|
||||
Custom agents start with a clean prompt: they do not inherit Pi's base prompt, project instruction files, or the skills catalog unless `systemPromptMode: append`, `inheritProjectContext: true`, or `inheritSkills: true`.
|
||||
|
||||
Tool selection: omitting `tools` gives Pi's normal builtins; an explicit list is a strict allowlist; an empty field emits `--no-tools`. Allowlisting a name does not load the extension that registers it — load it through normal discovery, `extensions`, `subagentOnlyExtensions`, or a path-like `tools` entry. `mcp:` entries select direct MCP tools (requires `pi-mcp-adapter`; a global `directTools: true` is not sufficient). Children never get the `subagent` tool unless their resolved builtin `tools` explicitly includes it. Missing providers now fail the run before the first model turn instead of continuing silently.
|
||||
Tool selection: omitting `tools` gives Pi's normal builtins; an explicit list is a strict allowlist; an empty field emits `--no-tools`. Allowlisting a name does not load the extension that registers it — load it through normal discovery, `extensions`, `subagentOnlyExtensions`, or a path-like `tools` entry. `mcp:` entries select direct MCP tools (requires `pi-mcp-adapter`; a global `directTools: true` is not sufficient, and an `mcp:` entry named `subagent` does not authorize nested fanout). Children never get the `subagent` tool unless their resolved builtin `tools` explicitly includes it. Missing providers fail the run before the first model turn.
|
||||
|
||||
Per-agent memory (`memory: { scope: "project" | "user", path }`) injects the first 200 lines of `MEMORY.md` from `<project>/.pi/agent-memory/<path>` or `~/.pi/agent/agent-memory/<path>` into the child prompt. Agents with write tools are told they may append dated entries; read-only agents get a read-only block. Paths are validated against traversal and symlink escape, and the directory is created lazily by the agent's own `write`.
|
||||
|
||||
## Chain Files
|
||||
### Refinement Overlays
|
||||
|
||||
`.chain.md` for sequential chains, `.chain.json` when dynamic fanout is needed. Scopes: installed package (`pi-subagents.chains` / `pi.subagents.chains`), user `~/.pi/agent/chains/**`, project `.pi/chains/**`. Project beats user; `.chain.json` beats `.chain.md` within a scope.
|
||||
`/subagents-refine <agent>` (or `{ action: "refine" }`) layers bounded project-local guidance on one agent's system prompt without editing its file. It collects bounded evidence from that agent's recent project runs, then launches a fresh read-only proposal child to draft small guidance edits. Proposals are validated first — edits attempting to override safety, policy, tool, output, acceptance, developer, or system instructions are rejected, as are edits targeting all agents or base agent files. The overlay lands at `.pi/subagents/refinements/<agent>.md` with revision metadata and snapshots, and is injected at launch as a `<pi-subagents-refinement>` block scoped to the project. `refine.show` prints the overlay and history; `refine.rollback` restores the previous revision; deleting the file removes the refinement.
|
||||
|
||||
```md
|
||||
---
|
||||
name: scout-planner
|
||||
description: Gather context then plan implementation
|
||||
---
|
||||
|
||||
## scout
|
||||
phase: Context
|
||||
label: Map auth flow
|
||||
as: context
|
||||
output: context.md
|
||||
|
||||
Analyze the codebase for {task}
|
||||
|
||||
## planner
|
||||
reads: context.md
|
||||
model: anthropic/claude-sonnet-4-5:high
|
||||
|
||||
Create an implementation plan based on {outputs.context}
|
||||
```
|
||||
|
||||
Config lines (`phase`, `label`, `as`, `outputSchema`, `output`, `outputMode`, `reads`, `model`, `skills`, `progress`) go immediately after the `## agent` header, then a blank line, then the task text. For `output`/`reads`/`skills`/`progress`, behavior is three-state: omitted inherits from the agent, a value overrides, `false` disables. Template variables: `{task}`, `{previous}`, `{chain_dir}`, `{outputs.name}`. Duplicate `as` names, invalid identifiers, and unknown output references fail before child execution.
|
||||
|
||||
## Configuration
|
||||
## Settings
|
||||
|
||||
`~/.pi/agent/settings.json` or `.pi/settings.json` (project wins):
|
||||
|
||||
@@ -154,68 +198,174 @@ Config lines (`phase`, `label`, `as`, `outputSchema`, `output`, `outputMode`, `r
|
||||
"defaultExtensions": [],
|
||||
"disableThinking": false,
|
||||
"disableBuiltins": false,
|
||||
"projectRootResolution": "git-root",
|
||||
"agentOverrides": {
|
||||
"reviewer": { "model": "anthropic/claude-sonnet-4", "thinking": "high", "fallbackModels": ["openai/gpt-5-mini"] }
|
||||
},
|
||||
"watchdog": { "enabled": true, "main": { "model": "anthropic/claude-opus-4-8", "thinking": "high" } },
|
||||
"modelScope": { "enforce": true, "allow": ["anthropic/*", "openai/gpt-5-*"] }
|
||||
"modelScope": { "enforce": true, "strict": true, "allow": ["anthropic/*", "openai/gpt-5-*"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Override fields: `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `defaultContext`, `acceptanceRole`, `disabled`, `skills`, `tools`, `systemPrompt`, `extensions`. Use `false` to clear an inherited `defaultContext`/`acceptanceRole`. `defaultModel`/`defaultThinking`/`defaultExtensions` apply to builtin, package, user, and project agents that omit the field; explicit frontmatter and per-run overrides still win. `disableThinking: true` clears bundled builtin thinking defaults for providers that reject `:level` suffixes.
|
||||
Model precedence, strongest first: per-run override → agent frontmatter `model` → `agentOverrides.<name>.model` → `subagents.defaultModel` → the parent session model.
|
||||
|
||||
`modelScope.allow` is glob-matched (only `*` is special, case-insensitive) against the resolved `provider/id`. Explicitly passed models that match nothing error and abort; models from frontmatter, `defaultModel`, or the inherited session model only warn. `enforce: true` requires a non-empty `allow`.
|
||||
Override fields: `description`, `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `defaultContext`, `acceptanceRole`, `disabled`, `skills`, `tools`, `systemPrompt`, `extensions`. Use `false` to clear an inherited `defaultContext`/`acceptanceRole`, and `tools: "inherit"` on a builtin to drop its bundled allowlist for Pi's normal builtins. Matching user and project agents also receive override fields their frontmatter leaves unset. `disableThinking: true` clears bundled builtin thinking defaults for providers that reject `:level` suffixes.
|
||||
|
||||
Extension config at `~/.pi/agent/extensions/subagent/config.json`: `asyncByDefault`, `forceTopLevelAsync`, `parallel: { maxTasks, concurrency }`, `defaultSessionDir`, `singleRunOutputBaseDir`, `maxSubagentDepth`, `turnBudget`, `intercomBridge: { mode: "always" | "fork-only" | "off", instructionFile }`, `worktreeSetupHook` (+ `worktreeSetupHookTimeoutMs`; receives repo/worktree paths on stdin and must print `{ "syntheticPaths": [...] }`). Nesting depth defaults to 2; tighten or relax with `PI_SUBAGENT_MAX_DEPTH`, config `maxSubagentDepth`, or per-agent frontmatter (per-agent can only tighten).
|
||||
`modelScope.allow` is glob-matched (only `*` is special, case-insensitive) against the resolved `provider/id`. Explicitly passed models that match nothing error and abort; models from frontmatter, `defaultModel`, the inherited session model, or fallback chains only warn — unless `strict: true`, which rejects every out-of-scope resolved model and fails the run on an invalid fallback. `enforce: true` requires a non-empty `allow`.
|
||||
|
||||
Profiles live in `~/.pi/agent/profiles/pi-subagents/` and cached provider catalogs in `.../providers/`. Workflow: `/subagents-refresh-provider-models <provider>` → `/subagents-generate-profiles <provider>` → `/subagents-load-profile <provider>.quota`.
|
||||
`projectRootResolution` defaults to `"nearest"` (nearest parent with `.pi` or `.agents`); set `"git-root"` in the repository root `.pi/settings.json` so monorepos and worktrees anchor package discovery, project agents, and `agentOverrides` at the git worktree root.
|
||||
|
||||
## Watchdog
|
||||
Recommended model tiering: a cheap model at low thinking for recon/mechanical edits, a mid-tier at medium for most delegations, a top reasoning model at high only for hard tasks arriving with explicit completion criteria (they loop on vague goals), and an intent-reading model for ambiguous UX/product/planning work. Give intent-tier agents cross-provider `fallbackModels` so subscription limits degrade gracefully; note that forked context over an Anthropic parent forces child thinking off, so intent-tier agents work best with fresh context.
|
||||
|
||||
Opt-in adversarial change reviewer — **not** the `reviewer` agent, and not configured by `defaultModel`/`agentOverrides.reviewer`. It runs at the `agent_end` boundary only when the repo's final state changed during the turn; multiple edits coalesce into one review, unchanged/reverted diffs are skipped, and `.pi-subagents/`/`tmp/` artifacts do not trigger it. When enabled it also checks changed TypeScript/JavaScript files for fresh language-server diagnostics (auto-detected `typescript-language-server` from `node_modules/.bin` or PATH; errors become blockers, warnings concerns) bounded by `watchdog.lsp.enabled`, `timeoutMs`, `maxFiles`, `maxDiagnostics`.
|
||||
Profiles live in `~/.pi/agent/profiles/pi-subagents/` and cached provider catalogs in `.../providers/`. Workflow: `/subagents-refresh-provider-models <provider>` → `/subagents-generate-profiles <provider>` → `/subagents-load-profile <provider>.quota`; `/subagents-check-profile` re-checks assigned models against the registry and a live probe.
|
||||
|
||||
Use a strong complementary model: `/subagents-watchdog recommend-model` (current policy is Opus 4.8 high or GPT 5.5 high — use whichever your main session is not). `session model recommended` changes only this session; `model recommended` saves to settings without enabling. Settings keys: `watchdog.main.model`/`.thinking` (omitting `main.model` uses the session model; setting it without a thinking suffix runs with thinking off), `watchdog.children.model`, `watchdog.children.overrides.<agent>.model`.
|
||||
## Extension Config
|
||||
|
||||
`~/.pi/agent/extensions/subagent/config.json`:
|
||||
|
||||
| Key | Notes |
|
||||
|---|---|
|
||||
| `toolDescriptionMode` | `full` (default), `compact`, or `custom` (reads `subagent-tool-description.md`; safety guidance is always retained) |
|
||||
| `legacyChainControls` | Default `false`; enables the legacy `append-step`/checkpoint schema |
|
||||
| `inlineToolDisplay` | `"rich"` default, or `"summary"` for one stable row per run |
|
||||
| `mainWindowRenderer` | `{ horizontalSpacing: 0–4, compactResultMaxLines }` for the chat call/result renderer only |
|
||||
| `foregroundDetachShortcut` | Optional detach shortcut (e.g. `"ctrl+b"`; conflicts with Pi's editor cursor-left binding) |
|
||||
| `asyncByDefault` | Default `true`; `false` restores foreground-by-default for the internal single-run primitive |
|
||||
| `forceTopLevelAsync` | Forces depth-0 runs into background and `clarify: false` |
|
||||
| `fleetView`, `fleetViewPlacement`, `fleetKeybindings`, `asyncWidget` | FleetView display and inspector keys |
|
||||
| `waitTool` | `{ enabled: false }` (or `false`) makes `subagent_wait` return immediately; `PI_SUBAGENT_WAIT_TOOL_ENABLED` overrides per process |
|
||||
| `timeoutMs` | Global default deadline replacing the 30-minute backstop for foreground and plain single-agent async runs; composite async runs stay unbounded at the top level |
|
||||
| `toolTimeoutMs` | Hard per-tool-call deadline. Without it, known-fast builtins (`read`, `grep`, `find`, `ls`, `edit`, `write`, `structured_output`) get five minutes; `bash`, custom, and MCP tools get attention notices only. `contact_supervisor`, `intercom`, and `subagent_wait` are exempt |
|
||||
| `globalConcurrencyLimit` | Concurrency inside durable legacy multi-child runs |
|
||||
| `maxSubagentSpawnsPerSession` | Cumulative launches per parent session (unlimited by default); `grant-spawn-budget` adds capacity up to the original cap |
|
||||
| `maxSubagentSpawnsPerRun` | Cumulative logical children in one run tree; default `64`. Claims are never refunded |
|
||||
| `maxActiveAsyncRunsPerSession` | Concurrent top-level async runs (unset/`0` = unlimited); slots release only on terminal state plus observed process-terminal proof |
|
||||
| `scheduledRuns` | `{ enabled, maxPending, storeRoot }` for durable schedules |
|
||||
| `parallel` | `{ maxTasks: 8, concurrency: 4 }`; per-call `concurrency` wins |
|
||||
| `defaultSessionDir`, `singleRunOutputBaseDir`, `artifactDir` | Session/output/artifact locations; `artifactDir` is `"session"` (default), `"project"`, or `"temp"` |
|
||||
| `maxSubagentDepth` | Nesting limit when no `PI_SUBAGENT_MAX_DEPTH` applies; per-agent frontmatter can only tighten |
|
||||
| `intercomBridge` | `{ mode: "always" \| "fork-only" \| "off", instructionFile, resultDelivery }` |
|
||||
| `worktreeBaseDir`, `worktreeSetupHook`, `worktreeSetupHookTimeoutMs` | Worktree base dir and setup hook |
|
||||
| `missions` | `{ enabled, directory, globalIndex, globalIndexDir, retainTerminal: 200 }` |
|
||||
| `authorityPolicy` | Fixed action map of `auto`/`confirm`/`forbid` for `discardWorktree`, `destructiveCleanup`, `spawnBudgetGrant`, `scheduleCreate`, `stopRun`, `steerRun` |
|
||||
| `completionBatch` | Smart batching of async-completion notices; failures and pauses bypass it |
|
||||
| `permissions` | Native child tool permission rules (see below) |
|
||||
|
||||
Environment: `PI_SUBAGENT_MAX_DEPTH` (nesting; default 2), `PI_SUBAGENT_MAX_SPAWNS_PER_SESSION`, `PI_SUBAGENT_MAX_SPAWNS_PER_RUN`, `PI_SUBAGENT_TOOL_TIMEOUT_MS`, `PI_SUBAGENT_WAIT_TOOL_ENABLED`, `PI_SUBAGENT_PI_BINARY` (override the child Pi launch command), `PI_SUBAGENT_TASK_DELIVERY` (`auto` default writes tasks over 8000 chars to a temp `task.md`; `file` always does, for hosts whose EDR kills children with long argv), `PI_SUBAGENTS_WORKTREE_DIR`. `PI_SUBAGENT_DEPTH` is internal — do not set it.
|
||||
|
||||
The worktree setup hook runs once per created worktree with an absolute, `~/`, or repo-relative path (bare command names rejected). stdin is JSON with `repoRoot`, `worktreePath`, `agentCwd`, `branch`, `index`, `runId`, `baseCommit`; stdout must be one JSON object such as `{ "syntheticPaths": [".venv", ".env.local"] }`, whose worktree-relative paths are removed before diff capture. Tracked files can never be marked synthetic. Default timeout 30000 ms.
|
||||
|
||||
## Worktrees and Acceptance Gates
|
||||
|
||||
`worktree: true` on parallel tasks, chain steps, or group options runs each agent in an isolated git worktree. Requires a git repo with a clean tree; `node_modules/` is symlinked in; task-level `cwd` overrides must match the shared cwd.
|
||||
Set `worktree: true` on `runs.run`/`runs.all` items (or at the top level to make it the default, overridable per child with `worktree: false`) to give each writing child its own managed git worktree. Each branches from clean HEAD, journals ownership before launch, captures a patch and handoff manifest, then removes cleanly captured temporary worktrees and branches; the manifest path stays in the child's `artifactPaths`. Keep one writer when parallel writes are not intentionally isolated. `action: "worktree.discard"` requires the aggregate `handoffPath`.
|
||||
|
||||
```javascript
|
||||
{ agent: "worker", task: "Implement the fix", acceptance: {
|
||||
level: "verified",
|
||||
criteria: ["Patch the bug without widening scope"],
|
||||
evidence: ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"],
|
||||
verify: [{ id: "focused", command: "npm test", timeoutMs: 120000 }],
|
||||
maxFinalizationTurns: 3
|
||||
verify: [{ id: "focused", command: "npm test", timeoutMs: 120000 }]
|
||||
} }
|
||||
```
|
||||
|
||||
Provenance levels reported: `attested`, `checked`, `verified`, `reviewed`, `rejected`. Inline `acceptance=` accepts scalar levels `auto`, `attested`, `checked`; object contracts need the tool API or `.chain.json`.
|
||||
Levels are `auto` (default), `none`, `attested`, `checked`, and `verified`; review is a separate gate under `acceptance.review`. Inference: async, risky, and dynamic writer contexts get checked evidence plus `review: { agent: "reviewer", required: true }`; read-only tasks get lightweight attestation; normal writer tasks get checked evidence without review. `acceptanceRole: "read-only" | "writer"` in frontmatter or overrides guides inference for ambiguous tasks without changing tool access.
|
||||
|
||||
`gate: "npm test"` is shorthand for one host-run verification command (`acceptance.level: "verified"` with that single command). Results are memoized per tracked workspace state and effective environment, so an unchanged tree does not rerun it; with `worktree: true` it runs inside the child's worktree. `gate` cannot combine with `acceptance` and is rejected on retained `resume` items.
|
||||
|
||||
Evidence statuses: `claimed`, `attested`, `checked`, `verified` (runtime verification commands passed — child-reported success does not count), `review-required`, `reviewed`, `rejected`. Bare `"none"` is rejected (use `{ level: "none", reason }`); `"reviewed"` is not a settable policy level. For `attested` or stricter, the child prompt asks for a fenced `acceptance-report` JSON block; fences are stripped from output artifacts while per-child metadata keeps the full acceptance ledger. Explicit failed gates fail the run; inferred gates stay observable without failing it.
|
||||
|
||||
## Missions and Schedules
|
||||
|
||||
Ordinary workflow launches create one enclosing mission by default, stored under `~/.pi/agent/missions/projects/<project-hash>/` and linking objectives, run ids, lifecycle status, decisions, artifact paths, and delivery receipts. Children do not create separate missions. `details.missionId` is authoritative and human receipts end with `Mission: <id> (<status>)`. Pass `mission: false` for an ephemeral workflow with no mission and no `state` global, or `missions.enabled: false` to disable automatic creation (explicit fields and actions still work). Automatic persistence failures are reported as `details.missionWarning` without blocking the run; explicit `missionId`/`mission` requests are strict before launch.
|
||||
|
||||
An explicit `mission` object needs exactly one non-empty `title` or `summary` (`objective` and `labels` optional). `goal: true` requires `budget: { tokens }` and turns the mission into a continuation driver: after each parent turn an idle goal mission emits one needs-attention notice with its title, remaining budget, and next ready action (from `state.nextReadyAction`, `state.nextAction`, a ready state item, an open decision, or linked-run state). Reaching the budget sets `budget-exhausted` and stops notices. The extension never launches or replans goal work itself.
|
||||
|
||||
`state.get(key)` / `state.set(key, value)` give a workflow durable JSON state through its mission, shared across later workflows attached with the same `missionId`. Each `set` takes the state-file lock and merges with the latest on-disk state; missing keys return `undefined`, and the whole state file is capped at 256 KiB.
|
||||
|
||||
Durable schedules are enabled by default under `.pi/subagents/schedules/<id>/` (or `scheduledRuns.storeRoot`):
|
||||
|
||||
```javascript
|
||||
{ action: "schedule.create", id: "evening-review", name: "Evening review", at: "+30m",
|
||||
workflowScript: `return runs.run("main", { agent: "reviewer", task: "Review the current diff." })` }
|
||||
{ action: "schedule.create", id: "backlog", every: "6h", catchUp: "latest", workflowScript: "..." }
|
||||
```
|
||||
|
||||
Fixed intervals support `m`/`h`/`d`/`w` and advance from the planned time without completion drift. Scheduled runs always launch async with fresh context and disable automatic mission creation. `overlap` is fixed to `skip`; `catchUp` supports `latest` (default) and `none`; `schedule.run-due` lets an external launcher start due work without making pi-subagents a daemon. Calendar/cron recurrence, queue/replace overlap, and a schedule TUI inspector are deferred.
|
||||
|
||||
For substantial work in another codebase, prefer a Herdr project pane (`project.open`) over ordinary child nesting; use an explicit `cwd` only for small bounded cross-project work.
|
||||
|
||||
## Watchdog and Child Permissions
|
||||
|
||||
The watchdog is an opt-in adversarial reviewer for repo edits — **not** the `reviewer` agent, and not configured by `defaultModel`/`agentOverrides.reviewer`. It runs at the `agent_end` boundary only when the repo's final state changed during the turn; multiple edits coalesce into one review, unchanged/reverted diffs are skipped, and `.pi/subagents/`/`tmp/` artifacts do not trigger it. In orchestrated runs each writing child can review its own worktree while the parent reviews the aggregate diff.
|
||||
|
||||
Use a strong complementary model: `/subagents-watchdog recommend-model` (current policy is Opus 4.8 high or GPT 5.5 high — use whichever your main session is not). `session model recommended` changes only this session; `model recommended` saves to settings without enabling. Settings keys: `watchdog.main.model`/`.thinking` (omitting `main.model` uses the session model; setting it without a thinking suffix runs with thinking off), `watchdog.children.model`, `watchdog.children.overrides.<agent>.model`.
|
||||
|
||||
Scope monitoring keeps a bounded in-memory current-scope artifact from real user prompts and prepends it to review input (`watchdog.scope.enabled`), so the reviewer can flag `scope-drift`; newer prompts supersede older ones and watchdog auto-follow prompts are not recorded as scope. `watchdog.cadence.everyNTools` adds Scopey-style non-blocking reviews every N tool results, delivered transcript-visibly via `steer` after the current tool boundary — pick a cheap model for frequent monitoring. `watchdog.autoFollow` (`blockers`, `maxAttempts`, `stalemateRepeats`) can queue a visible follow-up asking the agent to address a blocker, stopping on repeated identical blockers.
|
||||
|
||||
LSP diagnostics: when enabled, the watchdog checks changed TypeScript/JavaScript files for fresh language-server diagnostics before the model review, auto-detecting `typescript-language-server` from `node_modules/.bin` or `PATH` (never installing anything or scanning the workspace). Errors become blockers, warnings concerns; bound with `watchdog.lsp.enabled`, `timeoutMs`, `maxFiles`, `maxDiagnostics`.
|
||||
|
||||
Native child permissions are opt-in and apply only to Pi child runtimes. Configure non-bash rules under `permissions.rules` in the extension config (`"read": "allow"`, `"write": "ask"`, `"edit": "deny"`), overridable by an agent's `permission:`/`permissions:` frontmatter block. Omitted and unknown tools default to `allow`, explicit `allow` removes an inherited restriction, and the gate is not registered when no `ask`/`deny` rule resolves. An `ask` pauses that exact call and sends a bounded, redacted preview to a one-call arbiter owned by the child watchdog, which returns only approve/deny — enable and configure `subagents.watchdog.children` first, since a disabled watchdog, missing model/auth, timeout, or malformed response denies the call. Decisions are written to bounded audit JSONL with `decisionSource: "watchdog"`. `bash` is always passed through and bash rules are rejected rather than parsed — use `pi-guard` for command-level policy. External CLI profiles are opaque processes, so a launch with effective `ask`/`deny` rules is rejected rather than claiming enforcement.
|
||||
|
||||
## Supervisor Coordination
|
||||
|
||||
Native, no `pi-intercom` required: children call `contact_supervisor({ reason, message })` with `reason` ∈ `need_decision`, `interview_request`, `progress_update`; the parent replies with `subagent_supervisor({ action: "reply", replyTo, message })` or checks `{ action: "pending" }`. Requests are scoped to the exact Pi session id that spawned the child. If no external `pi-intercom` owns the name, the native channel also exposes `intercom` as a compatibility fallback. `pi install npm:pi-intercom` remains available as a companion.
|
||||
Native, no `pi-intercom` required: children call `contact_supervisor({ reason, message })` with `reason` ∈ `need_decision`, `interview_request`, `progress_update`; the parent replies with `subagent_supervisor({ action: "reply", replyTo, message })` or checks `{ action: "pending" }`. Requests are scoped to the exact Pi session id that spawned the child, so a second Pi session in the same repository does not receive them. If no external `pi-intercom` owns the name, the native channel also exposes `intercom` as a compatibility fallback. A foreground child may detach while awaiting a reply: reply first, then `subagent_wait({ id: runId })`. Children should not ask for clarification when the only conflict is review-only/no-edit versus progress- or artifact-writing instructions — no-edit wins.
|
||||
|
||||
A foreground child may detach while awaiting a reply: reply first, then `subagent_wait({ id: runId })`.
|
||||
Child-safety boundaries are enforced at runtime: spawned children never receive the bundled `pi-subagents` skill; forked child context is filtered to strip parent-only orchestration instructions, slash/status/control messages, and prior parent `subagent` tool history; and children get boundary instructions that they are not the orchestrator. The exception is an agent whose resolved builtin `tools` includes `subagent`, which gets a child-safe tool bounded by `maxSubagentDepth`.
|
||||
|
||||
## Observability
|
||||
|
||||
FleetView below the editor shows `main` plus active children with task, elapsed time, and token totals (↑/↓ or j/k to select, Enter to inspect, only when the editor is empty). `/subagents-fleet` opens the inspection-only fleet inspector (Shift+K/J scroll a line, PgUp/PgDn a page, `x`/Ctrl+O toggle tool details, `r` refresh, Esc close; Ctrl+Alt+F opens it mid-turn). Mutations stay explicit via `/subagents-stop`.
|
||||
FleetView below the editor (or above, via `fleetViewPlacement`) keeps active work visible as a compact summary; with the editor empty, `↓`/`←` expands it into `main` plus active children with agent, state, elapsed time, and token totals, `↑↓`/`jk` selects, and `Enter` inspects. `/subagents-fleet` opens the live inspector: `Shift+K`/`Shift+J` scroll a line, `PgUp`/`PgDn` a page, `x`/`Ctrl+O` toggle tool details, `r` refresh, `Esc` close, `s` compose an acknowledged message to a live async child (Tab cycles `steer`/`follow_up`/`auto`), `D` stop after confirmation, `H` open a Herdr inspector pane (Herdr 0.7.5+). `Ctrl+Alt+F` opens it mid-turn. Successful background completions stay quiet so inactive tabs are not marked unread; failures and pauses notify immediately.
|
||||
|
||||
Async runs write lifecycle artifacts: `details.asyncDir` holds `status.json`, `events.jsonl`, `output-<index>.log`, `subagent-log-<runId>.md`, with the final summary as `<runId>.json` in Pi's results directory. Stable v1 status fields include `lifecycleArtifactVersion`, `runId`/`id`, `sessionId`, `mode`, `state`, timestamps, `cwd`, `asyncDir`, `sessionFile`, `outputFile`, `workflowGraph`, `steps`, `results`, `totalTokens`, `totalCost`, `model`/`attemptedModels`/`modelAttempts`, `toolCount`, `turnCount`, and nested `children`. Read these files rather than scraping terminal output, and ignore unknown fields for forward compatibility. Lifecycle artifact v3 adds `process-terminal-candidate.json` and `process-terminal.json`; a proof is `observed` only when the live parent saw the runner's `close` event, otherwise `unknown` — never infer exit from `endedAt`, result-file existence, PID disappearance, or lease absence.
|
||||
Async runs write lifecycle artifacts under `<tmpdir>/pi-subagents-<scope>/async-subagent-runs/<id>/`: `status.json`, `events.jsonl`, `output-<n>.log`, `subagent-log-<runId>.md`, with the final summary as `<runId>.json` in Pi's results directory (`details.asyncDir` points at the run directory). Stable v1 status fields: `lifecycleArtifactVersion`, `runId`/`id`, `sessionId`, `mode`, `state`, timestamps, `durationMs`, `cwd`, `asyncDir`, `sessionFile`, `outputFile`, `workflowGraph`, `steps`, `results`, `totalTokens`, `totalCost`, `model`/`attemptedModels`/`modelAttempts`, `toolCount`, `turnCount`, optional `launchResolvedExtensions` and `runtimeAcknowledgedExtensions`, and nested `children`. Read these files rather than scraping terminal output, and ignore unknown fields.
|
||||
|
||||
The result file is consumed and deleted once its completion notice is delivered; before deletion the watcher writes a versioned replay record under `<resultsDir>/completion-replay/<runId>.json` and a bounded output archive under `<resultsDir>/output-archives/<runId>.json` (64 KiB of result tail when no child output/session file exists). `subagent_wait` surfaces a slim projection in `details.completions`. Lifecycle artifact v3 adds `process-terminal-candidate.json` and `process-terminal.json`; a proof is `observed` only when the live parent saw the runner's `close` event, every recorded child writer has a close record, and any tracked session lease is free — otherwise `unknown`. Never infer exit from `endedAt`, result-file existence, PID disappearance, or lease absence.
|
||||
|
||||
Child-protocol bounds: a child JSONL line above 16 MiB fails with `protocolError` code `protocol_output_limit` (oversized `turn_end`/`agent_end` aggregates are replaced with bounded lifecycle records while preserving `agent_end.willRetry`); stderr retains its latest 128 KiB; `agent_settled` is the terminal watermark on current Pi builds.
|
||||
|
||||
Debug artifacts live under `{sessionDir}/subagent-artifacts/`, `.pi/subagents/artifacts/` for project-scoped runs, or a temp dir: `{runId}_{agent}_input.md`, `_output.md`, `.jsonl`, `_meta.json` (timing, usage, exit code, final/attempted models, fallback outcomes, resolved acceptance ledger). For npm package projects, project-scoped artifacts need a `.npmignore`/`files` rule — pi-subagents warns when package settings could publish `.pi/subagents/`.
|
||||
|
||||
## Extension Integration
|
||||
|
||||
Versioned in-process event-bus RPC: listen for `subagents:rpc:v1:ready`, emit on `subagents:rpc:v1:request` (`{ version: 1, requestId, method, params }`), read `subagents:rpc:v1:reply:<requestId>`. Methods: `ping`, `status`, `spawn` (async-only), `steer`, `interrupt`, `stop`. `ping.capabilities` advertises `processTerminalProof`, `nonRecoveringSteer`, and `events.asyncComplete`. `pi.events` is in-process only — use file artifacts or `pi-intercom` across processes.
|
||||
Versioned in-process event-bus RPC: listen for `subagents:rpc:v1:ready`, emit on `subagents:rpc:v1:request` (`{ version: 1, requestId, method, params }`), read `subagents:rpc:v1:reply:<requestId>`. Methods: `ping`, `status`, `spawn` (requires `workflowScript`, async-only), `steer`, `interrupt`, `stop`, `resume`. `ping.capabilities` advertises `events.asyncComplete`, `launchResolvedExtensions`, `runtimeAcknowledgedExtensions`, `processTerminalProof`, `nonRecoveringSteer`, `resume`, and `fleetStatus: { version: 1 }` (successful `status` replies then include a bounded `data.fleet` DTO that never exposes run, async, or tool IDs). RPC steering disables pause-and-revive recovery so the caller keeps authority over the child it spawned.
|
||||
|
||||
Also exported: `pi-subagents/preflight` (`resolveSubagentLaunchContract`), `pi-subagents/delegation` (`SUBAGENT_DELEGATION_REQUEST_EVENT`, `SUBAGENT_DELEGATION_RESPONSE_EVENT`), and `pi-subagents/background-work` (`registerBackgroundWorkProvider`). Events on the bus: `subagent:async-started`, `subagent:async-complete`, `subagent:control-intercom`, `subagent:result-intercom`, `subagent:process-terminal`.
|
||||
Also exported:
|
||||
|
||||
Optional `@gotgenes/pi-permission-system` adds a runtime `allow`/`ask`/`deny` policy layer via a `permission:` frontmatter block, composing independently with the visibility-based `tools:` allowlist. pi-subagents passes `PI_SUBAGENT_PARENT_SESSION` so headless children can forward `ask` prompts to the parent UI — place `ask` policies on direct children of the interactive session.
|
||||
- `pi-subagents/preflight` — `resolveSubagentLaunchContract(...)` resolves an ordinary single-agent launch contract side-effect-free (agent identity and shadowed candidates, parsed-definition digest, context/model/tools/skills/MCP/extensions, artifact and async paths, capability-ceiling audit data, `launchContractDigest`). Failure codes: `missing_agent`, `ambiguous_agent`, `missing_skill`, `denied_required_tool`, `invalid_artifact_dir`, `invalid_cwd`, `unsupported_mode`; host-only facts appear as `host_required` diagnostics.
|
||||
- `pi-subagents/delegation` — `SUBAGENT_DELEGATION_REQUEST_EVENT` / `SUBAGENT_DELEGATION_RESPONSE_EVENT` run one configured foreground leaf agent. `ownerRunId` + `nodeId` is the logical identity (`requestId` is one attempt; a second active attempt gets `duplicate_node`), result mode is explicit (`text` stays literal, `structured` returns schema-validated JSON), schemas cap at 64 KiB and values at 1 MiB. Foreground-only; requires an active extension context.
|
||||
- `pi-subagents/capability-ceiling` — `registerSubagentCapabilityCeiling({ sessionId, source, ceiling })` enforces a session-scoped ceiling (`allowedAgents`, `allowedTools`, `denyExtensions`). Active registrations intersect allowlists and OR `denyExtensions`; non-allowlisted agents fail before spawn and stay visible in `list` as non-executable; the snapshot propagates monotonically to nested/async children.
|
||||
- `pi-subagents/background-work` — `registerBackgroundWorkProvider({ name, wakeChannels, listActiveWork, reconcile })` makes another extension's jobs visible to `subagent_wait`, keyed by stable provider-local id plus owning session id.
|
||||
- `pi-subagents/project-panes` — `PROJECT_PANES_API_VERSION` (currently `1`), `openProjectPane`, `getProjectPaneStatus`, `closeProjectPane`.
|
||||
|
||||
## Skills and Bundled Skill
|
||||
Bus events: `subagent:async-started` (payload includes truncated `task` and workflow-level `goal`), `subagent:async-complete`, `subagent:control-intercom`, `subagent:result-intercom`, `subagent:process-terminal`, plus child-emitted `subagent:acknowledge-extension`. `pi.events` is in-process only — use file artifacts or `pi-intercom` across processes.
|
||||
|
||||
Skills are `SKILL.md` files selected per agent; discovery is project-first (project config `skills/`, project/task packages, project settings, `~/.pi/agent/skills/`, user packages, user settings). Set them via agent defaults, per-run `skill: "tmux, safe-bash"`, or `skill: false`. For chains, top-level `skill` is additive and a step-level value overrides. Missing skills warn instead of failing. When an agent has an explicit `tools` allowlist plus resolved skills, `read` is added so skill files can be loaded.
|
||||
Herdr integration: when `HERDR_ENV=1` and `HERDR_PANE_ID` are set, pi-subagents reports active async-run counts through pane metadata, emits `herdr:blocked`/`herdr:busy`, and restores state after `/reload` or `/resume`. Herdr 0.7.5+ adds on-demand inspector panes (`inspector.open/status/close`, a raw dashboard reading lifecycle artifacts — closing it never stops the run) and project panes (`project.open/status/close`, a Pi session rooted in another repo that owns its own subagents; bindings at `<projectRoot>/.pi/subagents/project-panes/herdr.json`).
|
||||
|
||||
The package bundles a `pi-subagents` skill for the **orchestrating parent only** — children never receive it, and forked child context is filtered to strip parent-only orchestration instructions, slash/status/control messages, and prior parent `subagent` tool history.
|
||||
## Skills and the Bundled Skill
|
||||
|
||||
Recommended implementation pattern: clarify → planner → worker → fresh reviewers → worker.
|
||||
Skills are `SKILL.md` files selected per agent; discovery is project-first (project config `skills/`, project/task packages, project settings, `~/.pi/agent/skills/`, user packages, user settings). Set them via agent defaults, per-run `skill: "tmux, safe-bash"`, or `skill: false`; top-level `skill` in a chain is additive and a step-level value overrides. Missing skills warn instead of failing. When an agent has an explicit `tools` allowlist plus resolved skills, `read` is added so skill files can be loaded. Agent-local `skillPath` candidates never enter Pi's global catalog — pair `inheritSkills: false` with explicit `skills` and `skillPath` for a child that should receive only its private skills.
|
||||
|
||||
The package bundles a `pi-subagents` skill for the **orchestrating parent only**, covering delegation patterns, prompt-workflow recipes, role-agent prompting, safety boundaries, intercom conventions, and control/diagnostics.
|
||||
|
||||
## External CLI Runners
|
||||
|
||||
An agent profile can run a local one-shot command instead of a Pi child:
|
||||
|
||||
```yaml
|
||||
runner:
|
||||
type: external-cli
|
||||
command: node
|
||||
args: ["./scripts/local-reviewer.mjs"]
|
||||
promptDelivery: stdin
|
||||
async: true
|
||||
```
|
||||
|
||||
They are async-only, receive one combined system/task prompt over stdin, and use argv arrays without a shell. Supported: status artifacts, stdout/stderr logs, timeout, stop (full output goes to log files; in-memory final stdout/stderr keep the last 64 KiB). Not supported: foreground/clarify, steer/resume/interrupt-as-pause, Pi models/tools/extensions, skills, structured output, nested subagents, fallback models, and native permission enforcement.
|
||||
|
||||
## Recursion Guard
|
||||
|
||||
Subagents can call `subagent` only when their resolved builtin tools explicitly include it — intended for delegated fanout agents, not ordinary workers or reviewers. Nesting defaults to two levels (main → subagent → sub-subagent); deeper calls are blocked with guidance to finish directly. Nested runs appear in the parent status tree, and `status`, `interrupt`, and `resume` can target one by its nested id. Configure with `PI_SUBAGENT_MAX_DEPTH`, `config.maxSubagentDepth`, or agent frontmatter (which can only tighten).
|
||||
|
||||
## Session Sharing
|
||||
|
||||
`share: true` exports the full session to HTML, uploads it to a secret GitHub Gist through your `gh` credentials, and returns a `https://shittycodingagent.ai/session/?<gistId>` URL. Disabled by default — session data may contain source code, paths, environment variables, or credentials.
|
||||
|
||||
@@ -2,34 +2,38 @@
|
||||
|
||||
Source: https://pi.dev/packages/pi-web-access
|
||||
|
||||
Web search, content extraction, GitHub repo cloning, PDF extraction, YouTube and local-video understanding for Pi.
|
||||
Web search, content extraction, GitHub repo cloning, PDF conversion, YouTube and local-video understanding for Pi.
|
||||
|
||||
```bash
|
||||
pi install npm:pi-web-access
|
||||
```
|
||||
|
||||
Requires Pi v0.37.3+. Works with no API keys — Exa MCP provides zero-config search, and OpenAI search can reuse Codex auth from `/login`. Optional binaries for frame extraction: `brew install ffmpeg` (frames, thumbnails, local video duration) and `brew install yt-dlp` (YouTube stream URLs). Without them, transcripts and Gemini-based analysis still work.
|
||||
Works with no API keys — Exa MCP provides zero-config search, OpenAI search can reuse Codex auth from `/login`, and DuckDuckGo HTML search is keyless (explicit-only). Optional binaries for frame extraction: `brew install ffmpeg` (frames, thumbnails, local video duration) and `brew install yt-dlp` (YouTube stream URLs). Without them, transcripts and Gemini-based analysis still work.
|
||||
|
||||
## Tools
|
||||
|
||||
### web_search
|
||||
|
||||
Searches via OpenAI, Brave, Parallel, Tavily, SERPdive, AnySearch, self-hosted SearXNG, Exa, Perplexity, or Gemini and returns a synthesized answer with citations.
|
||||
Searches via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, xAI, Bright Data SERP, SerpBase, self-hosted SearXNG, keyless DuckDuckGo, Exa, Perplexity, or Gemini, and returns a synthesized answer with citations.
|
||||
|
||||
```javascript
|
||||
web_search({ query: "TypeScript best practices 2025" })
|
||||
web_search({ queries: ["query 1", "query 2"], workflow: "auto-summary" })
|
||||
web_search({ query: "latest news", numResults: 10, recencyFilter: "week" })
|
||||
web_search({ query: "...", domainFilter: ["github.com", "-old.example.com"], provider: "openai" })
|
||||
web_search({ query: "...", provider: "all" })
|
||||
web_search({ query: "...", provider: ["brave", "exa"] })
|
||||
```
|
||||
|
||||
Parameters: `query`/`queries`, `numResults` (default 5, max 20), `recencyFilter` (`day`/`week`/`month`/`year`), `domainFilter` (prefix `-` to exclude), `provider` (`auto` or an explicit provider), `includeContent`, `workflow` (`none`, `summary-review` default, `auto-summary`).
|
||||
Parameters: `query`/`queries`, `numResults` (default 5, max 20), `recencyFilter` (`day`/`week`/`month`/`year`), `domainFilter` (prefix `-` to exclude), `provider`, `includeContent`, `workflow` (`none`, `summary-review` default, `auto-summary`).
|
||||
|
||||
In `auto` mode the fallback order is configured SearXNG → OpenAI (when suitable and available) → Exa (direct API if keyed, MCP if not) → Brave → Parallel → Tavily → SERPdive → Perplexity → Gemini API → Gemini Web (only with browser cookies enabled). AnySearch is explicit-only and never auto-selected.
|
||||
In `auto` mode the fallback order is configured SearXNG → OpenAI (when suitable and available) → Exa (direct API if keyed, MCP if not) → Brave → Parallel → TinyFish → Search1API → Searchinfinity → Querit → Tavily → Jina → SERPdive → Perplexity → Gemini API → Gemini Web (only with browser cookies enabled). DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase are explicit-only and never auto-selected.
|
||||
|
||||
`provider: "all"` runs the same query against every eligible provider simultaneously, excluding the explicit-only ones (Bright Data and SerpBase are paid Google SERP providers, so `all` never spends on them). Exa participates through its zero-config MCP path and OpenAI can use Pi auth; browser-cookie access alone does not opt Gemini in. Successful answers are preserved separately while source URLs and inline content are deduplicated, and one provider failure does not discard the rest; if every provider fails, per-provider diagnostics are returned. In the curator, **All** is selectable like any provider — each participant gets its own result card with a provider badge and checkbox, failures get a disabled error card, and the summary is generated from the selected cards. `provider` also accepts a non-empty array of named providers, which run concurrently through the same aggregation path (`"auto"` and `"all"` are invalid inside arrays, and `"all"` is invalid inside `searchRouting.providers`).
|
||||
|
||||
### fetch_content
|
||||
|
||||
Extracts readable markdown from URLs or local files, auto-detecting GitHub repos, YouTube videos, PDFs, local video files, and regular pages.
|
||||
Fetches URLs or local files as readable markdown, exact textual HTTP bodies, direct images, or page-grounded answers, auto-detecting GitHub repos, YouTube videos, PDFs, local video files, images, and regular pages.
|
||||
|
||||
```javascript
|
||||
fetch_content({ url: "https://example.com/article" })
|
||||
@@ -38,19 +42,27 @@ fetch_content({ url: "https://github.com/owner/repo" })
|
||||
fetch_content({ url: "https://youtube.com/watch?v=abc", prompt: "What libraries are shown?" })
|
||||
fetch_content({ url: "/path/to/recording.mp4", prompt: "What error appears on screen?" })
|
||||
fetch_content({ url: "...", timestamp: "23:41-25:00", frames: 4 })
|
||||
fetch_content({ url: "https://example.com/api", mode: "raw" })
|
||||
fetch_content({ url: "https://example.com/guide", mode: "answer", prompt: "What are the installation steps?" })
|
||||
```
|
||||
|
||||
Parameters: `url`/`urls`, `prompt` (question about a video), `timestamp` (single `"23:41"`, range `"23:41-25:00"`, or bare seconds; accepts `H:MM:SS`, `MM:SS`, seconds), `frames` (max 12), `forceClone` (clone GitHub repos over the 350 MB threshold).
|
||||
Parameters: `url`/`urls`, `prompt` (video question, or the page-local question required by `mode: "answer"`), `mode` (`readable` default, `raw`, `answer`), `answerModel` (optional `provider/model-id` for answer mode; defaults to the current enabled Pi model), `timestamp` (single `"23:41"`, range `"23:41-25:00"`, or bare seconds), `frames` (max 12), `forceClone` (clone GitHub repos over the 350 MB threshold).
|
||||
|
||||
Raw and direct-image requests use the same SSRF validation, hostname domain policy, redirect checks, timeout, and 5 MB streamed response bound as normal extraction. Raw mode returns textual bodies even for non-2xx responses (HTTP status is in tool details) and runs no readability or hosted-extraction fallbacks.
|
||||
|
||||
### get_search_content
|
||||
|
||||
Retrieves stored content from previous searches or fetches. Content is stored in full but returned in bounded slices by default; page through with `offset`/`limit` (30,000-character bounds).
|
||||
Retrieves stored content from previous searches or fetches.
|
||||
|
||||
```javascript
|
||||
get_search_content({ responseId: "abc123", urlIndex: 0 })
|
||||
get_search_content({ responseId: "abc123", url: "https://...", offset: 30000 })
|
||||
get_search_content({ responseId: "abc123", urlIndex: 0, findText: "installation" })
|
||||
get_search_content({ responseId: "abc123", urlIndex: 0, findText: ["timeout", "retry"], findMode: "fuzzy" })
|
||||
```
|
||||
|
||||
Fetched content is stored in full in a private `web-search-cache` directory under the Pi config directory — not in the session JSONL — including the original page behind `fetch_content` answer mode. The cache has a one-hour lifetime with fixed limits of 128 entries and 128 MiB, evicting oldest first; on macOS/Linux the directory is `0700` and files are `0600`. `findText` locates bounded matching passages without paging (`findMode` is `exact`, `case-insensitive` default, or `fuzzy`; output capped at 20,000 characters with match counts and nearby context) and cannot be combined with `offset`/`limit`. The default and maximum `limit` come from `maxInlineContentChars`.
|
||||
|
||||
### source_check
|
||||
|
||||
Checks a claim and returns a machine-readable artifact with exact passage citations.
|
||||
@@ -60,15 +72,39 @@ source_check({ claim: "The API supports streaming responses",
|
||||
queries: ["API streaming documentation"], fetchContent: true, domainFilter: ["docs.example.com"] })
|
||||
```
|
||||
|
||||
Results are deduplicated and capped at 20 sources; `fetchContent` fetches at most 5 pages. The artifact carries claim status (`supported`, `contradicted`, `unclear`, `missing-evidence`), source-quality hints, SHA-256 content hashes, and passage IDs with exact source offsets. Search and fetch errors stay in the artifact instead of being discarded. Artifacts are stored with the session and retrieved through `get_search_content` by `responseId`.
|
||||
Results are deduplicated and capped at 20 sources; `fetchContent` fetches at most 5 pages, and stored/retrieved content stays within the configured `maxInlineContentChars` `offset`/`limit` bounds. The artifact carries claim status (`supported`, `contradicted`, `unclear`, `missing-evidence`), source-quality hints, SHA-256 content hashes, and passage IDs with exact source offsets. Search and fetch errors stay in the artifact instead of being discarded. Artifacts are stored with the session and retrieved through `get_search_content` by `responseId`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- **GitHub repos** are cloned locally instead of scraped: root URLs return the tree plus README, `/tree/` paths return directory listings, `/blob/` paths return file contents, and the agent gets a local path to explore with `read`/`bash`. Repos over 350 MB use a lightweight API view (override with `forceClone`). Commit-SHA URLs go through the API. Clones are cached per session and wiped on session change; private repos need the `gh` CLI. Setting `githubClone.enabled: false` only skips the clone/API specialization — `fetch_content` still handles the URL through normal extraction.
|
||||
- **YouTube** via Gemini: visual descriptions, timestamped transcripts, chapter markers, and the thumbnail. Fallback: Gemini Web (cookies enabled) → Gemini API → Perplexity (text only). Handles `/watch?v=`, `youtu.be/`, `/shorts/`, `/live/`, `/embed/`, `/v/`.
|
||||
- **Local video** (`/`, `./`, `../`, or `file://`): MP4, MOV, WebM, AVI and other common formats up to 50 MB for Gemini analysis; a thumbnail frame is included when ffmpeg is present. Timestamp/frame extraction uses ffmpeg directly and works on larger files.
|
||||
- **PDFs** are text-extracted and saved to `~/Downloads/` as markdown so the agent can `read` sections. No OCR.
|
||||
- **Blocked pages**: Readability → Next.js RSC flight-data parser → configured Firecrawl (cache-only by default) → Jina Reader → Parallel → Gemini URL Context → Gemini Web extraction when cookies are enabled.
|
||||
- **PDFs** are converted to Markdown and saved under the temporary `pi-web-pdf` directory so the agent can `read` sections — see below.
|
||||
- **Blocked pages**: Readability (plus declared `Link`/`rel` discovery) → Next.js RSC flight-data parser → configured Firecrawl → third-party hosted fallbacks, which stay disabled for remote HTTP(S) targets unless `fetchRouting.allowRemoteHostedProviders` is enabled.
|
||||
|
||||
### PDF conversion
|
||||
|
||||
Three engines, selected with `pdf.provider` (`"auto"` default):
|
||||
|
||||
| Provider | Engine | Trade-offs |
|
||||
|---|---|---|
|
||||
| `datalab` | Datalab hosted conversion (Marker) | Deterministic layout-aware output — tables, multi-column reading order, headings, math; `accurate` mode handles scanned pages; may return `parse_quality_score` (0–5); requires a Datalab key, billed per page with a free monthly credit |
|
||||
| `gemini` | Gemini API (vision LLM) | Best on scanned/complex pages; LLM transcription can drift or truncate; requires a Gemini key |
|
||||
| `unpdf` | Local pdf.js text extraction | Free, offline, no key; flattened text only — no layout, tables, or OCR |
|
||||
|
||||
`auto` order: Datalab (when keyed) → Gemini (when keyed) → local `unpdf`, continuing down the chain on failure including exhausted free credit. Pinning a provider skips the other remote tiers but still falls back to `unpdf` on error (except credential/config errors and caller cancellation). No Datalab key simply skips that tier.
|
||||
|
||||
Datalab pricing is per processed page: fast/balanced $4 per 1,000 pages, accurate $10 per 1,000. The free tier gives a $10 monthly credit (personal email; $20 with a work email) at 25 requests/minute — roughly 2,500 pages/month free in `fast` mode. Processing defaults to the US region; EU data residency costs 1.25× usage via `DATALAB_PROCESSING_LOCATION=eu`. Like the Gemini tier, PDF bytes are uploaded to the cloud and deleted best-effort after conversion.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"datalabApiKey": "$DATALAB_API_KEY",
|
||||
"pdf": { "enabled": true, "maxSizeMB": 20, "provider": "auto",
|
||||
"datalabMode": "balanced", "datalabTimeoutMs": 120000 }
|
||||
}
|
||||
```
|
||||
|
||||
Env vars: `DATALAB_API_KEY`, `DATALAB_PROCESSING_LOCATION`, `DATALAB_MODE`, `DATALAB_API_BASE`. `pdf.datalabMode` overrides `DATALAB_MODE`; `datalabTimeoutMs` defaults to 120s and is capped at 300s. `pdf.maxSizeMB` defaults to 20 and is capped at 50.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -81,6 +117,20 @@ Results are deduplicated and capped at 20 sources; `fetchContent` fetches at mos
|
||||
|
||||
`Ctrl+Shift+W` toggles a live activity monitor of request/response data. Results are injected when you approve the curator summary or send selected results without one; on timeout the curator auto-submits with a deterministic fallback summary. If a browser cannot be opened (Docker, WSL, SSH, headless), the curator URL appears in the tool output.
|
||||
|
||||
### Remote curator access
|
||||
|
||||
By default the curator HTTP server binds `127.0.0.1` and hands out `http://localhost:<port>/?session=<token>`. Opt in to remote access when Pi runs somewhere other than your browser:
|
||||
|
||||
| `curatorRemote` value | URL host | Bind address |
|
||||
|---|---|---|
|
||||
| omitted or `false` | `localhost` | `127.0.0.1` |
|
||||
| `true` | `os.hostname()` | `0.0.0.0` |
|
||||
| `{ "host": "h" }` | `h` | `0.0.0.0` |
|
||||
| `{ "bind": "b" }` | `os.hostname()` | `b` |
|
||||
| `{ "host": "h", "bind": "b" }` | `h` | `b` |
|
||||
|
||||
Anything else (a string, `null`, an array) is treated as unconfigured and stays local. `host` only changes the printed URL; `bind` determines who can reach the server — set a matching pair, and prefer one private-network interface over `0.0.0.0`. **Security**: the only access control is the unguessable session token, carried over plain HTTP with no TLS, so anyone observing the traffic or reaching the port with the token can run searches against your configured providers (spending your credits) and edit the summary returned into the agent's context. Remote sessions print the URL instead of opening a browser and raise the default curator idle timeout from 20 to 60 seconds; set `autoOpenBrowser: true` to launch a browser on the remote host anyway. `autoOpenBrowser: false` is also useful locally — it always prints the URL instead of opening Glimpse or a browser, and changes nothing about binding.
|
||||
|
||||
## Configuration
|
||||
|
||||
Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODING_AGENT_DIR` / `XDG_CONFIG_HOME/pi`. Every field is optional. Config changes require a Pi restart.
|
||||
@@ -88,54 +138,106 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
||||
```json
|
||||
{
|
||||
"openaiApiKey": "sk-...",
|
||||
"openaiResponsesUrl": "https://gateway.example.com/v1/responses",
|
||||
"braveApiKey": "BSA_...",
|
||||
"exaApiKey": "exa-...",
|
||||
"parallelApiKey": "...",
|
||||
"tinyfishApiKey": "sk-tinyfish-...",
|
||||
"search1apiApiKey": "...",
|
||||
"searchinfinityApiKey": "...",
|
||||
"queritApiKey": "...",
|
||||
"tavilyApiKey": "tvly-...",
|
||||
"jinaApiKey": "$JINA_API_KEY",
|
||||
"serpdiveApiKey": "sd_live_...",
|
||||
"serpdiveModel": "krill",
|
||||
"kagiApiKey": "$KAGI_API_KEY",
|
||||
"bochaApiKey": "sk-...",
|
||||
"ollamaApiKey": "$OLLAMA_API_KEY",
|
||||
"serpbaseApiKey": "$SERPBASE_API_KEY",
|
||||
"brightdataApiKey": "$BRIGHTDATA_API_KEY",
|
||||
"brightdataSerpZone": "pi_serp",
|
||||
"brightdataUnlockerZone": "pi_unlocker",
|
||||
"perplexityApiKey": "pplx-...",
|
||||
"geminiApiKey": "AIza...",
|
||||
"geminiBaseUrl": "https://my-gateway.example.com/gemini",
|
||||
"cloudflareApiKey": "...",
|
||||
"datalabApiKey": "$DATALAB_API_KEY",
|
||||
"searxngBaseUrl": "https://search.example.com",
|
||||
"searxngHeaders": { "CF-Access-Client-Id": "...", "CF-Access-Client-Secret": "..." },
|
||||
"firecrawlBaseUrl": "https://crawl.example.com",
|
||||
"firecrawlApiKey": "fc-...",
|
||||
"firecrawlApiVersion": "v2",
|
||||
"firecrawlFreshScrape": false,
|
||||
"provider": "openai",
|
||||
"searchRouting": { "providers": ["openai", "brave", "exa"], "fallbackOn": ["transient", "quota", "network"] },
|
||||
"webSearch": { "enabled": true },
|
||||
"searchRouting": { "providers": ["openai", "brave", "exa"],
|
||||
"fallbackOn": ["transient", "quota", "network", "invalid-response"] },
|
||||
"fetchRouting": { "providers": ["http", "firecrawl", "jina", "tinyfish", "search1api",
|
||||
"querit", "kagi", "ollama", "parallel", "brightdata", "gemini"],
|
||||
"allowRemoteHostedProviders": false },
|
||||
"tools": { "webSearch": { "enabled": true }, "sourceCheck": { "enabled": true },
|
||||
"fetchContent": { "enabled": true }, "getSearchContent": { "enabled": true } },
|
||||
"commands": { "websearch": { "enabled": true }, "curator": { "enabled": true },
|
||||
"search": { "enabled": true }, "google-account": { "enabled": true } },
|
||||
"image": { "enabled": true },
|
||||
"toolNames": { "webSearch": "web_search", "sourceCheck": "source_check", "fetchContent": "fetch_content", "getSearchContent": "get_search_content" },
|
||||
"searchModel": "gemini-2.5-flash",
|
||||
"searchModel": "gemini-3.6-flash",
|
||||
"summaryModel": "anthropic/claude-haiku-4-5",
|
||||
"summaryGenerationDeadlineMs": 30000,
|
||||
"maxInlineContentChars": 30000,
|
||||
"workflow": "summary-review",
|
||||
"curatorTimeoutSeconds": 20,
|
||||
"curatorRemote": { "host": "my-box.tailnet.ts.net", "bind": "100.101.102.103" },
|
||||
"autoOpenBrowser": true,
|
||||
"chromeProfile": "Profile 2",
|
||||
"allowBrowserCookies": false,
|
||||
"githubClone": { "enabled": true, "maxRepoSizeMB": 350, "cloneTimeoutSeconds": 30, "clonePath": "/tmp/pi-github-repos" },
|
||||
"youtube": { "enabled": true, "preferredModel": "gemini-3-flash-preview" },
|
||||
"video": { "enabled": true, "preferredModel": "gemini-3-flash-preview", "maxSizeMB": 50 },
|
||||
"youtube": { "enabled": true, "preferredModel": "gemini-3.6-flash" },
|
||||
"video": { "enabled": true, "preferredModel": "gemini-3.6-flash", "maxSizeMB": 50 },
|
||||
"pdf": { "enabled": true, "maxSizeMB": 20, "provider": "auto" },
|
||||
"fetchContent": { "domainPolicy": { "allow": ["example.com"], "deny": ["blocked.example.com"] } },
|
||||
"shortcuts": { "curate": "ctrl+shift+s", "activity": "ctrl+shift+w" },
|
||||
"ssrf": { "allowRanges": ["198.18.0.0/15"], "trustEnvProxy": false }
|
||||
}
|
||||
```
|
||||
|
||||
**Credential sources** (provider API-key fields only): `$NAME` / `${NAME}` reads one env var; a leading `!` runs a trusted local command at provider request time; `$$` and `$!` escape literal prefixes. Commands never run at load or tool registration — each selected provider request re-runs them with a 5-second timeout, 16 KiB output limit, minimized environment, and one-line non-empty stdout requirement (`OP_SESSION_*` is forwarded for 1Password). An explicit source overrides legacy env vars and fails that provider locally rather than falling back on a stale credential.
|
||||
**Credential sources** (provider API-key fields only — including `tinyfishApiKey`, `search1apiApiKey`, `searchinfinityApiKey`, `queritApiKey`, `jinaApiKey`, `kagiApiKey`, `bochaApiKey`, `ollamaApiKey`, `serpbaseApiKey`, `xaiApiKey`, `brightdataApiKey`, `datalabApiKey`): `$NAME` / `${NAME}` reads one env var; a leading `!` runs a trusted local command at provider request time; `$$` and `$!` escape literal prefixes. Commands never run at load or tool registration — each selected provider request re-runs them with a 5-second timeout, 16 KiB output limit, minimized environment, and one-line non-empty stdout requirement (`OP_SESSION_*` is forwarded for 1Password). An explicit source overrides legacy env vars and fails that provider locally rather than falling back on a stale credential. Non-credential fields (`firecrawlBaseUrl`, `firecrawlApiVersion`, `firecrawlFreshScrape`, `brightdataSerpZone`, `brightdataUnlockerZone`) are literal.
|
||||
|
||||
**Legacy env vars** (lower precedence than an explicit source, higher than literal config values): `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TAVILY_API_KEY`, `SERPDIVE_API_KEY`, `ANYSEARCH_API_KEY`, `FIRECRAWL_API_KEY`, `EXA_API_KEY`, `GEMINI_API_KEY`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, `CLOUDFLARE_API_KEY`. Also `SEARXNG_BASE_URL`, `FIRECRAWL_BASE_URL`, `FIRECRAWL_API_VERSION`, `FIRECRAWL_FRESH_SCRAPE`, `SERPDIVE_MODEL`, `PI_ALLOW_BROWSER_COOKIES`.
|
||||
**Legacy env vars** (lower precedence than an explicit source, higher than literal config values): `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TINYFISH_API_KEY`, `SEARCH1API_KEY`, `SEARCHINFINITY_API_KEY`, `QUERIT_API_KEY`, `TAVILY_API_KEY`, `JINA_API_KEY`, `SERPDIVE_API_KEY`, `KAGI_API_KEY`, `BOCHA_API_KEY`, `OLLAMA_API_KEY`, `SERPBASE_API_KEY`, `ANYSEARCH_API_KEY`, `XAI_API_KEY`, `BRIGHTDATA_API_KEY`, `FIRECRAWL_API_KEY`, `EXA_API_KEY`, `GEMINI_API_KEY`, `DATALAB_API_KEY`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, `CLOUDFLARE_API_KEY`. Also `SEARXNG_BASE_URL`, `FIRECRAWL_BASE_URL`, `FIRECRAWL_API_VERSION`, `FIRECRAWL_FRESH_SCRAPE`, `SERPDIVE_MODEL`, `PI_ALLOW_BROWSER_COOKIES`.
|
||||
|
||||
**Routing**: `provider` (or `searchProvider`) sets the default and takes precedence over `searchRouting`. `searchRouting` opts into an ordered `providers` list plus `fallbackOn` (`transient`, `quota`, `network`) — only those typed failures continue to the next candidate. Named providers stay strict and exhausted routes return per-provider diagnostics. `webSearch.enabled: false` unregisters the search and source-check tools while leaving fetch/content tools. `toolNames` renames the public tools for environments where the defaults collide.
|
||||
**Routing**: `provider` (or `searchProvider`) sets the default and takes precedence over `searchRouting`. `searchRouting` opts into an ordered `providers` list plus `fallbackOn` (`transient`, `quota`, `network`, `invalid-response`) — only those typed failures continue to the next candidate. Named providers stay strict and exhausted routes return per-provider diagnostics. `fetchRouting.providers` reorders or restricts the `fetch_content` chain (`http`, `firecrawl`, `jina`, `tinyfish`, `search1api`, `querit`, `kagi`, `ollama`, `parallel`, `brightdata`, `gemini`); when absent the default order is unchanged. Third-party hosted fetchers are disabled for remote HTTP(S) targets unless `fetchRouting.allowRemoteHostedProviders: true`, because a hosted service performs its own fetch and can see a different redirect chain than the local safety gate.
|
||||
|
||||
**Models**: `searchModel` overrides only the Gemini API model used for search (default `gemini-2.5-flash`). `summaryModel` sets the curator/`auto-summary` draft model; when Pi's `enabledModels` is configured, summaries are limited to that allowlist and fall back to a deterministic summary rather than calling an unrelated model.
|
||||
**Enabling and naming tools**: set `"enabled": false` under `tools`, `commands`, `image`, or `pdf` to disable a feature. Tool-specific settings override the legacy `webSearch.enabled` shorthand, which otherwise still disables `web_search` and `source_check`. `image.enabled: false` blocks direct image fetches, video frame extraction, and thumbnails; `pdf.enabled: false` blocks PDF extraction. `toolNames` renames the public tools where the defaults collide. Tool and command registration changes need a Pi restart.
|
||||
|
||||
**Models**: `searchModel` overrides only the Gemini API model used for search (default `gemini-3.6-flash`); Gemini Web browser-cookie fallback has its own `gemini-3.1-pro` default, and explicitly configured unsupported Web models fail rather than silently downgrading. `openaiSearchModel` pins the OpenAI `web_search` model verbatim (bypassing automatic newest-terra selection, so gateway-only ids work), and `xaiSearchModel` does the same for xAI. `openaiResponsesUrl` points OpenAI `web_search`/`source_check` at a third-party Responses-compatible gateway (default `https://api.openai.com/v1/responses`). `summaryModel` sets the curator/`auto-summary` draft model, resolving through routed provider registrations such as OpenRouter when the native provider is unavailable; when Pi's `enabledModels` is configured, summaries are limited to that allowlist and fall back to a deterministic summary rather than calling an unrelated model. `summaryGenerationDeadlineMs` bounds one summary attempt (default 30000, capped at 600000). `maxInlineContentChars` sets the direct `fetch_content` slice plus the default and maximum `get_search_content` slice (default 30000, capped at 200000; full content stays stored for later retrieval).
|
||||
|
||||
**Security**: `fetchContent.domainPolicy` is an optional hostname allow/deny policy checked before HTTP(S) handling and each redirect this extension follows — bare hostnames match subdomains, `deny` wins, and local/non-HTTP sources are exempt. It adds to, not replaces, the SSRF guard. `ssrf.allowRanges` exempts specific CIDRs (for TUN + fake-IP proxies such as Surge/Clash/Mihomo/Stash); it is off by default and all-address CIDRs are rejected. `ssrf.trustEnvProxy` skips local DNS preflight for proxied hostnames only, still blocking localhost, literal private IPs, and `NO_PROXY` matches. Firecrawl requests are cache-only (`lockdown: true`) unless `firecrawlFreshScrape` is set — only enable that for an isolated Firecrawl deployment, since this extension cannot control the Firecrawl server's own egress.
|
||||
|
||||
**SERPdive** `serpdiveModel` picks retrieval depth: `krill` (free default, extracted page content, answer assembled from sources), `mako` (1 credit, fact-carrying sentences plus synthesized answer), `moby` (1.5 credits, full readable content plus cited answer). Unrecognized values fall back to `krill` so a typo cannot cost money. SERPdive has no time-range or domain parameter, so `recencyFilter` is a ranking hint appended to the question and `domainFilter` is applied locally; `numResults` maps to `max_results`, a cap between 1 and 10.
|
||||
## Provider Notes
|
||||
|
||||
**SearXNG**: `searxngBaseUrl` / `SEARXNG_BASE_URL` enables a self-hosted JSON API, preferred first in `auto` mode. Its base URL and redirects remain subject to the SSRF guard — add only the narrowest self-hosted range to `ssrf.allowRanges` when it resolves privately. Optional `searxngHeaders` merges extra HTTP headers (string values only; invalid names ignored) for reverse-proxy or Zero Trust auth such as Cloudflare Access service tokens, overriding the default `Accept: application/json` when the same name is supplied.
|
||||
|
||||
**SERPdive**: `serpdiveModel` picks retrieval depth: `krill` (free default, extracted page content, answer assembled from sources), `mako` (1 credit, fact-carrying sentences plus synthesized answer), `moby` (1.5 credits, full readable content plus cited answer). Unrecognized values fall back to `krill` so a typo cannot cost money. SERPdive has no time-range or domain parameter, so `recencyFilter` is a ranking hint appended to the question and `domainFilter` is applied locally; `numResults` maps to `max_results`, a cap between 1 and 10.
|
||||
|
||||
**Jina**: `jinaApiKey` / `JINA_API_KEY` enables [Jina Search](https://s.jina.ai); in `auto` mode it runs after Tavily and before SERPdive. `numResults` maps to its bounded `count`, included domains become `site` filters, and excluded domains plus recency go into the query. Without `includeContent` it requests SERP metadata only; with it, Jina visits pages and returns Markdown inline (slower, more tokens). Jina Reader remains a `fetch_content` fallback.
|
||||
|
||||
**TinyFish**: `tinyfishApiKey` / `TINYFISH_API_KEY` enables the Search and Fetch APIs (endpoints are built in). In `auto` mode it runs after Parallel and before Search1API. Supports `numResults`, `recencyFilter`, and include/exclude domain filters, paginating above 10 results; with `includeContent`, URLs go to TinyFish Fetch in batches of up to 10. TinyFish Fetch is also a `fetch_content` fallback after Jina Reader. Both APIs are documented as credit-free with Free-plan limits of 30 searches/minute and 150 fetched URLs/minute.
|
||||
|
||||
**Search1API**: `search1apiApiKey` / `SEARCH1API_KEY` enables Search and Crawl; in `auto` mode it runs after TinyFish and before Searchinfinity. `includeContent` maps to Deep Search and returns crawled result content inline. Credit-based: a basic search is 1 credit, Deep Search adds 1 per successfully crawled page, and a Crawl request is 1 — Deep Search is never enabled unless `includeContent` is true. The Crawl endpoint is a `fetch_content` fallback after Jina Reader and TinyFish.
|
||||
|
||||
**Searchinfinity**: `searchinfinityApiKey` / `SEARCHINFINITY_API_KEY` enables Byteplus Searchinfinity (the Global edition of Volcengine 豆包搜索); in `auto` mode it runs after Search1API and before Querit.
|
||||
|
||||
**Kagi**: `kagiApiKey` / `KAGI_API_KEY` enables Kagi Search as a normal configured provider, mapping `numResults` to Kagi's `limit`; when Kagi includes extracted Markdown, `includeContent` exposes it inline. Kagi Extract is a `fetch_content` fallback after Querit and before Ollama/Parallel, with local target validation and authorization stripped across cross-origin API redirects.
|
||||
|
||||
**Ollama**: `ollamaApiKey` / `OLLAMA_API_KEY` enables Ollama Cloud Web Search without a local daemon — the same account key used for Cloud inference authenticates `POST https://ollama.com/api/web_search`, with `numResults` capped at Ollama's documented max of 10. Ollama Web Fetch is a `fetch_content` fallback after Kagi and before Parallel.
|
||||
|
||||
**DuckDuckGo**: keyless and explicit-only — select `provider: "duckduckgo"` or place it in `searchRouting`; it is never chosen by `auto` and never participates in `provider: "all"`. Domain filters are enforced locally after redirect URLs are decoded, and `recencyFilter` is not guaranteed because the HTML endpoint has no documented stable time parameter. A 200 page with no parseable results is reported as an invalid response.
|
||||
|
||||
**Bright Data**: `brightdataApiKey` / `BRIGHTDATA_API_KEY` plus a zone. The SERP provider needs `brightdataSerpZone` (a zone of type `serp`); the Web Unlocker extraction fallback needs `brightdataUnlockerZone` (type `unblocker`). The zones are never substituted for each other, so enabling one product does not opt into the other. Search is explicit-only, maps domain filters to Google `site:` clauses and recency to `tbs`, validates the returned SERP envelope, and surfaces provider errors rather than converting them to empty results — every billed `200` that cannot be read throws instead of reporting zero results, and quoted upstream text cannot impersonate a status or rate-limit phrase. Web Unlocker runs last of the remote scraping providers, ahead of only the Gemini fallbacks, and applies no minimum-length check — any non-empty body it returns (including a short consent or paywall stub) is final for that URL. Keep `brightdataUnlockerZone` unset for URLs that must not be disclosed to a third party.
|
||||
|
||||
**SerpBase**: `serpbaseApiKey` / `SERPBASE_API_KEY` with `provider: "serpbase"` queries SerpBase's Google Search Results API. Explicit-only, because each request can consume paid Google SERP credits. Domain filters become Google `site:` clauses (reapplied locally) and recency maps to `tbs`.
|
||||
|
||||
**Gemini gateway**: `geminiBaseUrl` / `GOOGLE_GEMINI_BASE_URL` overrides the Gemini API host (bare host, no trailing slash or version segment). When the host contains `gateway.ai.cloudflare.com`, auth uses `cf-aig-authorization: Bearer <token>` from `cloudflareApiKey`/`CLOUDFLARE_API_KEY` and `GEMINI_API_KEY` is not required for generate-content calls — but local video upload still uses Google's Files API directly.
|
||||
|
||||
## Limits and Limitations
|
||||
|
||||
Perplexity is capped at 10 requests/minute client-side; content fetches run 3 concurrent with a 30s timeout per URL; Gemini handles videos up to ~1 hour; local video upload is 50 MB max. Chromium cookie extraction for Gemini Web is opt-in (`allowBrowserCookies: true` or `PI_ALLOW_BROWSER_COOKIES=1`) and may trigger a macOS Keychain dialog; cookie DBs are copied to a temporary read-only working copy. Private/age-restricted YouTube videos may fail on all paths, PDFs are text-only, GitHub branch names with slashes may misresolve file paths, and non-code GitHub URLs (issues, PRs, wiki) fall through to normal web extraction.
|
||||
Perplexity is capped at 10 requests/minute client-side; Jina Search, TinyFish, Search1API, and Searchinfinity apply their documented plan limits, and Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent with a 30s timeout for the direct HTTP fetch of each URL; remote extraction fallbacks carry their own budgets — Jina Reader 30s, Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s, Datalab 120s (capped at 300s, 25 requests/minute on the free tier). Gemini handles videos up to ~1 hour; local video upload is 50 MB max. Chromium cookie extraction for Gemini Web is opt-in (`allowBrowserCookies: true` or `PI_ALLOW_BROWSER_COOKIES=1`) and may trigger a macOS Keychain dialog; cookie DBs are copied to a temporary read-only working copy. Private/age-restricted YouTube videos may fail on all paths, GitHub branch names with slashes may misresolve file paths, and non-code GitHub URLs (issues, PRs, wiki) fall through to normal web extraction.
|
||||
|
||||
@@ -12,7 +12,7 @@ Run `/login` and select: ChatGPT Plus/Pro (Codex), Claude Pro/Max, GitHub Copilo
|
||||
- **Claude Pro/Max**: third-party harness usage draws from Anthropic "extra usage" and is billed per token, not against plan limits.
|
||||
- **GitHub Copilot**: Enter for github.com, or enter a GitHub Enterprise Server domain. "Model not supported" is fixed by enabling the model in VS Code Copilot Chat.
|
||||
- **xAI**: `/login xai` → **Use a subscription**; `XAI_API_KEY` remains available under **Use an API key**.
|
||||
- **OpenRouter**: `/login openrouter` → **Sign in with OpenRouter** runs a PKCE flow that mints a user-controlled API key billed from OpenRouter credits (it does not expire automatically).
|
||||
- **OpenRouter**: `/login openrouter` → **Sign in with OpenRouter** runs a PKCE flow that mints a user-controlled API key billed from OpenRouter credits (it does not expire automatically). On remote/headless machines (e.g. over SSH) the browser cannot reach the loopback callback — paste the final redirect URL or the authorization code into the login prompt instead.
|
||||
- **Radius**: a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens; the catalog refreshes independently into `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` plus a gateway `baseUrl`.
|
||||
|
||||
## API Key Providers
|
||||
@@ -43,13 +43,17 @@ Set an environment variable before startup, or store a key with `/login`.
|
||||
| Hugging Face | `HF_TOKEN` | `huggingface` |
|
||||
| Fireworks | `FIREWORKS_API_KEY` | `fireworks` |
|
||||
| Together AI | `TOGETHER_API_KEY` | `together` |
|
||||
| Baseten | `BASETEN_API_KEY` | `baseten` |
|
||||
| Kimi For Coding | `KIMI_API_KEY` | `kimi-coding` |
|
||||
| MiniMax (Global / China) | `MINIMAX_API_KEY` / `MINIMAX_CN_API_KEY` | `minimax` / `minimax-cn` |
|
||||
| Qwen Token Plan (Global / China) | `QWEN_TOKEN_PLAN_API_KEY` / `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan` / `qwen-token-plan-cn` |
|
||||
| Qwen Token Plan (existing catalog / Individual) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` / `qwen-token-plan-individual` |
|
||||
| Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |
|
||||
| Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |
|
||||
| Xiaomi MiMo Token Plan (CN / AMS / SGP) | `XIAOMI_TOKEN_PLAN_CN_API_KEY`, `XIAOMI_TOKEN_PLAN_AMS_API_KEY`, `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-cn`, `-ams`, `-sgp` |
|
||||
|
||||
Authoritative source: `packages/ai/src/env-api-keys.ts` in `earendil-works/pi-mono`.
|
||||
`qwen-token-plan-individual` uses the same international endpoint and `QWEN_TOKEN_PLAN_API_KEY` as `qwen-token-plan`, but limits the picker to models documented for Individual subscriptions; the older provider keeps its broader catalog for backward compatibility. With `auth.json`, store the credential under the provider you select — the environment variable is shared by both international providers.
|
||||
|
||||
Authoritative source: `packages/ai/src/env-api-keys.ts` in `earendil-works/pi`.
|
||||
|
||||
## Auth File
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Pi loads context files at startup:
|
||||
- `~/.pi/agent/AGENTS.md`
|
||||
- `AGENTS.md` or `CLAUDE.md` from parent directories and the current directory
|
||||
|
||||
Run `/reload` or restart after changing context files.
|
||||
A directory containing `AGENTS.override.md` contributes that file instead of its `AGENTS.md`/`CLAUDE.md`. Run `/reload` or restart after changing context files.
|
||||
|
||||
## Common First Tasks
|
||||
|
||||
|
||||
@@ -69,7 +69,16 @@ Responses have the shape `{"type":"response","command":"...","success":true|fals
|
||||
|
||||
`agent_start`; `agent_end` (`messages`, `willRetry`); `agent_settled` (nothing will continue automatically — no retry, compaction retry, or queued continuation); `turn_start` / `turn_end`; `message_start` / `message_update` / `message_end`; `bash_execution_update`; `tool_execution_start` / `_update` / `_end`; `queue_update`; `compaction_start` / `compaction_end`; `auto_retry_start` / `auto_retry_end`; `summarization_retry_scheduled` / `summarization_retry_attempt_start` / `summarization_retry_finished`; `extension_error`.
|
||||
|
||||
`message_update.assistantMessageEvent` types: `start`, `text_start`, `text_delta`, `text_end`, `thinking_start`, `thinking_delta`, `thinking_end`, `toolcall_start`, `toolcall_delta`, `toolcall_end`, `done` (`stop`/`length`/`toolUse`), `error` (`aborted`/`error`).
|
||||
`message_update.assistantMessageEvent` types: `text_start`, `text_delta`, `text_end`, `thinking_start`, `thinking_delta`, `thinking_end`, `toolcall_start`, `toolcall_delta`, `toolcall_end`.
|
||||
|
||||
`message_update` is delta-only — it carries a top-level `usage` object plus the delta event, and omits both the former cumulative `message` field and `assistantMessageEvent.partial`:
|
||||
|
||||
```json
|
||||
{"type":"message_update","usage":{"input":100,"output":1,"cacheRead":0,"cacheWrite":0,"totalTokens":101,"cost":{}},
|
||||
"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello "}}
|
||||
```
|
||||
|
||||
`usage` is the latest cumulative provider-reported usage and may stay zero until completion. Clients needing a live partial message must assemble it from `message_start` and subsequent events using `contentIndex`; treat `message_end.message` as authoritative. For tool calls, buffer `toolcall_delta.delta` — `toolcall_end.toolCall` holds the completed call.
|
||||
|
||||
`compaction_start`/`compaction_end` carry `reason` (`"manual"`, `"threshold"`, `"overflow"`). On overflow success, `willRetry` is `true` and the prompt is retried. Aborted compaction returns `result: null, aborted: true`; failed compaction returns `result: null, aborted: false` plus `errorMessage`. `tool_execution_update.partialResult` is cumulative, so clients can replace their display each update.
|
||||
|
||||
|
||||
@@ -93,11 +93,27 @@ modelRuntime.getModel(provider, id); // built-ins + models.json, no auth
|
||||
await modelRuntime.getAvailable(); // only models with valid auth
|
||||
await modelRuntime.checkAuth(providerId);
|
||||
modelRuntime.getProviders(); // provider.auth methods and status
|
||||
modelRuntime.setRuntimeApiKey(provider, key); // not persisted
|
||||
await modelRuntime.setRuntimeApiKey(provider, key); // not persisted
|
||||
```
|
||||
|
||||
Credential priority: runtime overrides → `auth.json` → environment variables → custom fallback from `models.json`. `getModel(provider, id)` from `@earendil-works/pi-ai` looks up built-ins only. Inject any pi-ai `CredentialStore` (for example `InMemoryCredentialStore`) via `credentials`.
|
||||
|
||||
### Catalog Refresh and Deadlines
|
||||
|
||||
`create()` restores cached catalogs but does not refresh them from `pi.dev` by default. Opt in with `ModelRuntime.create({ allowModelNetwork: true, modelRefreshTimeoutMs: 15_000 })`. Remote catalogs persist to `~/.pi/agent/models-store.json` (override with `modelsStorePath`, or inject `modelsStore`); refreshes are throttled to once per provider every four hours unless forced, and `PI_OFFLINE` disables model network access entirely.
|
||||
|
||||
Public model/auth operations and `ModelRuntime.create({ signal })` accept optional abort signals and are unbounded when omitted — SDK applications own deadline policy:
|
||||
|
||||
```ts
|
||||
const result = await modelRuntime.refresh({ providers: ["anthropic"], signal: AbortSignal.timeout(15_000) });
|
||||
if (result.aborted) console.warn("Catalog refresh timed out; using cached models");
|
||||
for (const [providerId, error] of result.errors) console.warn(providerId, error);
|
||||
```
|
||||
|
||||
Force an immediate refresh with `await modelRuntime.refresh({ allowNetwork: true, force: true, signal })`. Each `refresh()` starts a new provider generation, so it does not queue behind a stalled refresh and stale generations cannot publish afterward. A failed or timed-out refresh never undoes a successful credential operation.
|
||||
|
||||
`login()`, `logout()`, `setRuntimeApiKey()`, and `removeRuntimeApiKey()` are async and resolve once the affected provider's cached/built-in catalog, composition, and availability snapshot are locally consistent; they do not wait for remote freshness. If credentials committed but local synchronization failed, they reject with the exported `CredentialSynchronizationError` — inspect `providerId`, `operation`, `credential`, and `cause` rather than blindly retrying the credential mutation.
|
||||
|
||||
To match CLI parsing, use `resolveCliModel({ cliModel, modelRuntime })` (uses all registered models so `--api-key` first-run flows resolve before stored auth exists) and `resolveModelScopeWithDiagnostics(patterns, modelRuntime)` (matches `--models`/`enabledModels` semantics and returns warnings instead of printing).
|
||||
|
||||
Session options also accept `model`, `thinkingLevel` (`off`…`max`), and `scopedModels: [{ model, thinkingLevel }]` for Ctrl+P cycling. With no model: restore from session, then settings default, then first available.
|
||||
@@ -132,4 +148,4 @@ Prefer the SDK for type safety, same-process integration, direct state access, o
|
||||
|
||||
## Important Exports
|
||||
|
||||
`createAgentSession`, `createAgentSessionRuntime`, `AgentSessionRuntime`, `createAgentSessionServices`, `createAgentSessionFromServices`, `ModelRuntime`, `ModelRegistry`, `resolveCliModel`, `resolveModelScopeWithDiagnostics`, `DefaultResourceLoader`, `ResourceLoader` type, `createEventBus`, `CONFIG_DIR_NAME`, `defineTool`, `getAgentDir`, `getPackageDir`, `getReadmePath`, `getDocsPath`, `getExamplesPath`, `SessionManager`, `SettingsManager`, the tool factories above, `InteractiveMode`, `runPrintMode`, `runRpcMode`, and types for options, results, extensions (`ExtensionAPI`, `ExtensionFactory`, `InlineExtension`), tools, skills, and prompt templates.
|
||||
`createAgentSession`, `createAgentSessionRuntime`, `AgentSessionRuntime`, `createAgentSessionServices`, `createAgentSessionFromServices`, `ModelRuntime`, `ModelRegistry`, `CredentialSynchronizationError`, `resolveCliModel`, `resolveModelScopeWithDiagnostics`, `DefaultResourceLoader`, `ResourceLoader` type, `createEventBus`, `CONFIG_DIR_NAME`, `defineTool`, `getAgentDir`, `getPackageDir`, `getReadmePath`, `getDocsPath`, `getExamplesPath`, `SessionManager`, `SettingsManager`, the tool factories above, `InteractiveMode`, `runPrintMode`, `runRpcMode`, and types for options, results, extensions (`ExtensionAPI`, `ExtensionFactory`, `InlineExtension`), tools, skills, and prompt templates.
|
||||
|
||||
@@ -21,7 +21,7 @@ When an interactive session starts in such a project with no saved decision for
|
||||
|
||||
Trusting a project allows Pi to load `.pi/settings.json`, `.pi` resources (extensions, skills, prompt templates, themes, system prompt files), install missing project packages configured through project settings, and execute project-local and project package-managed extensions.
|
||||
|
||||
Declining skips protected resources. `AGENTS.md` and `CLAUDE.md` context files load regardless of trust unless context loading is disabled. Before trust resolves, Pi loads only context files, user/global extensions, and CLI `-e` extensions — those can handle the `project_trust` event, and the first extension returning a yes/no decision owns it.
|
||||
Declining skips protected resources. Context files — `AGENTS.override.md`, `AGENTS.md`, and `CLAUDE.md` — load regardless of trust unless context loading is disabled. Before trust resolves, Pi loads only context files, user/global extensions, and CLI `-e` extensions — those can handle the `project_trust` event, and the first extension returning a yes/no decision owns it.
|
||||
|
||||
Non-interactive modes (`-p`, `--mode json`, `--mode rpc`) never prompt. Without an applicable saved decision, `"ask"` and `"never"` ignore trust-gated resources while `"always"` trusts them. `--approve`/`-a` and `--no-approve`/`-na` override for one run.
|
||||
|
||||
@@ -49,4 +49,4 @@ Bind-mounting a host workspace read/write means writes from inside the container
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
Follow the repository [Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md); do not open a public issue. Expected local-agent behavior, the absence of a built-in sandbox, prompt injection from untrusted content, and behavior of user-installed extensions or skills are generally outside the security boundary unless the report shows a real privilege-boundary bypass or access the local user did not already have.
|
||||
Follow the repository [Security Policy](https://github.com/earendil-works/pi/blob/main/SECURITY.md); do not open a public issue. Expected local-agent behavior, the absence of a built-in sandbox, prompt injection from untrusted content, and behavior of user-installed extensions or skills are generally outside the security boundary unless the report shows a real privilege-boundary bypass or access the local user did not already have.
|
||||
|
||||
@@ -33,6 +33,8 @@ Base (from `pi-ai`):
|
||||
- `ToolResultMessage` — `toolCallId`, `toolName`, `content: (Text|Image)[]`, optional `details`, optional `usage` (nested LLM work performed by the tool), `isError`, `timestamp`
|
||||
- `Usage` — `input`, `output`, `cacheRead`, `cacheWrite`, `totalTokens`, and `cost` with the same four fields plus `total`
|
||||
|
||||
The exported pi-ai `StopReason` type also includes `"pending"`, but that value is reserved for partial messages in streaming events. Terminal `done`/`error` messages replace it with a completion reason before Pi persists the assistant message, so `"pending"` should never appear in session JSONL.
|
||||
|
||||
Extended (from `pi-coding-agent`):
|
||||
|
||||
- `BashExecutionMessage` — `command`, `output`, `exitCode`, `cancelled`, `truncated`, optional `fullOutputPath`, optional `excludeFromContext` (true for `!!`)
|
||||
|
||||
@@ -30,9 +30,11 @@ Interactive startup asks before trusting a project folder that has project-local
|
||||
|
||||
`theme` (`"dark"`), `externalEditor` (Ctrl+G command; takes precedence over `$VISUAL`/`$EDITOR` — use `"code --wait"` for VS Code), `quietStartup` (`false`), `defaultProjectTrust` (`"ask"`, global only), `collapseChangelog` (`false`), `enableInstallTelemetry` (`true`), `enableAnalytics` (`false`, only asked during experimental first-time setup with `PI_EXPERIMENTAL=1`), `trackingId`, `doubleEscapeAction` (`"tree"` | `"fork"` | `"none"`), `treeFilterMode` (`"default"` | `"no-tools"` | `"user-only"` | `"labeled-only"` | `"all"`), `editorPaddingX` (`0`, range 0–3), `outputPad` (`1`, 0 or 1), `autocompleteMaxVisible` (`5`, range 3–20), `showHardwareCursor` (`false`).
|
||||
|
||||
Fullscreen TUI: `tuiMode` (`"regular"` default, or experimental `"fullscreen"`; `/settings` changes apply immediately and `--tui-mode` overrides at startup), `fullscreenExitOutput` (`"transcript"` prints the final transcript plus resume hint, `"resume-hint"` restores the previous screen and prints only the hint), `fullscreenScrollbar` (`"auto"` shows it while scrolling, `"always"` reserves the rightmost column, `"hidden"`). The last two have no effect in regular mode.
|
||||
|
||||
## Network, Warnings, Markdown
|
||||
|
||||
`httpProxy` (applied as `HTTP_PROXY`/`HTTPS_PROXY`, global only). `warnings.anthropicExtraUsage` (`true`) warns when Anthropic subscription auth may use paid extra usage. `markdown.codeBlockIndent` (`" "`).
|
||||
`httpProxy` (applied as `HTTP_PROXY`/`HTTPS_PROXY`, global only). `warnings.anthropicExtraUsage` (`true`) warns when Anthropic subscription auth may use paid extra usage. `markdown.codeBlockIndent` (`" "`), `markdown.mermaid` (`"streaming"`, or `"final"` / `"off"`).
|
||||
|
||||
## Compaction and Branch Summary
|
||||
|
||||
@@ -50,11 +52,15 @@ Keep `retry.provider.maxRetries` at `0` unless provider-level retries are explic
|
||||
|
||||
## Terminal and Images
|
||||
|
||||
`terminal.showImages` (`true`), `terminal.imageWidthCells` (`60`), `terminal.clearOnShrink` (`false`), `images.autoResize` (`true`, 2000×2000 max), `images.blockImages` (`false`).
|
||||
`terminal.showImages` (`true`), `terminal.imageWidthCells` (`60`), `terminal.clearOnShrink` (`false`), `images.autoResize` (`true`, 2000×2000 max; applies to `@file` attachments, `read`, and images returned by tools), `images.blockImages` (`false`).
|
||||
|
||||
## Tools
|
||||
|
||||
`defaultTools` is a string array of built-in tools enabled at startup; omitting it uses Pi's standard defaults. Extension and SDK custom tools stay enabled, and an empty array starts with no built-ins while preserving them. `--tools` replaces this with a strict allowlist for all tools, `--no-tools` disables everything, `--no-builtin-tools` disables the built-in defaults, and `--exclude-tools` filters the result. A project `defaultTools` array replaces the global array.
|
||||
|
||||
## Shell
|
||||
|
||||
`shellPath` (supports leading `~`), `shellCommandPrefix` (prefix for every bash command), `npmCommand` (argv array, e.g. `["mise", "exec", "node@20", "--", "npm"]`). `npmCommand` covers all npm package-manager operations; user npm packages install under `~/.pi/agent/npm/`, project ones under `.pi/npm/`. When `npmCommand` is configured, git package dependency installs use plain `install` for wrapper compatibility.
|
||||
`shellPath` (supports leading `~`; Windows paths in JSON need forward slashes or escaped backslashes, e.g. `"C:/Program Files/Git/bin/bash.exe"`), `shellCommandPrefix` (prefix for every bash command), `npmCommand` (argv array, e.g. `["mise", "exec", "node@20", "--", "npm"]`). `npmCommand` covers all npm package-manager operations; user npm packages install under `~/.pi/agent/npm/`, project ones under `.pi/npm/`. When `npmCommand` is configured, git package dependency installs use plain `install` for wrapper compatibility.
|
||||
|
||||
## Sessions and Model Cycling
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ Pi uses the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-p
|
||||
|
||||
## Works Out of the Box
|
||||
|
||||
Kitty and iTerm2. Apple Terminal enables enhanced key reporting when available; if it still sends plain Return for `Shift+Enter`, Pi uses a local macOS modifier fallback — which only works when Pi runs on the same Mac, not over SSH. VS Code 1.109.5+ also works by default.
|
||||
Kitty and iTerm2 (regular TUI mode). Apple Terminal enables enhanced key reporting when available; if it still sends plain Return for `Shift+Enter`, Pi uses a local macOS modifier fallback — which only works when Pi runs on the same Mac, not over SSH. VS Code 1.109.5+ also works by default.
|
||||
|
||||
### iTerm2 in fullscreen TUI mode
|
||||
|
||||
Pi owns the viewport, so iTerm2 sends mouse-wheel reports instead of scrolling its native scrollback. With iTerm2's default fast-trackpad behavior those reports can lose most of an accelerated wheel delta. If fast gestures move only ~one line at a time, open **iTerm2 → Settings → Advanced**, find **Trackpad scrolls fast?** and set it to **No**. This is an iTerm2-wide workaround (tracked in iTerm2 issue 9619). Inline images also render as text placeholders in fullscreen mode because iTerm2's inline-image protocol cannot delete or crop placements during application-owned scrolling.
|
||||
|
||||
## Ghostty
|
||||
|
||||
@@ -18,6 +22,8 @@ keybind = alt+backspace=text:\x1b\x7f
|
||||
|
||||
Older Claude Code versions may have added `keybind = shift+enter=text:\n`. That sends a raw linefeed, which inside Pi is indistinguishable from `Ctrl+J`, so tmux and Pi no longer see a real `shift+enter` event. Remove it unless you still need it for Claude Code in tmux. Pi binds `Ctrl+J` as a default newline alias, so `Shift+Enter` keeps working through that remap without extra Pi configuration.
|
||||
|
||||
In fullscreen TUI mode links stay clickable, but Ghostty hides its hover underline and lower-left URL preview while Pi captures mouse input. Hold `Shift+Command` (macOS) or `Shift+Ctrl` (Linux) for Ghostty's native link handling.
|
||||
|
||||
## WezTerm
|
||||
|
||||
Usually works via xterm `modifyOtherKeys`. To force the Kitty protocol, in `~/.wezterm.lua`:
|
||||
|
||||
@@ -11,10 +11,12 @@ Themes are JSON files defining TUI colors.
|
||||
- Project: `.pi/themes/*.json` (only after project trust)
|
||||
- Packages: `themes/` directories or `pi.themes` entries in `package.json`
|
||||
- Settings: `themes` array
|
||||
- CLI: `--theme <path>` (repeatable)
|
||||
- CLI: `--theme <path>` (repeatable) loads a theme file; `--use-theme <name[/name]>` selects the initial theme for one run
|
||||
|
||||
Disable discovery with `--no-themes`. Select via `/settings` or `{"theme": "my-theme"}`. On first run Pi detects the terminal background and defaults to `dark` or `light`. Editing the active custom theme file hot-reloads it for immediate feedback.
|
||||
|
||||
`--use-theme light` starts a run with that theme without changing the saved setting; `--use-theme light/dark` uses `lightTheme/darkTheme` syntax to follow terminal appearance. Picking another theme later in `/settings` applies immediately and saves normally.
|
||||
|
||||
## Format
|
||||
|
||||
```json
|
||||
@@ -28,13 +30,13 @@ Disable discovery with `--no-themes`. Select via `/settings` or `{"theme": "my-t
|
||||
|
||||
- `name` is required, must be unique, and must not contain `/`.
|
||||
- `vars` is optional — reusable colors referenced by name from `colors`.
|
||||
- `colors` must define all 51 tokens. `thinkingMax` is the only optional one and falls back to `thinkingXhigh`.
|
||||
- `colors` must define all 51 required tokens. Optional tokens fall back: `thinkingMax` → `thinkingXhigh`, `scrollbarThumb` and `searchMatchBg` → `selectedBg`, `searchMatchText` → `text`.
|
||||
- `$schema` enables editor auto-completion and validation.
|
||||
|
||||
## Color Tokens (51)
|
||||
## Color Tokens (51 required + 4 optional)
|
||||
|
||||
- **Core UI (11)**: `accent`, `border`, `borderAccent`, `borderMuted`, `success`, `error`, `warning`, `muted`, `dim`, `text`, `thinkingText`
|
||||
- **Backgrounds & content (11)**: `selectedBg`, `userMessageBg`, `userMessageText`, `customMessageBg`, `customMessageText`, `customMessageLabel`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`, `toolTitle`, `toolOutput`
|
||||
- **Backgrounds & content (11 required + 3 optional)**: `selectedBg`, `userMessageBg`, `userMessageText`, `customMessageBg`, `customMessageText`, `customMessageLabel`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`, `toolTitle`, `toolOutput`, plus optional `scrollbarThumb` (fullscreen scrollbar thumb), `searchMatchBg`, and `searchMatchText` (transcript search). Non-current search matches render `searchMatchText` on `searchMatchBg` with an underline; the current match reverses that pair and uses bold.
|
||||
- **Markdown (10)**: `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet`
|
||||
- **Tool diffs (3)**: `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext`
|
||||
- **Syntax (9)**: `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation`
|
||||
|
||||
@@ -67,7 +67,7 @@ if (matchesKey(data, Key.ctrl("c"))) { /* ... */ }
|
||||
|
||||
## Theming
|
||||
|
||||
Use the `theme` passed into the callback or renderer — never import a global theme. `theme.fg(color, text)` covers general (`text`, `accent`, `muted`, `dim`), status (`success`, `error`, `warning`), borders (`border`, `borderAccent`, `borderMuted`), messages (`userMessageText`, `customMessageText`, `customMessageLabel`), tools (`toolTitle`, `toolOutput`), diffs (`toolDiffAdded`/`Removed`/`Context`), markdown (`md*`), syntax (`syntax*`), thinking levels (`thinkingOff`…`thinkingMax`), and `bashMode`. `theme.bg(color, text)` covers `selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`. Text styles: `theme.bold`, `theme.italic`, `theme.strikethrough`.
|
||||
Use the `theme` passed into the callback or renderer — never import a global theme. `theme.fg(color, text)` covers general (`text`, `accent`, `muted`, `dim`, `searchMatchText`), status (`success`, `error`, `warning`), borders (`border`, `borderAccent`, `borderMuted`), messages (`userMessageText`, `customMessageText`, `customMessageLabel`), tools (`toolTitle`, `toolOutput`), diffs (`toolDiffAdded`/`Removed`/`Context`), markdown (`md*`), syntax (`syntax*`), thinking levels (`thinkingOff`…`thinkingMax`), and `bashMode`. `theme.bg(color, text)` covers `selectedBg`, `searchMatchBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`. Text styles: `theme.bold`, `theme.italic`, `theme.strikethrough`.
|
||||
|
||||
Components that pre-bake theme colors into cached strings must rebuild that content in `invalidate()` — clearing a render cache is not enough. This applies to `theme.fg`/`theme.bg` strings stored in child components, `highlightCode()` output, and child trees that embed colors. It is unnecessary when you pass theme callbacks that run at render time, for simple containers, or for stateless render.
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ On Windows Terminal, Alt+Enter is fullscreen by default — remap it (`reference
|
||||
|
||||
## Context and System Prompt Files
|
||||
|
||||
Pi loads `AGENTS.md` or `CLAUDE.md` from `~/.pi/agent/AGENTS.md`, parent directories walking up from cwd, and the current directory. Disable with `--no-context-files` / `-nc`.
|
||||
Pi loads `AGENTS.md` or `CLAUDE.md` from `~/.pi/agent/AGENTS.md`, parent directories walking up from cwd, and the current directory. A directory containing `AGENTS.override.md` contributes that file instead of its `AGENTS.md`/`CLAUDE.md`; other directories still layer normally. Disable with `--no-context-files` / `-nc`.
|
||||
|
||||
Replace the default system prompt with `.pi/SYSTEM.md` (project) or `~/.pi/agent/SYSTEM.md` (global). Append instead of replacing with `APPEND_SYSTEM.md` in either location.
|
||||
|
||||
@@ -102,7 +102,11 @@ Tools: `-t/--tools`, `-xt/--exclude-tools`, `-nbt/--no-builtin-tools` (keeps ext
|
||||
|
||||
Resources: `-e/--extension <source>` (path, npm, or git; repeatable), `--no-extensions`, `--skill`, `--no-skills`, `--prompt-template`, `--no-prompt-templates`, `--theme`, `--no-themes`, `-nc/--no-context-files`. Combine `--no-*` with explicit flags to load exactly what you need: `pi --no-extensions -e ./my-extension.ts`.
|
||||
|
||||
Other: `--system-prompt <text>` (context files and skills are still appended), `--append-system-prompt`, `--verbose`, `-a/--approve`, `-na/--no-approve`, `-h/--help`, `-v/--version`.
|
||||
Other: `--system-prompt <text>` (context files and skills are still appended), `--append-system-prompt`, `--tui-mode <regular|fullscreen>`, `--use-theme <name[/name]>` (initial theme for this run only), `--verbose`, `-a/--approve`, `-na/--no-approve`, `-h/--help`, `-v/--version`.
|
||||
|
||||
## Fullscreen TUI Mode
|
||||
|
||||
`--tui-mode fullscreen` (experimental) scrolls the transcript inside the terminal viewport while queued messages, working status, extension widgets, editor, and footer stay pinned at the bottom. Mouse/trackpad input scrolls the region under the pointer, and keyboard viewport actions (`tui.altScreen.*` in `references/keybindings.md`) stay available. Inline images work in terminals supporting the Kitty graphics protocol (Kitty, Ghostty); iTerm2 renders text placeholders instead. `regular` mode uses the main screen and terminal-owned scrollback. Switch at runtime and set the default in `/settings`; `fullscreenExitOutput` controls whether exiting prints the final transcript or only the resume hint.
|
||||
|
||||
File arguments: prefix with `@` to include in the message (`pi @code.ts @test.ts "Review these"`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user