Skip to main content

Extension Interface

Extensions are TypeScript modules that export a default function receiving the ExtensionAPI:
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';

export default function (pi: ExtensionAPI) {
  // Register event handlers, tools, commands
}
Extensions are automatically discovered from:
  • ~/.pi/agent/extensions/
  • <cwd>/.pi/extensions/
  • Paths in settings.json “extensions” array

ExtensionAPI

The pi object passed to extensions provides methods for registration and interaction.

Event Subscription

Subscribe to lifecycle events using pi.on():
pi.on(event: string, handler: ExtensionHandler): void

Available Events

resources_discover
ResourcesDiscoverEvent
Fired after session_start to provide additional resource pathsReturns: ResourcesDiscoverResult with skillPaths, promptPaths, themePaths
session_start
SessionStartEvent
Fired on initial session load
session_before_switch
SessionBeforeSwitchEvent
Fired before switching sessions (can be cancelled)Returns: { cancel?: boolean }
session_switch
SessionSwitchEvent
Fired after switching to another session
session_before_fork
SessionBeforeForkEvent
Fired before forking a session (can be cancelled)Returns: { cancel?: boolean }
session_fork
SessionForkEvent
Fired after forking a session
session_before_compact
SessionBeforeCompactEvent
Fired before context compaction (can be cancelled or customized)Returns: { cancel?: boolean; compaction?: CompactionResult }
session_compact
SessionCompactEvent
Fired after context compaction completes
session_shutdown
SessionShutdownEvent
Fired on process exit
session_before_tree
SessionBeforeTreeEvent
Fired before navigating in session tree (can be cancelled)Returns: { cancel?: boolean; summary?: { summary: string; details?: unknown } }
session_tree
SessionTreeEvent
Fired after navigating in the session tree
context
ContextEvent
Fired before each LLM call. Can modify messagesReturns: { messages?: AgentMessage[] }
before_agent_start
BeforeAgentStartEvent
Fired after user submits prompt but before agent loopReturns: { message?: CustomMessage; systemPrompt?: string }
agent_start
AgentStartEvent
Fired when an agent loop starts
agent_end
AgentEndEvent
Fired when an agent loop ends
turn_start
TurnStartEvent
Fired at the start of each turn
turn_end
TurnEndEvent
Fired at the end of each turn
message_start
MessageStartEvent
Fired when a message starts (user, assistant, or toolResult)
message_update
MessageUpdateEvent
Fired during assistant message streaming with token-by-token updates
message_end
MessageEndEvent
Fired when a message ends
tool_execution_start
ToolExecutionStartEvent
Fired when a tool starts executing
tool_execution_update
ToolExecutionUpdateEvent
Fired during tool execution with partial/streaming output
tool_execution_end
ToolExecutionEndEvent
Fired when a tool finishes executing
model_select
ModelSelectEvent
Fired when a new model is selected
tool_call
ToolCallEvent
Fired before a tool executes. Can block executionReturns: { block?: boolean; reason?: string }
tool_result
ToolResultEvent
Fired after a tool executes. Can modify resultReturns: { content?: Content[]; details?: unknown; isError?: boolean }
user_bash
UserBashEvent
Fired when user executes bash via ! or !! prefixReturns: { operations?: BashOperations; result?: BashResult }
input
InputEvent
Fired when user input is received, before agent processingReturns: { action: 'continue' | 'transform' | 'handled'; text?: string; images?: ImageContent[] }

Tool Registration

Register LLM-callable tools:
pi.registerTool<TParams, TDetails>(tool: ToolDefinition<TParams, TDetails>): void

ToolDefinition

name
string
required
Tool name (used in LLM tool calls)
label
string
required
Human-readable label for UI
description
string
required
Description for LLM
parameters
TSchema
required
Parameter schema (TypeBox)
execute
function
required
Execute the tool
async execute(
  toolCallId: string,
  params: Static<TParams>,
  signal: AbortSignal | undefined,
  onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
  ctx: ExtensionContext,
): Promise<AgentToolResult<TDetails>>
renderCall
function
Custom rendering for tool call display
renderCall?: (args: Static<TParams>, theme: Theme) => Component
renderResult
function
Custom rendering for tool result display
renderResult?: (
  result: AgentToolResult<TDetails>,
  options: ToolRenderResultOptions,
  theme: Theme
) => Component

Example

import { Type } from '@mariozechner/pi-ai';
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: 'hello',
    label: 'Hello',
    description: 'A simple greeting tool',
    parameters: Type.Object({
      name: Type.String({ description: 'Name to greet' }),
    }),

    async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
      const { name } = params;
      return {
        content: [{ type: 'text', text: `Hello, ${name}!` }],
        details: { greeted: name },
      };
    },
  });
}

Command Registration

Register custom slash commands:
pi.registerCommand(name: string, options: {
  description?: string;
  getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
  handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
}): void

Example

pi.registerCommand('greet', {
  description: 'Greet the user',
  handler: async (args, ctx) => {
    const name = args || 'stranger';
    ctx.ui.notify(`Hello, ${name}!`);
  },
});

Shortcut Registration

Register keyboard shortcuts:
pi.registerShortcut(shortcut: KeyId, options: {
  description?: string;
  handler: (ctx: ExtensionContext) => Promise<void> | void;
}): void

