After reading this document, you should NOT need to consult the legacy TypeScript code.
This document is the authoritative specification for the Rust port of pi-mono.
- Project Overview
- Message Types and Content Blocks
- Streaming Events
- Provider Interface
- Tool System
- Session File Format
- Configuration
- Authentication Storage
- CLI Commands and Flags
- Execution Flow
- RPC Mode Protocol
- Extensions API
- Resources System
Pi is an AI coding agent platform with these core components:
| Component | TypeScript Package | Rust Equivalent |
|---|---|---|
| LLM Provider Abstraction | @mariozechner/pi-ai |
pi::provider module |
| Agent Runtime | @mariozechner/pi-agent |
pi::agent module |
| CLI Application | @mariozechner/pi-coding-agent |
pi binary |
| Terminal UI | @mariozechner/pi-tui |
pi::tui module |
- Core legacy tool set: read, bash, edit, write, grep, find, ls
- 20+ LLM providers: Anthropic, OpenAI, Google, Bedrock, etc.
- Session format version: 3
- Default tools enabled: read, bash, edit, write
pub enum Message {
User(UserMessage),
Assistant(AssistantMessage),
ToolResult(ToolResultMessage),
}pub struct UserMessage {
pub content: UserContent, // String or Vec<ContentBlock>
pub timestamp: i64, // Unix milliseconds
}
pub enum UserContent {
Text(String),
Blocks(Vec<ContentBlock>), // TextContent | ImageContent only
}pub struct AssistantMessage {
pub content: Vec<AssistantContentBlock>, // Text | Thinking | ToolCall
pub api: String, // e.g., "anthropic-messages"
pub provider: String, // e.g., "anthropic"
pub model: String, // Model ID
pub usage: Usage,
pub stop_reason: StopReason,
pub error_message: Option<String>,
pub timestamp: i64,
}pub struct ToolResultMessage {
pub tool_call_id: String,
pub tool_name: String,
pub content: Vec<ContentBlock>, // TextContent | ImageContent
pub details: Option<serde_json::Value>,
pub is_error: bool,
pub timestamp: i64,
}pub enum StopReason {
Stop, // Normal completion
Length, // Max tokens reached
ToolUse, // Tool call pending
Error, // Error occurred
Aborted, // User cancelled
}pub enum ContentBlock {
Text(TextContent),
Thinking(ThinkingContent),
Image(ImageContent),
ToolCall(ToolCall),
}
pub struct TextContent {
pub text: String,
pub text_signature: Option<String>, // Provider-specific
}
pub struct ThinkingContent {
pub thinking: String,
pub thinking_signature: Option<String>, // For replay
}
pub struct ImageContent {
pub data: String, // Base64 encoded
pub mime_type: String, // "image/jpeg", "image/png", etc.
}
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
pub thought_signature: Option<String>, // Google-specific
}pub struct Usage {
pub input: u64, // Input tokens (excluding cache read)
pub output: u64, // Output tokens
pub cache_read: u64, // Tokens read from cache
pub cache_write: u64, // Tokens written to cache
pub total_tokens: u64,
pub cost: Cost,
}
pub struct Cost {
pub input: f64, // Dollars
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
pub total: f64,
}pub enum StreamEvent {
Start { partial: AssistantMessage },
TextStart { content_index: usize, partial: AssistantMessage },
TextDelta { content_index: usize, delta: String, partial: AssistantMessage },
TextEnd { content_index: usize, content: String, partial: AssistantMessage },
ThinkingStart { content_index: usize, partial: AssistantMessage },
ThinkingDelta { content_index: usize, delta: String, partial: AssistantMessage },
ThinkingEnd { content_index: usize, content: String, partial: AssistantMessage },
ToolCallStart { content_index: usize, partial: AssistantMessage },
ToolCallDelta { content_index: usize, delta: String, partial: AssistantMessage },
ToolCallEnd { content_index: usize, tool_call: ToolCall, partial: AssistantMessage },
Done { reason: StopReason, message: AssistantMessage },
Error { reason: StopReason, error: AssistantMessage },
}Text response:
Start → TextStart → TextDelta* → TextEnd → Done(Stop)
Tool call:
Start → ToolCallStart → ToolCallDelta* → ToolCallEnd → Done(ToolUse)
With thinking:
Start → ThinkingStart → ThinkingDelta* → ThinkingEnd → TextStart → ... → Done
#[async_trait]
pub trait Provider: Send + Sync {
fn name(&self) -> &str;
fn api(&self) -> &str;
async fn stream(
&self,
context: &Context,
options: &StreamOptions,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>>;
}pub struct Context {
pub system_prompt: Option<String>,
pub messages: Vec<Message>,
pub tools: Vec<ToolDef>,
}pub struct StreamOptions {
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
pub api_key: Option<String>,
pub cache_retention: CacheRetention,
pub session_id: Option<String>,
pub headers: HashMap<String, String>,
pub thinking_level: Option<ThinkingLevel>,
pub thinking_budgets: Option<ThinkingBudgets>,
}
pub enum CacheRetention {
None,
Short,
Long, // 1 hour TTL on Anthropic
}
pub enum ThinkingLevel {
Off,
Minimal, // 1024 tokens
Low, // 2048 tokens
Medium, // 8192 tokens
High, // 16384 tokens
XHigh, // Model max
}
pub struct ThinkingBudgets {
pub minimal: u32, // Default: 1024
pub low: u32, // Default: 2048
pub medium: u32, // Default: 8192
pub high: u32, // Default: 16384
}pub struct Model {
pub id: String,
pub name: String,
pub api: String, // "anthropic-messages", "openai-completions", etc.
pub provider: String, // "anthropic", "openai", etc.
pub base_url: String,
pub reasoning: bool, // Supports thinking/reasoning
pub input: Vec<InputType>, // ["text", "image"]
pub cost: ModelCost,
pub context_window: u32,
pub max_tokens: u32,
pub headers: HashMap<String, String>,
}
pub struct ModelCost {
pub input: f64, // $/million tokens
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
}
pub enum InputType {
Text,
Image,
}pub enum Api {
AnthropicMessages,
OpenAICompletions,
OpenAIResponses,
AzureOpenAIResponses,
BedrockConverseStream,
GoogleGenerativeAI,
GoogleGeminiCli,
GoogleVertex,
Custom(String),
}pub enum KnownProvider {
Anthropic,
OpenAI,
Google,
GoogleVertex,
AmazonBedrock,
AzureOpenAI,
GithubCopilot,
XAI,
Groq,
Cerebras,
OpenRouter,
Mistral,
// ... more
}#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn label(&self) -> &str;
fn description(&self) -> &str;
fn parameters(&self) -> serde_json::Value; // JSON Schema
async fn execute(
&self,
tool_call_id: &str,
input: serde_json::Value,
on_update: Option<Box<dyn Fn(ToolUpdate) + Send>>,
) -> Result<ToolOutput>;
}
pub struct ToolOutput {
pub content: Vec<ContentBlock>,
pub details: Option<serde_json::Value>,
}
pub struct ToolUpdate {
pub content: Vec<ContentBlock>,
pub details: Option<serde_json::Value>,
}pub struct ToolDef {
pub name: String,
pub description: String,
pub parameters: serde_json::Value, // JSON Schema
}Purpose: Read file contents (text or images)
Parameters:
{
"type": "object",
"properties": {
"path": { "type": "string", "description": "File path (relative or absolute)" },
"offset": { "type": "integer", "description": "1-indexed line to start from" },
"limit": { "type": "integer", "description": "Max lines to read" }
},
"required": ["path"]
}Behavior:
- Truncation: 2000 lines OR 50KB (whichever first)
- Truncation method:
truncate_head(keeps beginning) - Image support: jpg, png, gif, webp → base64 with optional resize to 2000x2000
- Supports
~expansion in paths
Error conditions:
- Path not found
- Permission denied
- Offset beyond EOF
Purpose: Execute shell commands
Parameters:
{
"type": "object",
"properties": {
"command": { "type": "string", "description": "Bash command to execute" },
"timeout": { "type": "integer", "description": "Timeout in seconds" }
},
"required": ["command"]
}Behavior:
- Streams output via
on_updatecallback (last 100KB rolling buffer) - Truncation: 2000 lines OR 50KB (whichever first)
- Truncation method:
truncate_tail(keeps end, shows errors) - Creates temp file if output > 50KB (path in
details.full_output_path) - Inherits shell environment
- Optional command prefix (e.g., "shopt -s expand_aliases")
Error conditions:
- Non-zero exit code → Error with output + "Command exited with code X"
- Timeout → Error with output + "Command timed out after X seconds"
- Abort → Error "Command aborted"
- Kills entire process tree on timeout/abort
Purpose: Create or overwrite files
Parameters:
{
"type": "object",
"properties": {
"path": { "type": "string", "description": "File path" },
"content": { "type": "string", "description": "Content to write" }
},
"required": ["path", "content"]
}Behavior:
- Creates parent directories recursively
- UTF-8 encoding
- Output: "Successfully wrote X bytes to path"
Purpose: Replace exact text in files
Parameters:
{
"type": "object",
"properties": {
"path": { "type": "string", "description": "File path" },
"oldText": { "type": "string", "description": "Exact text to find" },
"newText": { "type": "string", "description": "Replacement text" }
},
"required": ["path", "oldText", "newText"]
}Matching algorithm:
- Try exact match
- Fall back to fuzzy match:
- Strip trailing whitespace per line
- Normalize smart quotes (U+2018-U+201F → ' or ")
- Normalize dashes (U+2010-U+2015, U+2212 → -)
- Normalize special spaces (U+00A0, U+2002-U+200A, etc. → space)
Validation:
- Must find
oldTextexactly once - Must actually change content (old != new)
Line ending handling:
- Detects original line ending (CRLF vs LF)
- Normalizes to LF internally
- Restores original ending in output
BOM handling:
- Strips UTF-8 BOM before matching
- Restores BOM in output
Output includes:
- Success message
- Unified diff with line numbers
- First changed line number
Purpose: Search file contents (via ripgrep)
Parameters:
{
"type": "object",
"properties": {
"pattern": { "type": "string", "description": "Regex or literal pattern" },
"path": { "type": "string", "description": "Directory or file", "default": "." },
"glob": { "type": "string", "description": "Glob filter (e.g., *.ts)" },
"ignoreCase": { "type": "boolean", "default": false },
"literal": { "type": "boolean", "default": false },
"context": { "type": "integer", "default": 0 },
"limit": { "type": "integer", "default": 100 }
},
"required": ["pattern"]
}Behavior:
- Uses ripgrep (
rg --json) - Respects
.gitignore - Searches dotfiles (
--hidden) - Individual lines truncated to 500 chars
Limits:
- Match limit: 100 (default)
- Byte limit: 50KB
- Line length: 500 chars
Output format:
path/file.ts:42: matching line content
path/file.ts-41- context line
Purpose: Find files by glob pattern (via fd)
Parameters:
{
"type": "object",
"properties": {
"pattern": { "type": "string", "description": "Glob pattern (e.g., *.ts)" },
"path": { "type": "string", "description": "Directory", "default": "." },
"limit": { "type": "integer", "default": 1000 }
},
"required": ["pattern"]
}Behavior:
- Uses fd (
fd --glob) - Respects
.gitignore - Searches dotfiles (
--hidden) - Returns relative paths
- Directories marked with trailing
/
Limits:
- Result limit: 1000 (default)
- Byte limit: 50KB
Purpose: List directory contents
Parameters:
{
"type": "object",
"properties": {
"path": { "type": "string", "description": "Directory", "default": "." },
"limit": { "type": "integer", "default": 500 }
}
}Behavior:
- Sorted alphabetically (case-insensitive)
- Directories marked with trailing
/ - Includes dotfiles
- Skips entries that fail stat
Limits:
- Entry limit: 500 (default)
- Byte limit: 50KB
pub const DEFAULT_MAX_LINES: usize = 2000;
pub const DEFAULT_MAX_BYTES: usize = 50 * 1024; // 50KB
pub const GREP_MAX_LINE_LENGTH: usize = 500;~/.pi/agent/sessions/
└── --{encoded-cwd}--/
└── {timestamp}_{session-id}.jsonl
CWD encoding: --${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--
- Example:
/home/user/project→--home-user-project--
Timestamp format: YYYY-MM-DDTHH-mm-ss.sssZ (colons replaced with hyphens)
Current version: 3
pub struct SessionHeader {
pub r#type: String, // "session"
pub version: Option<u8>, // Usually 3
pub id: String, // UUID
pub timestamp: String, // ISO-8601
pub cwd: String, // Absolute path
pub provider: Option<String>, // Provider name (optional)
pub model_id: Option<String>, // Model ID (optional)
pub thinking_level: Option<String>, // "off"|"minimal"|... (optional)
pub parent_session: Option<String>, // Parent session path (serialized as "branchedFrom"; accepts legacy "parentSession")
}Serialization:
{
"type": "session",
"version": 3,
"id": "uuid-string",
"timestamp": "2024-01-15T10:30:45.123Z",
"cwd": "/absolute/path/to/dir",
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"thinkingLevel": "medium",
"branchedFrom": "/path/to/parent.jsonl"
}All entries have base fields:
pub struct EntryBase {
pub id: Option<String>, // 8-char hex or UUID (may be missing on disk)
pub parent_id: Option<String>,
pub timestamp: String, // ISO-8601
}pub struct MessageEntry {
#[serde(flatten)]
pub base: EntryBase, // type: "message"
pub message: SessionMessage,
}
pub enum SessionMessage {
User { content: UserContent, timestamp: Option<i64> },
Assistant { /* full AssistantMessage fields */ },
ToolResult { tool_use_id: String, content: Vec<ContentBlock>, timestamp: Option<i64> },
Custom { custom_type: String, content: String, display: bool, details: Option<Value> },
BashExecution { command: String, output: String, exit_code: i32, /* ... */ },
BranchSummary { summary: String, from_id: String },
CompactionSummary { summary: String, tokens_before: u64 },
}pub struct ModelChangeEntry {
#[serde(flatten)]
pub base: EntryBase, // type: "model_change"
pub provider: String,
pub model_id: String,
}pub struct ThinkingLevelChangeEntry {
#[serde(flatten)]
pub base: EntryBase, // type: "thinking_level_change"
pub thinking_level: String, // "off"|"minimal"|"low"|"medium"|"high"|"xhigh"
}pub struct CompactionEntry {
#[serde(flatten)]
pub base: EntryBase, // type: "compaction"
pub summary: String,
pub first_kept_entry_id: String,
pub tokens_before: u64,
pub details: Option<Value>,
pub from_hook: Option<bool>,
}pub struct BranchSummaryEntry {
#[serde(flatten)]
pub base: EntryBase, // type: "branch_summary"
pub from_id: String, // "root" or entry ID
pub summary: String,
pub details: Option<Value>,
pub from_hook: Option<bool>,
}pub struct LabelEntry {
#[serde(flatten)]
pub base: EntryBase, // type: "label"
pub target_id: String,
pub label: Option<String>, // None = delete label
}pub struct SessionInfoEntry {
#[serde(flatten)]
pub base: EntryBase, // type: "session_info"
pub name: Option<String>,
}- Each entry has
idandparent_id - Forms a linked tree enabling branching
- Leaf pointer tracks current position
- Moving leaf pointer enables branching without modifying history
- Format: 8-character hexadecimal (from UUID slice)
- Collision checking: 100 retries before full UUID
- Uniqueness scope: Per-session only
| Type | Path |
|---|---|
| Global settings | ~/.pi/agent/settings.json |
| Project settings | ./.pi/settings.json |
| Auth | ~/.pi/agent/auth.json |
| Models | ~/.pi/agent/models.json |
| Sessions | ~/.pi/agent/sessions/ |
pub struct Settings {
// Appearance
pub theme: Option<String>,
pub hide_thinking_block: Option<bool>, // Default: false
pub show_hardware_cursor: Option<bool>, // Default: false
// Model Configuration
pub default_provider: Option<String>,
pub default_model: Option<String>,
pub default_thinking_level: Option<String>,
pub enabled_models: Option<Vec<String>>, // Patterns for Ctrl+P cycling
// Message Handling
pub steering_mode: Option<String>, // "all" | "one-at-a-time"
pub follow_up_mode: Option<String>, // "all" | "one-at-a-time"
// Terminal Behavior
pub quiet_startup: Option<bool>, // Default: false
pub collapse_changelog: Option<bool>, // Default: false
pub double_escape_action: Option<String>, // "fork" | "tree" | "none"
pub editor_padding_x: Option<u32>, // Default: 0
pub autocomplete_max_visible: Option<u32>,// Default: 5
// Compaction
pub compaction: Option<CompactionSettings>,
// Branch Summarization
pub branch_summary: Option<BranchSummarySettings>,
// Retry Configuration
pub retry: Option<RetrySettings>,
// Shell
pub shell_path: Option<String>,
pub shell_command_prefix: Option<String>,
// Images
pub images: Option<ImageSettings>,
// Terminal Display
pub terminal: Option<TerminalSettings>,
// Thinking Budgets
pub thinking_budgets: Option<ThinkingBudgets>,
// Extensions/Skills/etc.
pub packages: Option<Vec<PackageSource>>,
pub extensions: Option<Vec<String>>,
pub skills: Option<Vec<String>>,
pub prompts: Option<Vec<String>>,
pub themes: Option<Vec<String>>,
pub enable_skill_commands: Option<bool>, // Default: true
}
pub struct CompactionSettings {
pub enabled: Option<bool>, // Default: true
pub reserve_tokens: Option<u32>, // Default: 16384
pub keep_recent_tokens: Option<u32>, // Default: 20000
}
pub struct RetrySettings {
pub enabled: Option<bool>, // Default: true
pub max_retries: Option<u32>, // Default: 3
pub base_delay_ms: Option<u32>, // Default: 2000
pub max_delay_ms: Option<u32>, // Default: 60000
}
pub struct ImageSettings {
pub auto_resize: Option<bool>, // Default: true (2000x2000 max)
pub block_images: Option<bool>, // Default: false
}
pub struct TerminalSettings {
pub show_images: Option<bool>, // Default: true (if supported)
pub clear_on_shrink: Option<bool>, // Default: false
}- CLI flags (highest)
- Environment variables
- Project settings (
./.pi/settings.json) - Global settings (
~/.pi/agent/settings.json) - Built-in defaults (lowest)
// Config paths
PI_CODING_AGENT_DIR // Override ~/.pi/agent
PI_PACKAGE_DIR // Override package assets
// API Keys (per provider)
ANTHROPIC_API_KEY
OPENAI_API_KEY
GOOGLE_API_KEY
GOOGLE_CLOUD_API_KEY
AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
XAI_API_KEY
GROQ_API_KEY
CEREBRAS_API_KEY
OPENROUTER_API_KEY
MISTRAL_API_KEY
// ... etc.- Path:
~/.pi/agent/auth.json - Permissions:
0o600(read-write owner only) - Locking: File lock with 30-second stale timeout
pub enum AuthCredential {
ApiKey { key: String },
OAuth {
access_token: String,
refresh_token: String,
expires: i64, // Unix milliseconds
},
}{
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
"github-copilot": {
"type": "oauth",
"access_token": "...",
"refresh_token": "...",
"expires": 1234567890000
}
}- Runtime override (
--api-keyflag) - API key from
auth.json(type: "api_key") - OAuth token from
auth.json(auto-refresh if expired) - Environment variable (provider-specific)
- Fallback resolver (custom providers)
This port supports Anthropic OAuth as an alternative to API keys. Credentials are stored in auth.json under the provider key "anthropic" with type: "oauth".
The login flow uses PKCE (RFC 7636):
verifier: 32 random bytes, Base64URL (no padding)challenge: Base64URL(SHA256(verifier))
client_id:9d1c250a-e61b-44d9-88ed-5944d1962f5eauthorize_url:https://claude.ai/oauth/authorizeredirect_uri:https://console.anthropic.com/oauth/code/callbackscopes:org:create_api_key user:profile user:inference
Query params:
code=trueclient_id=<client_id>response_type=coderedirect_uri=<redirect_uri>scope=<scopes>code_challenge=<challenge>code_challenge_method=S256state=<verifier>
The user completes login in the browser and then pastes either:
- the full callback URL, or
code#state, or- just
code(in which casestatedefaults to the originalverifier)
POST https://console.anthropic.com/v1/oauth/token with JSON:
{
"grant_type": "authorization_code",
"client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
"code": "<code>",
"state": "<state>",
"redirect_uri": "https://console.anthropic.com/oauth/code/callback",
"code_verifier": "<verifier>"
}Response JSON:
{ "access_token": "...", "refresh_token": "...", "expires_in": 1234 }Expiry is stored in milliseconds as:
expires = now_ms + (expires_in * 1000) - (5 * 60 * 1000)
If an OAuth credential is expired, it must be refreshed automatically.
POST https://console.anthropic.com/v1/oauth/token with JSON:
{
"grant_type": "refresh_token",
"client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
"refresh_token": "<refresh_token>"
}The refreshed credentials overwrite the stored entry in auth.json and are persisted immediately.
| Command | Syntax | Description |
|---|---|---|
install |
pi install <source> [-l|--local] |
Install extension/skill/prompt/theme |
remove |
pi remove <source> [-l|--local] |
Remove from settings |
update |
pi update [source] |
Update all or specific source |
list |
pi list |
List global + project packages |
config |
pi config |
Open TUI config selector |
| Flag | Aliases | Type | Default |
|---|---|---|---|
--help |
-h |
bool | false |
--version |
-v |
bool | false |
| Flag | Type | Default | Description |
|---|---|---|---|
--provider |
string | "google" | Provider name |
--model |
string | "gemini-2.5-flash" | Model ID |
--api-key |
string | None | API key override |
--models |
string | None | Comma-separated model patterns for cycling |
| Flag | Type | Values | Default |
|---|---|---|---|
--thinking |
enum | off, minimal, low, medium, high, xhigh | None |
| Flag | Type | Description |
|---|---|---|
--system-prompt |
string | Override system prompt |
--append-system-prompt |
string | Append to system prompt |
| Flag | Aliases | Type | Description |
|---|---|---|---|
--continue |
-c |
bool | Continue previous session |
--resume |
-r |
bool | Select session from picker |
--session |
string | Specific session file path | |
--session-dir |
string | Session storage directory | |
--no-session |
bool | Ephemeral session |
| Flag | Aliases | Type | Values |
|---|---|---|---|
--mode |
enum | text, json, rpc | |
--print |
-p |
bool | Non-interactive mode |
--verbose |
bool | Verbose startup |
| Flag | Type | Default |
|---|---|---|
--no-tools |
bool | Disable all tools |
--tools |
string | "read,bash,edit,write" |
| Flag | Aliases | Type |
|---|---|---|
--extension |
-e |
string (repeatable) |
--no-extensions |
bool |
| Flag | Type |
|---|---|
--skill |
string (repeatable) |
--no-skills |
bool |
| Flag | Type |
|---|---|
--prompt-template |
string (repeatable) |
--no-prompt-templates |
bool |
| Flag | Type |
|---|---|
--theme |
string (repeatable) |
--no-themes |
bool |
| Flag | Type | Description |
|---|---|---|
--export |
string | Export session to HTML |
--list-models |
bool/string | List available models |
- File arguments: Prefixed with
@(e.g.,@file.md) - Message arguments: Any non-flag positional argument
pi [options] [@files...] [messages...]
1. Check for package commands (install/remove/update/list/config)
└─ If matched: execute & exit(0)
2. Run migrations
3. First pass: parse extension/skill/prompt/theme flags
└─ Load resources
4. Second pass: parse with extension-registered flags
5. Early exits:
├─ --version → print version & exit(0)
├─ --help → print help & exit(0)
├─ --list-models → list models & exit(0)
└─ --export → export & exit(0/1)
6. Handle stdin (if not TTY and not RPC mode)
└─ Prepend stdin to messages, force print=true
7. Prepare initial message from @files and message args
8. Determine mode:
└─ isInteractive = !print && mode == undefined
9. Create session manager:
├─ --no-session → in-memory
├─ --session → open/fork specific file
├─ --continue → continue most recent
├─ --resume → show picker
└─ default → create new
10. Resolve model scope from --models or settings
11. Build session options
12. Apply --api-key override
13. Create agent session
└─ exit(1) if no model and not interactive
14. Clamp thinking level to model capabilities
15. Run mode:
├─ RPC → runRpcMode() [continues indefinitely]
├─ Interactive → InteractiveMode.run() [continues indefinitely]
└─ Print → runPrintMode() → exit(0)
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (invalid args, file not found, API error, etc.) |
pi --mode rpc [options]Common options:
--provider <name>--model <id>--no-session--session-dir <path>
- Commands: JSON objects sent to stdin, one per line.
- Responses: JSON objects with
type: "response"indicating success/failure. - Events: JSON objects streamed to stdout as JSON lines.
- All commands support optional
idfor correlation; responses echoid.
prompt
- Request:
{"id":"req-1","type":"prompt","message":"Hello"} - Optional
images: array ofImageContentobjects (type:"image",source={type:"base64", mediaType, data}). - Optional
streamingBehavior:"steer"or"followUp".- If agent is streaming and
streamingBehavioris absent → error. "steer": interrupt after current tool execution; remaining tool calls skipped."followUp": queue after agent finishes.
- If agent is streaming and
- Extension commands (
/command) execute immediately even during streaming. - Skill commands (
/skill:name) and prompt templates (/template) expand before queueing. - Response:
{"type":"response","command":"prompt","success":true}
steer
- Request:
{"type":"steer","message":"Stop and do this instead"} - Same expansion rules; extension commands not allowed (use
prompt). - Response:
{"type":"response","command":"steer","success":true}
follow_up
- Request:
{"type":"follow_up","message":"After you're done, also do this"} - Same expansion rules; extension commands not allowed.
- Response:
{"type":"response","command":"follow_up","success":true}
abort
- Request:
{"type":"abort"} - Response:
{"type":"response","command":"abort","success":true}
new_session
- Request:
{"type":"new_session"}or{"type":"new_session","parentSession":"/path/to/parent.jsonl"} - Can be cancelled by
session_before_switchextension handler. - Response:
{"type":"response","command":"new_session","success":true,"data":{"cancelled":false}}
get_state
- Request:
{"type":"get_state"} - Response data:
model: fullModelornullthinkingLevel:"off"|"minimal"|"low"|"medium"|"high"|"xhigh"isStreaming: boolisCompacting: boolsteeringMode:"all"|"one-at-a-time"followUpMode:"all"|"one-at-a-time"sessionFile: string (omitted for in-memory sessions)sessionId: stringsessionName: string (omitted if not set)autoCompactionEnabled: boolmessageCount: numberpendingMessageCount: number
get_messages
- Request:
{"type":"get_messages"} - Response:
{"type":"response","command":"get_messages","success":true,"data":{"messages":[...]}} - Messages are
AgentMessageobjects (User/Assistant/ToolResult/BashExecution).
set_model
- Request:
{"type":"set_model","provider":"anthropic","modelId":"claude-sonnet-4-20250514"} - Response:
{"type":"response","command":"set_model","success":true,"data":<Model>}
cycle_model
- Request:
{"type":"cycle_model"} - Response:
{"type":"response","command":"cycle_model","success":true,"data":{"model":<Model>,"thinkingLevel":"medium","isScoped":false}} - Returns
data: nullif only one model available.
get_available_models
- Request:
{"type":"get_available_models"} - Response:
{"type":"response","command":"get_available_models","success":true,"data":{"models":[<Model>,...]}}
set_thinking_level
- Request:
{"type":"set_thinking_level","level":"high"} - Levels:
"off"|"minimal"|"low"|"medium"|"high"|"xhigh" - Response: success true
cycle_thinking_level
- Request:
{"type":"cycle_thinking_level"} - Response:
{"type":"response","command":"cycle_thinking_level","success":true,"data":{"level":"high"}} - Returns
data: nullif model does not support thinking.
set_steering_mode
- Request:
{"type":"set_steering_mode","mode":"one-at-a-time"} - Modes:
"all"or"one-at-a-time"(default) - Response: success true
set_follow_up_mode
- Request:
{"type":"set_follow_up_mode","mode":"one-at-a-time"} - Modes:
"all"or"one-at-a-time"(default) - Response: success true
compact
- Request:
{"type":"compact"}or{"type":"compact","customInstructions":"Focus on code changes"} - Response data:
{summary, firstKeptEntryId, tokensBefore, details}
set_auto_compaction
- Request:
{"type":"set_auto_compaction","enabled":true} - Response: success true
set_auto_retry
- Request:
{"type":"set_auto_retry","enabled":true} - Response: success true
abort_retry
- Request:
{"type":"abort_retry"} - Response: success true
bash
- Request:
{"type":"bash","command":"ls -la"} - Response data:
output: stringexitCode: numbercancelled: booltruncated: boolfullOutputPath: string | null (only when truncated)
abort_bash
- Request:
{"type":"abort_bash"} - Response: success true
get_session_stats
- Request:
{"type":"get_session_stats"} - Response data:
sessionFile,sessionIduserMessages,assistantMessages,toolCalls,toolResults,totalMessagestokens:{input, output, cacheRead, cacheWrite, total}cost: number (total $)
export_html
- Request:
{"type":"export_html"}or{"type":"export_html","outputPath":"/tmp/session.html"} - Response data:
{path: "<output path>"}
switch_session
- Request:
{"type":"switch_session","sessionPath":"/path/to/session.jsonl"} - Can be cancelled by
session_before_switchextension handler. - Response data:
{cancelled: false}
fork
- Request:
{"type":"fork","entryId":"abc123"} entryIdmust be a user message entry.- Creates a new session (branched from parent of selected entry).
- Response data:
{text:"<user message text>", cancelled:false} - Can be cancelled by
session_before_forkextension handler.
get_fork_messages
- Request:
{"type":"get_fork_messages"} - Response data:
{messages:[{entryId, text}, ...]}
get_last_assistant_text
- Request:
{"type":"get_last_assistant_text"} - Response data:
{text: "<assistant text>"}or{text: null}
set_session_name
- Request:
{"type":"set_session_name","name":"my-feature-work"} - Response: success true
get_commands
- Request:
{"type":"get_commands"} - Response data:
{"commands":[{name, description?, source, location?, path?}, ...]}source:"extension"|"template"|"skill"location:"user"|"project"|"path"(not present for extensions)
| Event | Fields |
|---|---|
agent_start |
{type:"agent_start"} |
agent_end |
{type:"agent_end", messages:[AgentMessage], error?} |
turn_start |
{type:"turn_start"} |
turn_end |
{type:"turn_end", message:AgentMessage, toolResults:[AgentMessage]} |
message_start |
{type:"message_start", message:AgentMessage} |
message_update |
{type:"message_update", message:AgentMessage, assistantMessageEvent:<delta>} |
message_end |
{type:"message_end", message:AgentMessage} |
tool_execution_start |
{type:"tool_execution_start", toolCallId, toolName, args} |
tool_execution_update |
{type:"tool_execution_update", toolCallId, toolName, args, partialResult} |
tool_execution_end |
{type:"tool_execution_end", toolCallId, toolName, result, isError} |
auto_compaction_start |
`{type:"auto_compaction_start", reason:"threshold" |
auto_compaction_end |
{type:"auto_compaction_end", result, aborted, willRetry, errorMessage?} |
auto_retry_start |
{type:"auto_retry_start", attempt, maxAttempts, delayMs, errorMessage} |
auto_retry_end |
{type:"auto_retry_end", success, attempt, finalError?} |
extension_error |
{type:"extension_error", extensionPath, event, error} |
assistantMessageEvent delta types (streaming):
starttext_start,text_delta,text_endthinking_start,thinking_delta,thinking_endtoolcall_start,toolcall_delta,toolcall_end(includes fulltoolCall)done(reason:"stop"|"length"|"toolUse")error(reason:"aborted"|"error")
extension_ui_request (stdout):
- Base:
{type:"extension_ui_request", id, method, ...} - Dialog methods block until response or timeout:
select:{title, options:[{label,value}], placeholder?, default?, timeout?}confirm:{title, message, default?, timeout?}input:{title, placeholder?, default?, password?, timeout?}editor:{title, language?, default?, readOnly?, timeout?}
- Fire-and-forget methods (no response expected):
notify:{title, message, level?}setStatus:{text}setWidget:{content}setTitle:{title}set_editor_text:{text}
- RPC limitations:
custom()returnsundefined;setWorkingMessage,setFooter,setHeader,setEditorComponentare no-ops;getEditorText()returns"".
extension_ui_response (stdin):
- Base:
{type:"extension_ui_response", id, value?, cancelled?} - Dialog responses:
- select/input/editor:
{value: <selected/entered>} - confirm:
{value: true|false} - cancellation:
{cancelled: true}
- select/input/editor:
Model
id,name,api,provider,baseUrlreasoning(bool)input:["text","image"]contextWindow,maxTokenscost:{input, output, cacheRead, cacheWrite}
UserMessage
{role:"user", content, timestamp, attachments:[]}contentcan be string or array ofTextContent/ImageContent.
AssistantMessage
{role:"assistant", content:[...], api, provider, model, usage, stopReason, timestamp}usage:{input, output, cacheRead, cacheWrite, cost:{input, output, cacheRead, cacheWrite, total}}stopReason:"stop"|"length"|"toolUse"|"error"|"aborted"
ToolResultMessage
{role:"toolResult", toolCallId, toolName, content, isError, timestamp}
BashExecutionMessage
{role:"bashExecution", command, output, exitCode, cancelled, truncated, fullOutputPath, timestamp}
Attachment
{id, type:"image", fileName, mimeType, size, content, extractedText, preview}
Extensions are Node.js modules that can register tools, commands, and event handlers. The Rust port implements a PiJS runtime (see EXTENSIONS.md for connector architecture).
// Extension module exports activate function
export async function activate(ctx: ExtensionContext): Promise<void>;pub struct ExtensionContext {
// Registration
fn register_tool(name: &str, config: ToolConfig, handler: ToolHandler);
fn register_command(name: &str, config: CommandConfig, handler: CommandHandler);
// Event handlers
fn on_agent_start(handler: Fn(AgentStartEvent));
fn on_agent_end(handler: Fn(AgentEndEvent));
fn on_turn_start(handler: Fn(TurnStartEvent));
fn on_turn_end(handler: Fn(TurnEndEvent));
fn on_tool_execution_start(handler: Fn(ToolExecutionStartEvent));
fn on_tool_execution_end(handler: Fn(ToolExecutionEndEvent));
fn on_session_before_switch(handler: Fn(SessionEvent) -> bool); // Return false to cancel
fn on_session_before_fork(handler: Fn(SessionEvent) -> bool); // Return false to cancel
fn on_startup(handler: Fn(StartupEvent));
// UI access
ui: ExtensionUi,
// Session access
session: SessionAccess,
// Logging
log: Logger,
}interface ToolConfig {
label: string; // Display name
description: string; // For LLM context
parameters: JsonSchema; // JSON Schema for validation
}
type ToolHandler = (args: ToolArgs, update?: UpdateCallback) => Promise<ToolResult>;
interface ToolArgs {
toolCallId: string;
input: Record<string, unknown>;
}
interface ToolResult {
content: ContentBlock[];
details?: Record<string, unknown>;
isError?: boolean;
}
type UpdateCallback = (partial: ToolResult) => void;interface CommandConfig {
description?: string; // Help text
}
type CommandHandler = (args: string) => Promise<string | void>;Commands are invoked via /command args syntax. If handler returns a string, it's used as the user message.
interface ExtensionUi {
// Dialogs (blocking, support cancellation)
select(options: SelectOptions): Promise<string | undefined>;
confirm(options: ConfirmOptions): Promise<boolean>;
input(options: InputOptions): Promise<string | undefined>;
editor(options: EditorOptions): Promise<string | undefined>;
// Non-blocking updates
notify(options: NotifyOptions): void;
setStatus(text: string): void;
setWidget(content: string): void;
setTitle(title: string): void;
// Editor interaction
setEditorText(text: string): void;
getEditorText(): string; // Returns "" in RPC mode
// Custom component (interactive TUI only)
custom<T>(component: Component): Promise<T | undefined>; // Returns undefined in RPC
}
interface SelectOptions {
title: string;
options: Array<{label: string; value: string}>;
placeholder?: string;
default?: string;
timeout?: number; // ms, undefined = no timeout
}
interface ConfirmOptions {
title: string;
message: string;
default?: boolean;
timeout?: number;
}
interface InputOptions {
title: string;
placeholder?: string;
default?: string;
password?: boolean;
timeout?: number;
}
interface EditorOptions {
title: string;
language?: string;
default?: string;
readOnly?: boolean;
timeout?: number;
}
interface NotifyOptions {
title: string;
message: string;
level?: "info" | "warning" | "error";
}Cancellation semantics:
- Dialog methods return
undefinedwhen user cancels (Esc key) - Timeout triggers cancellation
- In RPC mode,
extension_ui_responsewithcancelled: trueindicates cancellation confirm()returnsfalseon cancel (notundefined)
interface SessionAccess {
getMessages(): Message[];
getState(): SessionState;
getFile(): string | undefined; // undefined for in-memory sessions
}
interface SessionState {
sessionId: string;
messageCount: number;
isStreaming: boolean;
model: Model | null;
thinkingLevel: ThinkingLevel;
}interface AgentStartEvent {
sessionId: string;
}
interface AgentEndEvent {
sessionId: string;
messages: Message[];
error?: string;
}
interface TurnStartEvent {
sessionId: string;
turnIndex: number;
}
interface TurnEndEvent {
sessionId: string;
turnIndex: number;
message: AssistantMessage;
toolResults: ToolResultMessage[];
}
interface ToolExecutionStartEvent {
toolCallId: string;
toolName: string;
args: Record<string, unknown>;
}
interface ToolExecutionEndEvent {
toolCallId: string;
toolName: string;
result: ToolResult;
isError: boolean;
}
interface SessionEvent {
currentSession: string | undefined;
targetSession?: string; // For switch
forkEntryId?: string; // For fork
}
interface StartupEvent {
version: string;
sessionFile?: string;
}interface Logger {
debug(message: string, data?: Record<string, unknown>): void;
info(message: string, data?: Record<string, unknown>): void;
warn(message: string, data?: Record<string, unknown>): void;
error(message: string, data?: Record<string, unknown>): void;
}Logs are emitted as structured JSON (schema: pi.ext.log.v1).
Resources are discoverable user content: skills, prompt templates, and themes.
Resources are loaded from multiple locations in priority order:
- Explicit CLI flags (
--skill,--prompt-template,--theme) - Settings arrays (
skills,prompts,themesin settings.json) - Installed packages (via
packagessetting)
{
"name": "@scope/package-name",
"pi": {
"extensions": ["./dist/extension.js"],
"skills": ["./skills/"],
"prompts": ["./prompts/"],
"themes": ["./themes/"]
}
}If pi field is absent, defaults apply:
extensions:[]skills:["./skills/"]if directory existsprompts:["./prompts/"]if directory existsthemes:["./themes/"]if directory exists
Skills are markdown files with YAML frontmatter defining agent capabilities.
File locations:
- Global:
~/.pi/agent/skills/*.md - Project:
./.pi/skills/*.md - Package:
<package>/skills/*.md
Frontmatter schema:
---
name: skill-name # Required, kebab-case
description: Brief desc # Required, shown in skill list
allowed_tools: # Optional, restrict to specific tools
- read
- bash
---Skill content: Markdown body becomes the skill prompt.
Expansion: /skill:name args expands to:
<skill name="skill-name" arguments="args">
[skill markdown content]
</skill>System prompt injection: When read tool is enabled, active skills are appended:
<available_skills>
<skill name="name1">description1</skill>
<skill name="name2">description2</skill>
</available_skills>Prompt templates are markdown files for reusable user prompts.
File locations:
- Global:
~/.pi/agent/prompts/*.md - Project:
./.pi/prompts/*.md - Package:
<package>/prompts/*.md
Command format: /template-name arg1 arg2 ...
Variable substitution:
| Variable | Meaning |
|---|---|
$1, $2, ... |
Positional arguments |
$@ |
All arguments joined by space |
$ARGUMENTS |
Same as $@ |
${@:N} |
Arguments from position N onward |
Expansion result: Becomes the user message (after variable substitution).
Themes are JSON files defining terminal color schemes.
File locations:
- Global:
~/.pi/agent/themes/*.json - Project:
./.pi/themes/*.json - Package:
<package>/themes/*.json
Schema:
{
"name": "theme-name",
"colors": {
"text": "#ffffff",
"background": "#000000",
"primary": "#007acc",
"secondary": "#6c757d",
"success": "#28a745",
"warning": "#ffc107",
"error": "#dc3545",
"thinking": "#6c757d",
"tool": "#17a2b8"
}
}Packages can be specified as:
| Source Type | Format | Example |
|---|---|---|
| npm | npm:<package>@<version> |
npm:@pi/tools@1.0.0 |
| git | git:<url>#<ref> |
git:github.com/user/repo#main |
| local | path:<absolute-path> |
path:/home/user/my-extension |
Settings field:
{
"packages": [
"npm:@pi/tools@1.0.0",
"git:github.com/user/extension#v1.0"
]
}This specification covers:
- Message types: User, Assistant, ToolResult with all content block variants
- Streaming: Full event type enumeration with sequences
- Providers: Trait definition and model registry structure
- Tools: Built-in tools with exact parameters and behaviors
- Sessions: JSONL format with tree structure
- Config: Settings structure with precedence rules
- Auth: Credential storage with OAuth refresh
- CLI: Complete flag list with execution flow
- RPC: JSON command protocol with events, types, and extension UI
- Extensions: Full API surface with registration, events, UI, and cancellation semantics
- Resources: Skills, prompts, and themes discovery and expansion
After reading this document, you should NOT need to consult the legacy TypeScript code.
This appendix provides the complete, detailed Extension API extracted from the legacy pi-mono codebase.
Extensions export a default factory function receiving the ExtensionAPI object:
export default function (pi: ExtensionAPI) {
// Register handlers, tools, commands
}
// Or async:
export default async function (pi: ExtensionAPI) {
// Async initialization
}| Event | Can Modify | Can Cancel | Payload |
|---|---|---|---|
resources_discover |
Yes | No | {} |
session_start |
No | No | {} |
session_before_switch |
No | Yes | {} |
session_switch |
No | No | {reason, previousSessionFile?} |
session_before_fork |
No | Yes | {} |
session_fork |
No | No | {previousSessionFile?} |
session_before_compact |
Yes | Yes | {preparation, branchEntries, signal} |
session_compact |
No | No | {compactionEntry, fromExtension} |
session_before_tree |
Yes | Yes | {preparation, signal} |
session_tree |
No | No | {newLeafId, oldLeafId, summaryEntry?} |
session_shutdown |
No | No | {} |
context |
Yes | No | {messages} |
before_agent_start |
Yes | No | {prompt, images?, systemPrompt} |
agent_start |
No | No | {} |
agent_end |
No | No | {messages} |
turn_start |
No | No | {turnIndex, timestamp} |
turn_end |
No | No | {turnIndex, message, toolResults} |
model_select |
No | No | {model, previousModel?, source} |
tool_call |
No | Yes (block) | {toolName, input, toolCallId} |
tool_result |
Yes | No | {toolName, input, content, isError} |
user_bash |
Yes | No | {command, excludeFromContext, cwd} |
input |
Yes | Yes (handled) | {text, images?, source} |
Tool Registration:
pi.registerTool({
name: string,
label: string,
description: string,
parameters: TypeBoxSchema,
execute: (toolCallId, params, signal, onUpdate, ctx) => Promise<ToolOutput>,
renderCall?: (args, theme) => Component,
renderResult?: (result, options, theme) => Component,
});Command Registration:
pi.registerCommand(name, {
description: string,
handler: (args, ctx) => Promise<void>,
getArgumentCompletions?: (prefix) => CompletionItem[],
});Shortcut Registration:
pi.registerShortcut(key, {
description: string,
handler: (ctx) => Promise<void>,
});Flag Registration:
pi.registerFlag(name, {
description: string,
type: "boolean" | "string",
default?: boolean | string,
});Provider Registration:
pi.registerProvider(name, {
baseUrl: string,
apiKey: string, // env var name
api: "anthropic-messages" | "openai-responses",
models: Model[],
streamSimple?: StreamHandler,
oauth?: OAuthConfig,
});// Custom message (not for LLM)
pi.sendMessage({
customType: string,
content: string,
display?: boolean,
details?: unknown,
}, {
triggerTurn?: boolean,
deliverAs?: "steer" | "followUp" | "nextTurn",
});
// User message (triggers LLM turn)
pi.sendUserMessage(content, {
deliverAs?: "steer" | "followUp",
});
// Session entry (not sent to LLM)
pi.appendEntry(customType, data);// Session metadata
pi.setSessionName(name);
pi.getSessionName();
pi.setLabel(entryId, label);
// Tool management
pi.getActiveTools();
pi.getAllTools();
pi.setActiveTools(toolNames);
// Model control
await pi.setModel(model);
pi.getThinkingLevel();
pi.setThinkingLevel(level);
// Execution
await pi.exec(command, args, options);
// Inter-extension events
pi.events.emit(eventName, data);
pi.events.on(eventName, handler);Dialogs (blocking):
await ctx.ui.select(title, options, opts);
await ctx.ui.confirm(title, message, opts);
await ctx.ui.input(title, placeholder?, opts);
await ctx.ui.editor(title, prefill?);Non-blocking:
ctx.ui.notify(message, type?);
ctx.ui.setStatus(key, text);
ctx.ui.setWorkingMessage(message?);
ctx.ui.setWidget(key, content, options?);
ctx.ui.setFooter(factory?);
ctx.ui.setHeader(factory?);
ctx.ui.setTitle(title);Editor:
ctx.ui.setEditorText(text);
ctx.ui.getEditorText();
ctx.ui.setEditorComponent(factory?);Theme:
ctx.ui.theme;
ctx.ui.getAllThemes();
ctx.ui.getTheme(name);
ctx.ui.setTheme(theme);interface ExtensionContext {
ui: ExtensionUIContext;
hasUI: boolean;
cwd: string;
sessionManager: ReadonlySessionManager;
modelRegistry: ModelRegistry;
model: Model | undefined;
isIdle(): boolean;
abort(): void;
hasPendingMessages(): boolean;
shutdown(): void;
getContextUsage(): ContextUsage | undefined;
compact(options?): void;
getSystemPrompt(): string;
}
interface ExtensionCommandContext extends ExtensionContext {
waitForIdle(): Promise<void>;
newSession(options?): Promise<{cancelled: boolean}>;
fork(entryId): Promise<{cancelled: boolean}>;
navigateTree(targetId, options?): Promise<{cancelled: boolean}>;
switchSession(sessionPath): Promise<{cancelled: boolean}>;
}Currently Used:
Cxcapability contextmpsc/oneshotchannelsMutex/Notifysynchronizationtimeout/sleeptime operations
Should Use:
Cx::region()for structured concurrency (extension lifecycle)database::sqlitefor session indexing (optional)- Two-phase channel semantics for atomic message delivery
- Cancellation budgets for bounded cleanup
LabRuntimefor deterministic testing
Currently Used:
Consolewith markupPanelfor boxed contentTablefor dataRulefor dividers
Should Enable:
markdownfeature for assistant response renderingsyntaxfeature for code block highlightingjsonfeature for pretty-printed JSON output
Currently Used:
bubbletea::Modelfor Elm Architecturebubbles::TextAreafor inputbubbles::Viewportfor scrollingbubbles::Spinnerfor loadingglamour::Rendererfor markdown
Should Use:
lipgloss::Stylefor direct stylingbubbles::Listfor history navigationbubbles::Tablefor structured output- Custom glamour theme matching Pi visual style