Example

pi.registerShortcut('ctrl+shift+g', {
  description: 'Quick greeting',
  handler: async (ctx) => {
    ctx.ui.notify('Hello from shortcut!');
  },
});

Flag Registration

Register CLI flags:
pi.registerFlag(name: string, options: {
  description?: string;
  type: 'boolean' | 'string';
  default?: boolean | string;
}): void

pi.getFlag(name: string): boolean | string | undefined

Example

pi.registerFlag('verbose', {
  description: 'Enable verbose logging',
  type: 'boolean',
  default: false,
});

pi.on('agent_start', () => {
  if (pi.getFlag('verbose')) {
    console.log('Agent starting in verbose mode');
  }
});

Message Actions

Send messages to the session:
// Send custom message
pi.sendMessage<T>(
  message: Pick<CustomMessage<T>, 'customType' | 'content' | 'display' | 'details'>,
  options?: { triggerTurn?: boolean; deliverAs?: 'steer' | 'followUp' | 'nextTurn' }
): void

// Send user message (always triggers a turn)
pi.sendUserMessage(
  content: string | (TextContent | ImageContent)[],
  options?: { deliverAs?: 'steer' | 'followUp' }
): void

// Append custom entry for state persistence (not sent to LLM)
pi.appendEntry<T>(customType: string, data?: T): void

Session Metadata

pi.setSessionName(name: string): void
pi.getSessionName(): string | undefined
pi.setLabel(entryId: string, label: string | undefined): void

Tool Management

pi.getActiveTools(): string[]
pi.getAllTools(): ToolInfo[]
pi.setActiveTools(toolNames: string[]): void

Model Management

pi.setModel(model: Model<any>): Promise<boolean>
pi.getThinkingLevel(): ThinkingLevel
pi.setThinkingLevel(level: ThinkingLevel): void

Provider Registration

Register or override model providers:
pi.registerProvider(name: string, config: ProviderConfig): void

Example

pi.registerProvider('my-proxy', {
  baseUrl: 'https://proxy.example.com',
  apiKey: 'PROXY_API_KEY',
  api: 'anthropic-messages',
  models: [
    {
      id: 'claude-sonnet-4-20250514',
      name: 'Claude 4 Sonnet (proxy)',
      reasoning: false,
      input: ['text', 'image'],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 200000,
      maxTokens: 16384,
    },
  ],
});

Message Rendering

Register custom renderers for CustomMessageEntry:
pi.registerMessageRenderer<T>(
  customType: string,
  renderer: MessageRenderer<T>
): void

Utility

pi.exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>
pi.getCommands(): SlashCommandInfo[]
pi.events: EventBus  // Shared event bus for extension communication

ExtensionContext

Context passed to event handlers and tool execution:
ui
ExtensionUIContext
UI methods for user interaction (see UI Context)
hasUI
boolean
Whether UI is available (false in print/RPC mode)
cwd
string
Current working directory
sessionManager
ReadonlySessionManager
Session manager (read-only)
modelRegistry
ModelRegistry
Model registry for API key resolution
model
Model<any> | undefined
Current model (may be undefined)
isIdle
() => boolean
Whether the agent is idle (not streaming)
abort
() => void
Abort the current agent operation
hasPendingMessages
() => boolean
Whether there are queued messages waiting
shutdown
() => void
Gracefully shutdown pi and exit
getContextUsage
() => ContextUsage | undefined
Get current context usage for the active model
compact
(options?: CompactOptions) => void
Trigger compaction without awaiting completion
getSystemPrompt
() => string
Get the current effective system prompt

ExtensionCommandContext

Extended context for command handlers with session control:
waitForIdle
() => Promise<void>
Wait for the agent to finish streaming
newSession
function
Start a new session, optionally with initialization
async newSession(options?: {
  parentSession?: string;
  setup?: (sessionManager: SessionManager) => Promise<void>;
}): Promise<{ cancelled: boolean }>
fork
(entryId: string) => Promise<{ cancelled: boolean }>
Fork from a specific entry, creating a new session file
navigateTree
function
Navigate to a different point in the session tree
async navigateTree(
  targetId: string,
  options?: {
    summarize?: boolean;
    customInstructions?: string;
    replaceInstructions?: boolean;
    label?: string;
  }
): Promise<{ cancelled: boolean }>
switchSession
(sessionPath: string) => Promise<{ cancelled: boolean }>
Switch to a different session file
reload
() => Promise<void>
Reload extensions, skills, prompts, and themes

UI Context

UI methods available via ctx.ui for interactive user interaction:
// Dialogs
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?)

// Notifications
ctx.ui.notify(message, type?)
ctx.ui.setStatus(key, text)
ctx.ui.setWorkingMessage(message?)

// Widgets
ctx.ui.setWidget(key, content, options?)
ctx.ui.setFooter(factory)
ctx.ui.setHeader(factory)

// Editor
ctx.ui.pasteToEditor(text)
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)

// Terminal
ctx.ui.setTitle(title)
ctx.ui.onTerminalInput(handler)

// Custom components
await ctx.ui.custom(factory, options?)
See the full API documentation for detailed signatures and examples.

Build docs developers (and LLMs) love