返回源码地图

packages/coding-agent/src/core/extensions/types.ts

v1.0.0 · a13d35a742c6 · 08:事件边界与mutable input;非全部公开类型逐句分析

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Extension system types.
3 *
4 * Extensions are TypeScript modules that can:
5 * - Subscribe to agent lifecycle events
6 * - Register LLM-callable tools
7 * - Register commands, keyboard shortcuts, and CLI flags
8 * - Interact with the user via UI primitives
9 */
10
11import type {
12 AgentMessage,
13 AgentTool,
14 AgentToolCallOutcome,
15 AgentToolResult,
16 AgentToolUpdateCallback,
17 ThinkingLevel,
18 ToolExecutionMode,
19} from "@earendil-works/pi-agent-core";
20import type {
21 AnyModel,
22 Api,
23 AssistantMessageEvent,
24 AssistantMessageEventStream,
25 ClassifierApi,
26 ConstrainedSamplingConfig,
27 ImageApi,
28 ImageContent,
29 JsonValue,
30 Message,
31 Model,
32 OAuthCredentials,
33 OAuthLoginCallbacks,
34 Provider,
35 ProviderClassifier,
36 ProviderHeaders,
37 ProviderId,
38 ProviderImages,
39 RefreshModelsContext,
40 SimpleStreamOptions,
41 TextContent,
42 ToolResultMessage,
43 TranscriptContext,
44 Usage,
45} from "@earendil-works/pi-ai";
46import type {
47 AutocompleteItem,
48 AutocompleteProvider,
49 Component,
50 EditorComponent,
51 EditorTheme,
52 KeyId,
53 OverlayHandle,
54 OverlayOptions,
55 TUI,
56} from "@earendil-works/pi-tui";
57import type { Static, TSchema } from "typebox";
58import type { Theme } from "../../modes/interactive/theme/theme.ts";
59import type { BashResult } from "../bash-executor.ts";
60import type { CacheWarmingDecisionEvent, CacheWarmingDecisionEventResult } from "../cache-warmer.ts";
61import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts";
62import type { EventBus } from "../event-bus.ts";
63import type { ExecOptions, ExecResult } from "../exec.ts";
64import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts";
65import type { KeybindingsManager } from "../keybindings.ts";
66import type { McpServerConfig, McpServerRegistry, RegisteredMcpServer } from "../mcp-servers.ts";
67import type { CustomMessage } from "../messages.ts";
68import type { ModelRegistry } from "../model-registry.ts";
69import type { ScopedModel } from "../model-resolver.ts";
70import type {
71 BranchSummaryEntry,
72 CompactionEntry,
73 ContextEditEntry,
74 CustomEntry,
75 ProjectedSessionEntry,
76 ReadonlySessionManager,
77 SessionEntry,
78 SessionManager,
79} from "../session-manager.ts";
80import type { Settings } from "../settings-manager.ts";
81import type { SlashCommandInfo } from "../slash-commands.ts";
82import type { SourceInfo } from "../source-info.ts";
83import type { BuildSystemPromptOptions, NormalizedBuildSystemPromptOptions } from "../system-prompt.ts";
84import type { BashOperations } from "../tools/bash.ts";
85import type { EditToolDetails } from "../tools/edit.ts";
86import type {
87 BashToolDetails,
88 BashToolInput,
89 EditToolInput,
90 FindToolDetails,
91 FindToolInput,
92 GrepToolDetails,
93 GrepToolInput,
94 LsToolDetails,
95 LsToolInput,
96 PowerShellToolDetails,
97 PowerShellToolInput,
98 ReadToolDetails,
99 ReadToolInput,
100 WriteToolInput,
101} from "../tools/index.ts";
102import type { ModelRoute, ModelRouteRequest, VirtualModelDefinition } from "../virtual-models.ts";
103
104export type { ExecOptions, ExecResult } from "../exec.ts";
105export type { BuildSystemPromptOptions, NormalizedBuildSystemPromptOptions } from "../system-prompt.ts";
106export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode };
107export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts";
108
109// ============================================================================
110// UI Context
111// ============================================================================
112
113/** Options for extension UI dialogs. */
114export interface ExtensionUIDialogOptions {
115 /** AbortSignal to programmatically dismiss the dialog. */
116 signal?: AbortSignal;
117 /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */
118 timeout?: number;
119}
120
121/** Placement for extension widgets. */
122export type WidgetPlacement = "aboveEditor" | "belowEditor";
123
124/** Options for extension widgets. */
125export interface ExtensionWidgetOptions {
126 /** Where the widget is rendered. Defaults to "aboveEditor". */
127 placement?: WidgetPlacement;
128}
129
130/** Raw terminal input listener for extensions. */
131export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: string } | undefined;
132
133/** Working indicator configuration for the interactive streaming loader. */
134export interface WorkingIndicatorOptions {
135 /** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */
136 frames?: string[];
137 /** Frame interval in milliseconds for animated indicators. */
138 intervalMs?: number;
139}
140
141/** Wrap the current autocomplete provider with additional behavior. */
142export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
143export type EditorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent;
144
145/**
146 * UI context for extensions to request interactive UI.
147 * Each mode (interactive, RPC, print) provides its own implementation.
148 */
149export interface ExtensionUIContext {
150 /** Show a selector and return the user's choice. */
151 select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
152
153 /** Show a confirmation dialog. */
154 confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise<boolean>;
155
156 /** Show a text input dialog. */
157 input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
158
159 /** Show a notification to the user. */
160 notify(message: string, type?: "info" | "warning" | "error"): void;
161
162 /** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */
163 onTerminalInput(handler: TerminalInputHandler): () => void;
164
165 /** Set status text in the footer/status bar. Pass undefined to clear. */
166 setStatus(key: string, text: string | undefined): void;
167
168 /** Set the working/loading message shown during streaming. Call with no argument to restore default. */
169 setWorkingMessage(message?: string): void;
170
171 /** Show or hide the built-in interactive working loader row during streaming. */
172 setWorkingVisible(visible: boolean): void;
173
174 /**
175 * Configure the interactive working indicator shown during streaming.
176 *
177 * - Omit the argument to restore the default animated spinner.
178 * - Use `frames: ["●"]` for a static indicator.
179 * - Use `frames: []` to hide the indicator entirely.
180 * - Custom frames are rendered as provided, so extensions must add their own colors.
181 */
182 setWorkingIndicator(options?: WorkingIndicatorOptions): void;
183
184 /** Set the label shown for hidden thinking blocks. Call with no argument to restore default. */
185 setHiddenThinkingLabel(label?: string): void;
186
187 /** Set a widget to display above or below the editor. Accepts string array or component factory. */
188 setWidget(key: string, content: string[] | undefined, options?: ExtensionWidgetOptions): void;
189 setWidget(
190 key: string,
191 content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined,
192 options?: ExtensionWidgetOptions,
193 ): void;
194
195 /** Set a custom footer component, or undefined to restore the built-in footer.
196 *
197 * The factory receives a FooterDataProvider for data not otherwise accessible:
198 * git branch and extension statuses from setStatus(). Context usage is on
199 * ctx.getContextUsage(), token stats on ctx.sessionManager.getEntries(), model info on ctx.model.
200 */
201 setFooter(
202 factory:
203 | ((tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
204 | undefined,
205 ): void;
206
207 /** Set a custom header component (shown at startup, above chat), or undefined to restore the built-in header. */
208 setHeader(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
209
210 /** Set the terminal window/tab title. */
211 setTitle(title: string): void;
212
213 /** Show a custom component with keyboard focus. */
214 custom<T>(
215 factory: (
216 tui: TUI,
217 theme: Theme,
218 keybindings: KeybindingsManager,
219 done: (result: T) => void,
220 ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
221 options?: {
222 overlay?: boolean;
223 /** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */
224 overlayOptions?: OverlayOptions | (() => OverlayOptions);
225 /** Called with the overlay handle after the overlay is shown. Use to control visibility. */
226 onHandle?: (handle: OverlayHandle) => void;
227 },
228 ): Promise<T>;
229
230 /** Paste text into the editor, triggering paste handling (collapse for large content). */
231 pasteToEditor(text: string): void;
232
233 /** Set the text in the core input editor. */
234 setEditorText(text: string): void;
235
236 /** Get the current text from the core input editor. */
237 getEditorText(): string;
238
239 /** Show a multi-line editor for text editing. */
240 editor(title: string, prefill?: string): Promise<string | undefined>;
241
242 /** Stack additional autocomplete behavior on top of the built-in provider. */
243 addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
244
245 /**
246 * Set a custom editor component via factory function.
247 * Pass undefined to restore the default editor.
248 *
249 * The factory receives:
250 * - `theme`: EditorTheme for styling borders and autocomplete
251 * - `keybindings`: KeybindingsManager for app-level keybindings
252 *
253 * For full app keybinding support (escape, ctrl+d, model switching, etc.),
254 * extend `CustomEditor` from `@earendil-works/pi-coding-agent` and call
255 * `super.handleInput(data)` for keys you don't handle.
256 *
257 * @example
258 * ```ts
259 * import { CustomEditor } from "@earendil-works/pi-coding-agent";
260 *
261 * class VimEditor extends CustomEditor {
262 * private mode: "normal" | "insert" = "insert";
263 *
264 * handleInput(data: string): void {
265 * if (this.mode === "normal") {
266 * // Handle vim normal mode keys...
267 * if (data === "i") { this.mode = "insert"; return; }
268 * }
269 * super.handleInput(data); // App keybindings + text editing
270 * }
271 * }
272 *
273 * ctx.ui.setEditorComponent((tui, theme, keybindings) =>
274 * new VimEditor(tui, theme, keybindings)
275 * );
276 * ```
277 */
278 setEditorComponent(factory: EditorFactory | undefined): void;
279
280 /** Get the currently configured custom editor factory, or undefined when using the default editor. */
281 getEditorComponent(): EditorFactory | undefined;
282
283 /** Get the current theme for styling. */
284 readonly theme: Theme;
285
286 /** Get all available themes with their names and file paths. */
287 getAllThemes(): { name: string; path: string | undefined }[];
288
289 /** Load a theme by name without switching to it. Returns undefined if not found. */
290 getTheme(name: string): Theme | undefined;
291
292 /** Set the current theme by name or Theme object. */
293 setTheme(theme: string | Theme): { success: boolean; error?: string };
294
295 /** Get current tool output expansion state. */
296 getToolsExpanded(): boolean;
297
298 /** Set tool output expansion state. */
299 setToolsExpanded(expanded: boolean): void;
300}
301
302// ============================================================================
303// Extension Context
304// ============================================================================
305
306export interface ContextUsage {
307 /** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */
308 tokens: number | null;
309 contextWindow: number;
310 /** Context usage as percentage of context window, or null if tokens is unknown. */
311 percent: number | null;
312}
313
314export interface CompactOptions {
315 customInstructions?: string;
316 onComplete?: (result: CompactionResult) => void;
317 onError?: (error: Error) => void;
318}
319
320/**
321 * Context passed to extension event handlers.
322 */
323export type ExtensionMode = "tui" | "rpc" | "json" | "print";
324
325export interface ExtensionContext {
326 /** UI methods for user interaction */
327 ui: ExtensionUIContext;
328 /** Current run mode. Use "tui" to guard terminal-only UI such as custom components. */
329 mode: ExtensionMode;
330 /** Whether dialog-capable UI is available (true in TUI and RPC modes) */
331 hasUI: boolean;
332 /** Current working directory */
333 cwd: string;
334 /** Session manager (read-only) */
335 sessionManager: ReadonlySessionManager;
336 /** Model registry for API key resolution */
337 modelRegistry: ModelRegistry;
338 /** Current model (may be undefined) */
339 model: Model<any> | undefined;
340 /** Models scoped to this session (resolved from `--models` /
341 * `enabledModels` settings against the available catalogue). Same set
342 * the `/scoped-models` command shows. Empty when no scoping is
343 * configured (all available models are usable). Read-only snapshot. */
344 scopedModels: readonly ScopedModel[];
345 /** Current thinking level, when provided by the session runtime. */
346 thinkingLevel?: ThinkingLevel;
347 /** Whether the agent is idle (not streaming) */
348 isIdle(): boolean;
349 /** Whether project-local trust is active for this context. */
350 isProjectTrusted(): boolean;
351 /** The current abort signal, or undefined when the agent is not streaming. */
352 signal: AbortSignal | undefined;
353 /** Abort the current agent operation */
354 abort(): void;
355 /** Whether there are queued messages waiting */
356 hasPendingMessages(): boolean;
357 /** Gracefully shutdown pi and exit. Available in all contexts. */
358 shutdown(): void;
359 /** Get current context usage for the active model. */
360 getContextUsage(): ContextUsage | undefined;
361 /** Trigger compaction without awaiting completion. */
362 compact(options?: CompactOptions): void;
363 /** Get the current effective system prompt. */
364 getSystemPrompt(): string;
365}
366
367/** Options for {@link ExtensionToolContext.executeTool}. */
368export interface ExecuteToolOptions {
369 /** Defaults to the calling tool's signal. */
370 signal?: AbortSignal;
371 /** Receives partial results of the nested tool, in addition to `tool_execution_update` events. */
372 onUpdate?: AgentToolUpdateCallback;
373}
374
375/**
376 * Context passed to tool `execute()` in a session: the extension context plus `executeTool()`
377 * for running other tools through the same validation, hooks, and permission checks as
378 * model-issued calls.
379 *
380 * A tool wrapped with `wrapToolDefinition()` without a context factory, such as a built-in tool
381 * created with `createBashTool()` and run in a plain `Agent` or called directly, gets no context.
382 */
383export interface ExtensionToolContext extends ExtensionContext {
384 /** Tools {@link executeTool} can call. */
385 readonly tools: readonly AgentTool[];
386 /**
387 * Run another tool. The call gets the id `<calling id>/<n>`, and the `tool_call`, `tool_result`,
388 * and `tool_execution_*` events carry `parentToolCallId`. It does not appear in the transcript;
389 * a bounded record of it is kept as `nestedCalls` on the calling tool's result message.
390 *
391 * Never rejects for tool failures: unknown tools, validation errors, blocked calls, and thrown
392 * errors come back as `isError: true`.
393 */
394 executeTool(name: string, args: unknown, options?: ExecuteToolOptions): Promise<AgentToolCallOutcome>;
395}
396
397/**
398 * Extended context for command handlers.
399 * Includes session control methods only safe in user-initiated commands.
400 */
401export interface ExtensionCommandContext extends ExtensionContext {
402 /** Get the current base system-prompt construction options. */
403 getSystemPromptOptions(): BuildSystemPromptOptions;
404
405 /** Wait for the agent to finish streaming */
406 waitForIdle(): Promise<void>;
407
408 /** Start a new session, optionally with initialization. */
409 newSession(options?: {
410 parentSession?: string;
411 setup?: (sessionManager: SessionManager) => Promise<void>;
412 withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
413 }): Promise<{ cancelled: boolean }>;
414
415 /** Fork from a specific entry, creating a new session file. */
416 fork(
417 entryId: string,
418 options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
419 ): Promise<{ cancelled: boolean }>;
420
421 /** Navigate to a different point in the session tree. */
422 navigateTree(
423 targetId: string,
424 options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
425 ): Promise<{ cancelled: boolean }>;
426
427 /** Switch to a different session file. */
428 switchSession(
429 sessionPath: string,
430 options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
431 ): Promise<{ cancelled: boolean }>;
432
433 /** Reload extensions, skills, prompts, themes, and context files. */
434 reload(): Promise<void>;
435}
436
437/**
438 * Fresh command-capable context bound to the replacement session after a session switch.
439 *
440 * This is passed to `withSession()` callbacks on `newSession()`, `fork()`, and `switchSession()`.
441 */
442export interface ReplacedSessionContext extends ExtensionCommandContext {
443 sendMessage<T = unknown>(
444 message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
445 options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
446 ): Promise<void>;
447
448 sendUserMessage(
449 content: string | (TextContent | ImageContent)[],
450 options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
451 ): Promise<void>;
452}
453
454// ============================================================================
455// Tool Types
456// ============================================================================
457
458/** Rendering options for tool results */
459export interface ToolRenderResultOptions {
460 /** Whether the result view is expanded */
461 expanded: boolean;
462 /** Whether this is a partial/streaming result */
463 isPartial: boolean;
464}
465
466/** Context passed to tool renderers. */
467export interface ToolRenderContext<TState = any, TArgs = any> {
468 /** Current tool call arguments. Shared across call/result renders for the same tool call. */
469 args: TArgs;
470 /** Unique id for this tool execution. Stable across call/result renders for the same tool call. */
471 toolCallId: string;
472 /** Invalidate just this tool execution component for redraw. */
473 invalidate: () => void;
474 /** Previously returned component for this render slot, if any. */
475 lastComponent: Component | undefined;
476 /** Shared renderer state for this tool row. Initialized by tool-execution.ts. */
477 state: TState;
478 /** Working directory for this tool execution. */
479 cwd: string;
480 /** Whether the tool execution has started. */
481 executionStarted: boolean;
482 /** Whether the tool call arguments are complete. */
483 argsComplete: boolean;
484 /** Whether the tool result is partial/streaming. */
485 isPartial: boolean;
486 /** Whether the result view is expanded. */
487 expanded: boolean;
488 /** Whether inline images are currently shown in the TUI. */
489 showImages: boolean;
490 /** Whether the current result is an error. */
491 isError: boolean;
492}
493
494/**
495 * How the model reaches a tool. "Callable" means callable from other tools through
496 * `ctx.executeTool()`, as the `codemode` tool does.
497 *
498 * - `direct`: declared to the model while active, and callable while active.
499 * - `model-only`: declared to the model while active, never callable. Use it for orchestrating or
500 * interactive tools.
501 * - `codemode`: callable whenever registered. Not declared to the model unless explicitly
502 * activated. Codemode tools list it in their description.
503 * - `deferred`: like `codemode`, but codemode tools do not list it; tool search can find it.
504 * - `hidden`: registered but unreachable. Activating it has no effect.
505 *
506 * `direct` and `model-only` tools are activated when they are registered; the others are not.
507 * The active tool set (`getActiveTools`/`setActiveTools`) is the set declared to the model.
508 */
509export type ToolExposure = "direct" | "model-only" | "codemode" | "deferred" | "hidden";
510
511/**
512 * Hints about what a tool does, with the meaning of MCP tool annotations. They come from the tool's
513 * author and are not verified; permission extensions can use them to decide which calls to confirm.
514 */
515export interface ToolAnnotations {
516 /** The tool does not modify its environment. */
517 readOnlyHint?: boolean;
518 /** The tool may delete or overwrite data, rather than only add to it. Meaningful when not read-only. */
519 destructiveHint?: boolean;
520 /** Repeating a call with the same arguments has no further effect. Meaningful when not read-only. */
521 idempotentHint?: boolean;
522 /** The tool reaches an open world of external entities, such as the web, rather than a closed domain. */
523 openWorldHint?: boolean;
524}
525
526/** A group of related tools, such as the tools of one MCP server. Codemode tools list them together. */
527export interface ToolNamespace {
528 /** For example `mcp__docs`. */
529 name: string;
530 /** Short summary shown once with the group in model-facing tool listings. */
531 description?: string;
532 /**
533 * Longer usage guidance, such as MCP server instructions. Not part of tool listings; tools that
534 * describe the namespace on request (codemode's `describeNamespace()`) return it.
535 */
536 instructions?: string;
537}
538
539/** The tools of a session as {@link ToolDefinition.prepareLoadout} sees them. */
540export interface ToolLoadout {
541 /** Tools declared to the model (the active tools), in order, with their original descriptions. */
542 readonly declared: readonly AgentTool[];
543 /** Tools callable through `ctx.executeTool()`. */
544 readonly callable: readonly AgentTool[];
545 /** Every registered tool. */
546 readonly registered: readonly AgentTool[];
547 getExposure(name: string): ToolExposure;
548 getNamespace(name: string): ToolNamespace | undefined;
549}
550
551/** Changes {@link ToolDefinition.prepareLoadout} makes to what the model sees. */
552export interface ToolLoadoutChanges {
553 /** Model-facing descriptions of declared tools, by tool name. */
554 descriptions?: Readonly<Record<string, string>>;
555 /**
556 * Declared tools whose declarations requests leave out. They stay active and callable, and the
557 * transcript still declares them, so the active set survives `/tree` and resume.
558 */
559 hiddenDeclarations?: readonly string[];
560}
561
562/**
563 * Tool definition for registerTool().
564 */
565export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any> {
566 /** Tool name (used in LLM tool calls) */
567 name: string;
568 /** Human-readable label for UI */
569 label: string;
570 /** Description for LLM */
571 description: string;
572 /** Optional one-line snippet for the Available tools section in the default system prompt. Custom tools are omitted from that section when this is not provided. */
573 promptSnippet?: string;
574 /** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */
575 promptGuidelines?: string[];
576 /** Parameter schema (TypeBox) */
577 parameters: TParams;
578 /** Optional provider-side constrained sampling request for this tool. Set false to explicitly disable it, equivalent to leaving it undefined. */
579 constrainedSampling?: false | ConstrainedSamplingConfig;
580 /** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
581 renderShell?: "default" | "self";
582
583 /** Optional compatibility shim to prepare raw tool call arguments before schema validation. Must return an object conforming to TParams. */
584 prepareArguments?: (args: unknown) => Static<TParams>;
585
586 /**
587 * JSON Schema of `structuredContent` in successful results. Tools that declare it should always
588 * set `structuredContent`; codemode scripts then receive it instead of the text content.
589 */
590 outputSchema?: TSchema;
591
592 /**
593 * How the model reaches the tool. Default: `"direct"`. See {@link ToolExposure}.
594 */
595 exposure?: ToolExposure;
596
597 /** Group the tool belongs to, for example its MCP server. */
598 namespace?: ToolNamespace;
599
600 /** Hints about what the tool does, for example from an MCP server. */
601 annotations?: ToolAnnotations;
602
603 /**
604 * Whether registering the tool activates it. Default: `true` for `direct` and `model-only` tools;
605 * other exposures are never activated on registration. A tool with `defaultActive: false` is
606 * activated by naming it in `--tools` or the `defaultTools` setting, or with `setActiveTools()`.
607 */
608 defaultActive?: boolean;
609
610 /**
611 * Adjust how the loadout is presented to the model while this tool is active. Called whenever
612 * the active tools change. Tools that orchestrate other tools use it, for example to list the
613 * callable tools in their own description.
614 */
615 prepareLoadout?: (loadout: ToolLoadout) => ToolLoadoutChanges | undefined;
616
617 /**
618 * Per-tool execution mode override.
619 * - "sequential": this tool must execute one at a time with other tool calls.
620 * - "parallel": this tool can execute concurrently with other tool calls.
621 *
622 * If omitted, the default execution mode applies.
623 */
624 executionMode?: ToolExecutionMode;
625
626 /** Execute the tool. */
627 execute(
628 toolCallId: string,
629 params: Static<TParams>,
630 signal: AbortSignal | undefined,
631 onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
632 ctx: ExtensionToolContext,
633 ): Promise<AgentToolResult<TDetails>>;
634
635 /** Custom rendering for tool call display */
636 renderCall?: (args: Static<TParams>, theme: Theme, context: ToolRenderContext<TState, Static<TParams>>) => Component;
637
638 /** Custom rendering for tool result display */
639 renderResult?: (
640 result: AgentToolResult<TDetails>,
641 options: ToolRenderResultOptions,
642 theme: Theme,
643 context: ToolRenderContext<TState, Static<TParams>>,
644 ) => Component;
645}
646
647type AnyToolDefinition = ToolDefinition<any, any, any>;
648
649/**
650 * Preserve parameter inference for standalone tool definitions.
651 *
652 * Use this when assigning a tool to a variable or passing it through arrays such
653 * as `customTools`, where contextual typing would otherwise widen params to
654 * `unknown`.
655 */
656export function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(
657 tool: ToolDefinition<TParams, TDetails, TState>,
658): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition {
659 return tool as ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition;
660}
661
662// ============================================================================
663// Startup/Resource Events
664// ============================================================================
665
666export interface ProjectTrustEvent {
667 type: "project_trust";
668 cwd: string;
669}
670
671export type ProjectTrustEventDecision = "yes" | "no" | "undecided";
672
673export interface ProjectTrustEventResult {
674 trusted: ProjectTrustEventDecision;
675 remember?: boolean;
676}
677
678export interface ProjectTrustContext {
679 cwd: string;
680 mode: ExtensionMode;
681 hasUI: boolean;
682 ui: Pick<ExtensionUIContext, "select" | "confirm" | "input" | "notify">;
683}
684
685export type ProjectTrustHandler = (
686 event: ProjectTrustEvent,
687 ctx: ProjectTrustContext,
688) => Promise<ProjectTrustEventResult> | ProjectTrustEventResult;
689
690/** Fired after session_start to allow extensions to provide additional resource paths. */
691export interface ResourcesDiscoverEvent {
692 type: "resources_discover";
693 cwd: string;
694 reason: "startup" | "reload";
695}
696
697/** Result from resources_discover event handler */
698export interface ResourcesDiscoverResult {
699 skillPaths?: string[];
700 promptPaths?: string[];
701 themePaths?: string[];
702}
703
704/**
705 * Fired when an extension registers or unregisters an MCP server after the extensions are bound
706 * (see {@link ExtensionAPI.registerMcpServer}). Servers registered while extensions load are read
707 * with `pi.getMcpServers()` on `session_start`. Handling this event marks an extension as the one
708 * that connects registered servers.
709 */
710export interface McpServersChangeEvent {
711 type: "mcp_servers_change";
712 /** Every registered server after the change. */
713 servers: RegisteredMcpServer[];
714}
715
716// ============================================================================
717// Session Events
718// ============================================================================
719
720/** Fired when a session is started, loaded, or reloaded */
721export interface SessionStartEvent {
722 type: "session_start";
723 /** Why this session start happened. */
724 reason: "startup" | "reload" | "new" | "resume" | "fork";
725 /** Previously active session file. Present for "new", "resume", and "fork". */
726 previousSessionFile?: string;
727}
728
729/** Fired when the current session metadata changes. */
730export interface SessionInfoChangedEvent {
731 type: "session_info_changed";
732 /** Current normalized session name. Undefined when the name is cleared. */
733 name: string | undefined;
734}
735
736/** Fired before switching to another session (can be cancelled) */
737export interface SessionBeforeSwitchEvent {
738 type: "session_before_switch";
739 reason: "new" | "resume";
740 targetSessionFile?: string;
741}
742
743/** Fired before forking a session (can be cancelled) */
744export interface SessionBeforeForkEvent {
745 type: "session_before_fork";
746 entryId: string;
747 position: "before" | "at";
748}
749
750/** Fired before context compaction (can be cancelled or customized) */
751export interface SessionBeforeCompactEvent {
752 type: "session_before_compact";
753 preparation: CompactionPreparation;
754 branchEntries: SessionEntry[];
755 customInstructions?: string;
756 /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
757 reason: "manual" | "threshold" | "overflow";
758 /** True when the aborted turn is retried after this compaction (overflow recovery) */
759 willRetry: boolean;
760 signal: AbortSignal;
761}
762
763/** Fired after context compaction succeeds */
764export interface SessionCompactEvent {
765 type: "session_compact";
766 compactionEntry: CompactionEntry;
767 fromExtension: boolean;
768 /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
769 reason: "manual" | "threshold" | "overflow";
770 /** True when the aborted turn is retried after this compaction (overflow recovery) */
771 willRetry: boolean;
772}
773
774/** Fired after context compaction fails or is aborted */
775export interface SessionCompactFailedEvent {
776 type: "session_compact_failed";
777 /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
778 reason: "manual" | "threshold" | "overflow";
779 /** Error text when compaction failed for a non-abort reason. */
780 errorMessage?: string;
781 /** True when compaction was cancelled or aborted. */
782 aborted: boolean;
783 /** True when the aborted turn would have been retried after this compaction (overflow recovery) */
784 willRetry: boolean;
785 /** True when the failing compaction content came from a session_before_compact handler. */
786 fromExtension: boolean;
787}
788
789/** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */
790export interface SessionShutdownEvent {
791 type: "session_shutdown";
792 reason: "quit" | "reload" | "new" | "resume" | "fork";
793 /** Destination session file when shutting down due to session replacement. */
794 targetSessionFile?: string;
795}
796
797/** Preparation data for tree navigation */
798export interface TreePreparation {
799 targetId: string;
800 oldLeafId: string | null;
801 commonAncestorId: string | null;
802 entriesToSummarize: SessionEntry[];
803 userWantsSummary: boolean;
804 /** Custom instructions for summarization */
805 customInstructions?: string;
806 /** If true, customInstructions replaces the default prompt instead of being appended */
807 replaceInstructions?: boolean;
808 /** Label to attach to the branch summary entry */
809 label?: string;
810}
811
812/** Fired before navigating in the session tree (can be cancelled) */
813export interface SessionBeforeTreeEvent {
814 type: "session_before_tree";
815 preparation: TreePreparation;
816 signal: AbortSignal;
817}
818
819/** Fired after navigating in the session tree */
820export interface SessionTreeEvent {
821 type: "session_tree";
822 newLeafId: string | null;
823 oldLeafId: string | null;
824 summaryEntry?: BranchSummaryEntry;
825 fromExtension?: boolean;
826}
827
828export type SessionEvent =
829 | SessionStartEvent
830 | SessionInfoChangedEvent
831 | SessionBeforeSwitchEvent
832 | SessionBeforeForkEvent
833 | SessionBeforeCompactEvent
834 | SessionCompactEvent
835 | SessionCompactFailedEvent
836 | SessionShutdownEvent
837 | SessionBeforeTreeEvent
838 | SessionTreeEvent;
839
840// ============================================================================
841// Agent Events
842// ============================================================================
843
844/**
845 * Fired before each LLM call. Can modify messages.
846 *
847 * `messages` holds the conversation without system messages. The prompt and tool state
848 * belong to Pi: it restores them after the handler returns, so a handler cannot drop
849 * them and does not need to preserve them.
850 */
851export interface ContextEvent {
852 type: "context";
853 messages: AgentMessage[];
854}
855
856/**
857 * Fired before each LLM call, after every `context` handler has run and Pi has restored
858 * the prompt and tool state. `messages` is the full transcript including system messages,
859 * and the result is sent as returned: the handler owns the prompt and tool declarations.
860 */
861export interface ContextWithSystemEvent {
862 type: "context_with_system";
863 messages: AgentMessage[];
864}
865
866/** Fired before a provider request is sent. Can replace the payload. */
867export interface BeforeProviderRequestEvent {
868 type: "before_provider_request";
869 payload: unknown;
870}
871
872/**
873 * Fired after request headers are assembled, before the provider HTTP call.
874 * Handlers mutate `headers` in place (e.g. to inject tracing/session headers);
875 * the return value is ignored. A `null` value deletes that header.
876 */
877export interface BeforeProviderHeadersEvent {
878 type: "before_provider_headers";
879 headers: ProviderHeaders;
880}
881
882/** Fired after a provider response is received and before the response stream is consumed. */
883export interface AfterProviderResponseEvent {
884 type: "after_provider_response";
885 status: number;
886 headers: Record<string, string>;
887}
888
889/** Fired for a parsed provider stream event before Pi normalizes it. */
890export interface ProviderStreamEvent {
891 type: "provider_stream_event";
892 provider: ProviderId;
893 api: Api;
894 model: string;
895 data: unknown;
896}
897
898/** Fired after user submits prompt but before agent loop. */
899export interface BeforeAgentStartEvent {
900 type: "before_agent_start";
901 /** The raw user prompt text (after expansion). */
902 prompt: string;
903 /** Images attached to the user prompt, if any. */
904 images?: ImageContent[];
905 /** The current system prompt, rendered from systemPromptOptions and earlier handler changes. */
906 readonly systemPrompt: string;
907 /** Mutable prompt sections. Later handlers observe mutations made by earlier handlers. */
908 systemPromptOptions: NormalizedBuildSystemPromptOptions;
909}
910
911/** Fired when an agent loop starts */
912export interface AgentStartEvent {
913 type: "agent_start";
914}
915
916/** Fired when an agent loop ends */
917export interface AgentEndEvent {
918 type: "agent_end";
919 messages: AgentMessage[];
920}
921
922export type AgentActivityOutcome = "completed" | "aborted" | "error";
923
924export interface CustomEntryDraft {
925 type: "custom";
926 customType: string;
927 data?: unknown;
928}
929
930export interface CustomMessageEntryDraft {
931 type: "custom_message";
932 customType: string;
933 content: string | (TextContent | ImageContent)[];
934 display: boolean;
935 details?: unknown;
936}
937
938export interface ContextEditEntryDraft {
939 type: "context_edit";
940 targetId: string;
941 replacement: ContextEditEntry["replacement"];
942}
943
944export interface CompactionEntryDraft {
945 type: "compaction";
946 summary: string;
947 /** Null creates a self-retaining compaction that keeps no preceding entries. */
948 firstKeptEntryId: string | null;
949 details?: unknown;
950 usage?: Usage;
951}
952
953export type SessionBoundaryDraft =
954 | CustomEntryDraft
955 | CustomMessageEntryDraft
956 | ContextEditEntryDraft
957 | CompactionEntryDraft;
958
959export interface BoundaryContextPreview {
960 contextEntries: ProjectedSessionEntry[];
961 contextMessages: AgentMessage[];
962 llmMessages: Message[];
963 pendingMessages: AgentMessage[];
964 canContinue: boolean;
965}
966
967export interface BoundaryState {
968 entries: SessionBoundaryDraft[];
969 continue: boolean;
970 context: BoundaryContextPreview;
971 outcome: AgentActivityOutcome;
972}
973
974export interface BoundaryResult {
975 entries?: SessionBoundaryDraft[];
976 continue?: boolean;
977}
978
979/** Fired before final settlement. May append entries and ensure one next provider request. */
980export interface AgentBeforeSettleEvent extends BoundaryState {
981 type: "agent_before_settle";
982}
983
984/** Fired after an agent run has fully settled and no automatic retry, compaction, or queued continuation will run. */
985export interface AgentSettledEvent {
986 type: "agent_settled";
987}
988
989export type UIPromptKind = "select" | "confirm" | "input" | "editor" | "custom";
990
991/** Fired when Pi starts waiting on a blocking user-facing extension UI prompt. */
992export interface UIPromptStartEvent {
993 type: "ui_prompt_start";
994 reason: "ui_prompt";
995 kind: UIPromptKind;
996 title?: string;
997}
998
999/** Fired when Pi is no longer waiting on a blocking user-facing extension UI prompt. */
1000export interface UIPromptEndEvent {
1001 type: "ui_prompt_end";
1002 reason: "ui_prompt";
1003 kind: UIPromptKind;
1004 title?: string;
1005}
1006
1007/** Fired at the start of each turn */
1008export interface TurnStartEvent {
1009 type: "turn_start";
1010 turnIndex: number;
1011 timestamp: number;
1012}
1013
1014/** Fired at the end of each turn */
1015export interface TurnEndEvent extends BoundaryState {
1016 type: "turn_end";
1017 turnIndex: number;
1018 message: AgentMessage;
1019 toolResults: ToolResultMessage[];
1020 messageEntryId: string;
1021 toolResultEntryIds: string[];
1022}
1023
1024/** Fired when a message starts (user, assistant, or toolResult) */
1025export interface MessageStartEvent {
1026 type: "message_start";
1027 message: AgentMessage;
1028}
1029
1030/** Fired during assistant message streaming with token-by-token updates */
1031export interface MessageUpdateEvent {
1032 type: "message_update";
1033 message: AgentMessage;
1034 assistantMessageEvent: AssistantMessageEvent;
1035}
1036
1037/** Fired when a message ends */
1038export interface MessageEndEvent {
1039 type: "message_end";
1040 message: AgentMessage;
1041}
1042
1043/** Fired when a tool starts executing */
1044export interface ToolExecutionStartEvent {
1045 type: "tool_execution_start";
1046 toolCallId: string;
1047 toolName: string;
1048 args: any;
1049 /** Set when another tool (for example a codemode script) made this call. */
1050 parentToolCallId?: string;
1051}
1052
1053/** Fired during tool execution with partial/streaming output */
1054export interface ToolExecutionUpdateEvent {
1055 type: "tool_execution_update";
1056 toolCallId: string;
1057 toolName: string;
1058 args: any;
1059 partialResult: any;
1060 /** Set when another tool (for example a codemode script) made this call. */
1061 parentToolCallId?: string;
1062}
1063
1064/** Fired when a tool finishes executing */
1065export interface ToolExecutionEndEvent {
1066 type: "tool_execution_end";
1067 toolCallId: string;
1068 toolName: string;
1069 result: any;
1070 isError: boolean;
1071 /** Set when another tool (for example a codemode script) made this call. */
1072 parentToolCallId?: string;
1073}
1074
1075// ============================================================================
1076// Model Events
1077// ============================================================================
1078
1079export type ModelSelectSource = "set" | "cycle" | "restore";
1080
1081/** Fired when a new model is selected */
1082export interface ModelSelectEvent {
1083 type: "model_select";
1084 model: Model<any>;
1085 previousModel: Model<any> | undefined;
1086 source: ModelSelectSource;
1087}
1088
1089/** Fired when a new thinking level is selected */
1090export interface ThinkingLevelSelectEvent {
1091 type: "thinking_level_select";
1092 level: ThinkingLevel;
1093 previousLevel: ThinkingLevel;
1094}
1095
1096// ============================================================================
1097// User Bash Events
1098// ============================================================================
1099
1100/** Fired when user executes a bash command via ! or !! prefix */
1101export interface UserBashEvent {
1102 type: "user_bash";
1103 /** The command to execute */
1104 command: string;
1105 /** True if !! prefix was used (excluded from LLM context) */
1106 excludeFromContext: boolean;
1107 /** Current working directory */
1108 cwd: string;
1109}
1110
1111// ============================================================================
1112// Input Events
1113// ============================================================================
1114
1115/** Source of user input */
1116export type InputSource = "interactive" | "rpc" | "extension";
1117
1118/** Fired when user input is received, before agent processing */
1119export interface InputEvent {
1120 type: "input";
1121 /** The input text */
1122 text: string;
1123 /** Attached images, if any */
1124 images?: ImageContent[];
1125 /** Where the input came from */
1126 source: InputSource;
1127 /** How the input will be delivered during streaming, or undefined when idle */
1128 streamingBehavior?: "steer" | "followUp";
1129}
1130
1131/** Result from input event handler */
1132export type InputEventResult =
1133 | { action: "continue" }
1134 | { action: "transform"; text: string; images?: ImageContent[] }
1135 | { action: "handled" };
1136
1137// ============================================================================
1138// Tool Events
1139// ============================================================================
1140
1141interface ToolCallEventBase {
1142 type: "tool_call";
1143 /**
1144 * The call's id. For calls another tool made (with `parentToolCallId` set), pi assigns
1145 * `<parent id>/<n>`; such ids never appear as tool calls or tool results in the transcript, only
1146 * in the parent result's `nestedCalls` record.
1147 */
1148 toolCallId: string;
1149 /** Set when another tool (for example a codemode script) issued this call. */
1150 parentToolCallId?: string;
1151}
1152
1153export interface BashToolCallEvent extends ToolCallEventBase {
1154 toolName: "bash";
1155 input: BashToolInput;
1156}
1157
1158export interface PowerShellToolCallEvent extends ToolCallEventBase {
1159 toolName: "powershell";
1160 input: PowerShellToolInput;
1161}
1162
1163export interface ReadToolCallEvent extends ToolCallEventBase {
1164 toolName: "read";
1165 input: ReadToolInput;
1166}
1167
1168export interface EditToolCallEvent extends ToolCallEventBase {
1169 toolName: "edit";
1170 input: EditToolInput;
1171}
1172
1173export interface WriteToolCallEvent extends ToolCallEventBase {
1174 toolName: "write";
1175 input: WriteToolInput;
1176}
1177
1178export interface GrepToolCallEvent extends ToolCallEventBase {
1179 toolName: "grep";
1180 input: GrepToolInput;
1181}
1182
1183export interface FindToolCallEvent extends ToolCallEventBase {
1184 toolName: "find";
1185 input: FindToolInput;
1186}
1187
1188export interface LsToolCallEvent extends ToolCallEventBase {
1189 toolName: "ls";
1190 input: LsToolInput;
1191}
1192
1193export interface CustomToolCallEvent extends ToolCallEventBase {
1194 toolName: string;
1195 input: Record<string, unknown>;
1196}
1197
1198/**
1199 * Fired before a tool executes. Can block.
1200 *
1201 * `event.input` is mutable. Mutate it in place to patch tool arguments before execution.
1202 * Later `tool_call` handlers see earlier mutations. No re-validation is performed after mutation.
1203 */
1204export type ToolCallEvent =
1205 | BashToolCallEvent
1206 | PowerShellToolCallEvent
1207 | ReadToolCallEvent
1208 | EditToolCallEvent
1209 | WriteToolCallEvent
1210 | GrepToolCallEvent
1211 | FindToolCallEvent
1212 | LsToolCallEvent
1213 | CustomToolCallEvent;
1214
1215interface ToolResultEventBase {
1216 type: "tool_result";
1217 /** The call's id; `<parent id>/<n>` for nested calls, see `ToolCallEvent`. */
1218 toolCallId: string;
1219 /** Set when another tool (for example a codemode script) issued this call. */
1220 parentToolCallId?: string;
1221 input: Record<string, unknown>;
1222 content: (TextContent | ImageContent)[];
1223 /**
1224 * Machine-readable result for tools that declare an `outputSchema`. Handlers that redact
1225 * `content` should also replace this; replacing `content` alone drops it.
1226 */
1227 structuredContent?: JsonValue;
1228 isError: boolean;
1229 /** Usage from the tool execution itself, if available. */
1230 usage?: Usage;
1231}
1232
1233export interface BashToolResultEvent extends ToolResultEventBase {
1234 toolName: "bash";
1235 details: BashToolDetails | undefined;
1236}
1237
1238export interface PowerShellToolResultEvent extends ToolResultEventBase {
1239 toolName: "powershell";
1240 details: PowerShellToolDetails | undefined;
1241}
1242
1243export interface ReadToolResultEvent extends ToolResultEventBase {
1244 toolName: "read";
1245 details: ReadToolDetails | undefined;
1246}
1247
1248export interface EditToolResultEvent extends ToolResultEventBase {
1249 toolName: "edit";
1250 details: EditToolDetails | undefined;
1251}
1252
1253export interface WriteToolResultEvent extends ToolResultEventBase {
1254 toolName: "write";
1255 details: undefined;
1256}
1257
1258export interface GrepToolResultEvent extends ToolResultEventBase {
1259 toolName: "grep";
1260 details: GrepToolDetails | undefined;
1261}
1262
1263export interface FindToolResultEvent extends ToolResultEventBase {
1264 toolName: "find";
1265 details: FindToolDetails | undefined;
1266}
1267
1268export interface LsToolResultEvent extends ToolResultEventBase {
1269 toolName: "ls";
1270 details: LsToolDetails | undefined;
1271}
1272
1273export interface CustomToolResultEvent extends ToolResultEventBase {
1274 toolName: string;
1275 details: unknown;
1276}
1277
1278/** Fired after a tool executes. Can modify result. */
1279export type ToolResultEvent =
1280 | BashToolResultEvent
1281 | PowerShellToolResultEvent
1282 | ReadToolResultEvent
1283 | EditToolResultEvent
1284 | WriteToolResultEvent
1285 | GrepToolResultEvent
1286 | FindToolResultEvent
1287 | LsToolResultEvent
1288 | CustomToolResultEvent;
1289
1290// Type guards for ToolResultEvent
1291export function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {
1292 return e.toolName === "bash";
1293}
1294export function isPowerShellToolResult(e: ToolResultEvent): e is PowerShellToolResultEvent {
1295 return e.toolName === "powershell";
1296}
1297export function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {
1298 return e.toolName === "read";
1299}
1300export function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {
1301 return e.toolName === "edit";
1302}
1303export function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {
1304 return e.toolName === "write";
1305}
1306export function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {
1307 return e.toolName === "grep";
1308}
1309export function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {
1310 return e.toolName === "find";
1311}
1312export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {
1313 return e.toolName === "ls";
1314}
1315
1316/**
1317 * Type guard for narrowing ToolCallEvent by tool name.
1318 *
1319 * Built-in tools narrow automatically (no type params needed):
1320 * ```ts
1321 * if (isToolCallEventType("bash", event)) {
1322 * event.input.command; // string
1323 * }
1324 * ```
1325 *
1326 * Custom tools require explicit type parameters:
1327 * ```ts
1328 * if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
1329 * event.input.action; // typed
1330 * }
1331 * ```
1332 *
1333 * Note: Direct narrowing via `event.toolName === "bash"` doesn't work because
1334 * CustomToolCallEvent.toolName is `string` which overlaps with all literals.
1335 */
1336export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent;
1337export function isToolCallEventType(toolName: "powershell", event: ToolCallEvent): event is PowerShellToolCallEvent;
1338export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent;
1339export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent;
1340export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent;
1341export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent;
1342export function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent;
1343export function isToolCallEventType(toolName: "ls", event: ToolCallEvent): event is LsToolCallEvent;
1344export function isToolCallEventType<TName extends string, TInput extends Record<string, unknown>>(
1345 toolName: TName,
1346 event: ToolCallEvent,
1347): event is ToolCallEvent & { toolName: TName; input: TInput };
1348export function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean {
1349 return event.toolName === toolName;
1350}
1351
1352/** Union of all event types */
1353export type ExtensionEvent =
1354 | ProjectTrustEvent
1355 | ResourcesDiscoverEvent
1356 | McpServersChangeEvent
1357 | SessionEvent
1358 | ContextEvent
1359 | ContextWithSystemEvent
1360 | CacheWarmingDecisionEvent
1361 | BeforeProviderRequestEvent
1362 | BeforeProviderHeadersEvent
1363 | AfterProviderResponseEvent
1364 | ProviderStreamEvent
1365 | BeforeAgentStartEvent
1366 | AgentStartEvent
1367 | AgentEndEvent
1368 | AgentBeforeSettleEvent
1369 | AgentSettledEvent
1370 | UIPromptStartEvent
1371 | UIPromptEndEvent
1372 | TurnStartEvent
1373 | TurnEndEvent
1374 | MessageStartEvent
1375 | MessageUpdateEvent
1376 | MessageEndEvent
1377 | ToolExecutionStartEvent
1378 | ToolExecutionUpdateEvent
1379 | ToolExecutionEndEvent
1380 | ModelSelectEvent
1381 | ThinkingLevelSelectEvent
1382 | UserBashEvent
1383 | InputEvent
1384 | ToolCallEvent
1385 | ToolResultEvent;
1386
1387// ============================================================================
1388// Event Results
1389// ============================================================================
1390
1391export interface ContextEventResult {
1392 messages?: AgentMessage[];
1393}
1394
1395export type TurnEndEventResult = BoundaryResult;
1396export type AgentBeforeSettleEventResult = BoundaryResult;
1397
1398export type BeforeProviderRequestEventResult = unknown;
1399
1400export type { CacheWarmingDecisionEvent, CacheWarmingDecisionEventResult } from "../cache-warmer.ts";
1401
1402export interface ToolCallEventResult {
1403 /** Block tool execution. To modify arguments, mutate `event.input` in place instead. */
1404 block?: boolean;
1405 reason?: string;
1406 /**
1407 * Hint that the agent should stop after the current tool batch when this call is blocked.
1408 * Early termination only happens when every finalized tool result in the batch sets this to true.
1409 */
1410 terminate?: boolean;
1411}
1412
1413/** Result from user_bash event handler */
1414export type UserBashEventResult =
1415 | {
1416 /** Custom operations to use for execution */
1417 operations: BashOperations;
1418 result?: never;
1419 }
1420 | {
1421 operations?: never;
1422 /** Full replacement: extension handled execution, use this result */
1423 result: BashResult;
1424 };
1425
1426/**
1427 * Changes a `tool_result` handler makes. Omitted fields stay as they are, except that replacing
1428 * `content` without returning `structuredContent` drops the structured content, because it may no
1429 * longer match. Return it along with `content` to keep it.
1430 */
1431export interface ToolResultEventResult {
1432 content?: (TextContent | ImageContent)[];
1433 details?: unknown;
1434 structuredContent?: JsonValue;
1435 isError?: boolean;
1436 usage?: Usage;
1437}
1438
1439export interface MessageEndEventResult {
1440 /** Replace the finalized message. The replacement must keep the original message role. */
1441 message?: AgentMessage;
1442}
1443
1444export interface BeforeAgentStartEventResult {
1445 message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
1446 /** Replace the complete system prompt for this turn. Later handlers observe this exact override. */
1447 systemPrompt?: string;
1448}
1449
1450export interface SessionBeforeSwitchResult {
1451 cancel?: boolean;
1452}
1453
1454export interface SessionBeforeForkResult {
1455 cancel?: boolean;
1456 skipConversationRestore?: boolean;
1457}
1458
1459export interface SessionBeforeCompactResult {
1460 cancel?: boolean;
1461 compaction?: CompactionResult;
1462}
1463
1464export interface SessionBeforeTreeResult {
1465 cancel?: boolean;
1466 summary?: {
1467 summary: string;
1468 details?: unknown;
1469 usage?: Usage;
1470 };
1471 /** Override custom instructions for summarization */
1472 customInstructions?: string;
1473 /** Override whether customInstructions replaces the default prompt */
1474 replaceInstructions?: boolean;
1475 /** Override label to attach to the branch summary entry */
1476 label?: string;
1477}
1478
1479// ============================================================================
1480// Message and Entry Rendering
1481// ============================================================================
1482
1483export interface MessageRenderOptions {
1484 expanded: boolean;
1485 /** Horizontal padding configured by the outputPad setting. */
1486 outputPad: number;
1487}
1488
1489export interface MarkdownTransformContext {
1490 messageType: "user" | "assistant" | "assistant-thinking";
1491 isStreaming: boolean;
1492 availableWidth: number;
1493}
1494
1495export type MarkdownTransformer = (markdown: string, context: MarkdownTransformContext) => string;
1496
1497export interface EntryRenderOptions {
1498 expanded: boolean;
1499}
1500
1501export type MessageRenderer<T = unknown> = (
1502 message: CustomMessage<T>,
1503 options: MessageRenderOptions,
1504 theme: Theme,
1505) => Component | undefined;
1506
1507export type EntryRenderer<T = unknown> = (
1508 entry: CustomEntry<T>,
1509 options: EntryRenderOptions,
1510 theme: Theme,
1511) => Component | undefined;
1512
1513// ============================================================================
1514// Command Registration
1515// ============================================================================
1516
1517export interface RegisteredCommand {
1518 name: string;
1519 sourceInfo: SourceInfo;
1520 description?: string;
1521 getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
1522 handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
1523}
1524
1525export interface ResolvedCommand extends RegisteredCommand {
1526 invocationName: string;
1527}
1528
1529// ============================================================================
1530// Extension API
1531// ============================================================================
1532
1533/** Handler function type for events */
1534// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements
1535export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;
1536
1537/**
1538 * ExtensionAPI passed to extension factory functions.
1539 */
1540export interface ExtensionAPI {
1541 // =========================================================================
1542 // Event Subscription
1543 // =========================================================================
1544
1545 on(event: "project_trust", handler: ProjectTrustHandler): () => void;
1546 on(
1547 event: "resources_discover",
1548 handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>,
1549 ): () => void;
1550 on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): () => void;
1551 on(event: "session_info_changed", handler: ExtensionHandler<SessionInfoChangedEvent>): () => void;
1552 on(
1553 event: "session_before_switch",
1554 handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>,
1555 ): () => void;
1556 on(
1557 event: "session_before_fork",
1558 handler: ExtensionHandler<SessionBeforeForkEvent, SessionBeforeForkResult>,
1559 ): () => void;
1560 on(
1561 event: "session_before_compact",
1562 handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
1563 ): () => void;
1564 on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): () => void;
1565 on(event: "session_compact_failed", handler: ExtensionHandler<SessionCompactFailedEvent>): () => void;
1566 on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): () => void;
1567 on(event: "mcp_servers_change", handler: ExtensionHandler<McpServersChangeEvent>): () => void;
1568 on(
1569 event: "session_before_tree",
1570 handler: ExtensionHandler<SessionBeforeTreeEvent, SessionBeforeTreeResult>,
1571 ): () => void;
1572 on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>): () => void;
1573 on(event: "context", handler: ExtensionHandler<ContextEvent, ContextEventResult>): () => void;
1574 on(event: "context_with_system", handler: ExtensionHandler<ContextWithSystemEvent, ContextEventResult>): () => void;
1575 on(
1576 event: "cache_warming_decision",
1577 handler: ExtensionHandler<CacheWarmingDecisionEvent, CacheWarmingDecisionEventResult>,
1578 ): () => void;
1579 on(
1580 event: "before_provider_request",
1581 handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>,
1582 ): () => void;
1583 on(event: "before_provider_headers", handler: ExtensionHandler<BeforeProviderHeadersEvent>): () => void;
1584 on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>): () => void;
1585 on(event: "provider_stream_event", handler: ExtensionHandler<ProviderStreamEvent>): () => void;
1586 on(
1587 event: "before_agent_start",
1588 handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>,
1589 ): () => void;
1590 on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): () => void;
1591 on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): () => void;
1592 on(
1593 event: "agent_before_settle",
1594 handler: ExtensionHandler<AgentBeforeSettleEvent, AgentBeforeSettleEventResult>,
1595 ): () => void;
1596 on(event: "agent_settled", handler: ExtensionHandler<AgentSettledEvent>): () => void;
1597 on(event: "ui_prompt_start", handler: ExtensionHandler<UIPromptStartEvent>): () => void;
1598 on(event: "ui_prompt_end", handler: ExtensionHandler<UIPromptEndEvent>): () => void;
1599 on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): () => void;
1600 on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent, TurnEndEventResult>): () => void;
1601 on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): () => void;
1602 on(event: "message_update", handler: ExtensionHandler<MessageUpdateEvent>): () => void;
1603 on(event: "message_end", handler: ExtensionHandler<MessageEndEvent, MessageEndEventResult>): () => void;
1604 on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): () => void;
1605 on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): () => void;
1606 on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): () => void;
1607 on(event: "model_select", handler: ExtensionHandler<ModelSelectEvent>): () => void;
1608 on(event: "thinking_level_select", handler: ExtensionHandler<ThinkingLevelSelectEvent>): () => void;
1609 on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): () => void;
1610 on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): () => void;
1611 on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): () => void;
1612 on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): () => void;
1613
1614 // =========================================================================
1615 // Tool Registration
1616 // =========================================================================
1617
1618 /** Register a tool that the LLM can call. */
1619 registerTool<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>(
1620 tool: ToolDefinition<TParams, TDetails, TState>,
1621 ): void;
1622
1623 // =========================================================================
1624 // Command, Shortcut, Flag Registration
1625 // =========================================================================
1626
1627 /** Register a custom command. */
1628 registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void;
1629
1630 /** Register a keyboard shortcut. */
1631 registerShortcut(
1632 shortcut: KeyId,
1633 options: {
1634 description?: string;
1635 handler: (ctx: ExtensionContext) => Promise<void> | void;
1636 },
1637 ): void;
1638
1639 /** Register a CLI flag. */
1640 registerFlag(
1641 name: string,
1642 options:
1643 | {
1644 description?: string;
1645 type: "boolean";
1646 default?: boolean;
1647 }
1648 | {
1649 description?: string;
1650 type: "string";
1651 default?: string;
1652 },
1653 ): void;
1654
1655 /** Get the value of a registered CLI flag. */
1656 getFlag(name: string): boolean | string | undefined;
1657
1658 // =========================================================================
1659 // Message Rendering
1660 // =========================================================================
1661
1662 /** Register a custom renderer for CustomMessageEntry. */
1663 registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
1664
1665 /** Register a transformer for user and assistant Markdown before Pi renders it in the interactive transcript. */
1666 registerMarkdownTransformer(transformer: MarkdownTransformer): void;
1667
1668 /** Register a custom renderer for CustomEntry. Custom entries do not participate in LLM context. */
1669 registerEntryRenderer<T = unknown>(customType: string, renderer: EntryRenderer<T>): void;
1670
1671 // =========================================================================
1672 // Actions
1673 // =========================================================================
1674
1675 /** Send a custom message to the session. */
1676 sendMessage<T = unknown>(
1677 message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
1678 options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
1679 ): void;
1680
1681 /**
1682 * Send a user message to the agent. Always triggers a turn.
1683 * When the agent is streaming, use deliverAs to specify how to queue the message.
1684 * Set expandPromptTemplates to dispatch extension commands and expand skill commands and prompt templates.
1685 */
1686 sendUserMessage(
1687 content: string | (TextContent | ImageContent)[],
1688 options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
1689 ): void;
1690
1691 /** Append a custom entry to the session for state persistence (not sent to LLM). */
1692 appendEntry<T = unknown>(customType: string, data?: T): void;
1693
1694 // =========================================================================
1695 // Session Metadata
1696 // =========================================================================
1697
1698 /** Set the session display name (shown in session selector). */
1699 setSessionName(name: string): void;
1700
1701 /** Get the current session name, if set. */
1702 getSessionName(): string | undefined;
1703
1704 /** Set or clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */
1705 setLabel(entryId: string, label: string | undefined): void;
1706
1707 /** Execute a shell command. */
1708 exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
1709
1710 /** Get the names of the active tools, which are the tools declared to the model. */
1711 getActiveTools(): string[];
1712
1713 /** Get all configured tools with parameter schema, prompt guidelines, exposure, and source metadata. */
1714 getAllTools(): ToolInfo[];
1715
1716 /** Get a copy of the effective settings (global and project settings merged, with overrides). */
1717 getSettings(): Settings;
1718
1719 /**
1720 * Set the active tools by name. Unknown and `hidden` tools are ignored. Tools with `codemode` or
1721 * `deferred` exposure stay callable from codemode scripts whether active or not.
1722 */
1723 setActiveTools(toolNames: string[]): void;
1724
1725 /** Get available slash commands in the current session. */
1726 getCommands(): SlashCommandInfo[];
1727
1728 // =========================================================================
1729 // Model and Thinking Level
1730 // =========================================================================
1731
1732 /**
1733 * Set the model for the current session without changing the configured default for new sessions.
1734 * Returns false if authentication is not configured for the model's provider.
1735 */
1736 setModel(model: Model<any>): Promise<boolean>;
1737
1738 /** Get current thinking level. */
1739 getThinkingLevel(): ThinkingLevel;
1740
1741 /**
1742 * Set the thinking level (clamped to model capabilities) for the current session without changing the configured default
1743 * for new sessions.
1744 */
1745 setThinkingLevel(level: ThinkingLevel): void;
1746
1747 // =========================================================================
1748 // Provider Registration
1749 // =========================================================================
1750
1751 /**
1752 * Register or override a model provider.
1753 *
1754 * If `models` is provided: replaces all existing models for this provider.
1755 * If only `baseUrl` is provided: overrides the URL for existing models.
1756 * If `oauth` is provided: registers OAuth provider for /login support.
1757 * If `streamSimple` is provided: registers a custom API stream handler.
1758 *
1759 * During initial extension load this call is queued and applied once the
1760 * runner has bound its context. After that it takes effect immediately, so
1761 * it is safe to call from command handlers or event callbacks without
1762 * requiring a `/reload`.
1763 *
1764 * @example
1765 * // Register a new provider with custom models
1766 * pi.registerProvider("my-proxy", {
1767 * baseUrl: "https://proxy.example.com",
1768 * apiKey: "$PROXY_API_KEY",
1769 * api: "anthropic-messages",
1770 * models: [
1771 * {
1772 * id: "claude-sonnet-4-20250514",
1773 * name: "Claude 4 Sonnet (proxy)",
1774 * reasoning: false,
1775 * input: ["text", "image"],
1776 * cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1777 * contextWindow: 200000,
1778 * maxTokens: 16384
1779 * }
1780 * ]
1781 * });
1782 *
1783 * @example
1784 * // Override baseUrl for an existing provider
1785 * pi.registerProvider("anthropic", {
1786 * baseUrl: "https://proxy.example.com"
1787 * });
1788 *
1789 * @example
1790 * // Register provider with OAuth support
1791 * pi.registerProvider("corporate-ai", {
1792 * baseUrl: "https://ai.corp.com",
1793 * api: "openai-responses",
1794 * models: [...],
1795 * oauth: {
1796 * name: "Corporate AI (SSO)",
1797 * async login(callbacks) { ... },
1798 * async refreshToken(credentials) { ... },
1799 * getApiKey(credentials) { return credentials.access; }
1800 * }
1801 * });
1802 */
1803 registerProvider(provider: Provider): void;
1804 registerProvider(name: string, config: ProviderConfig): void;
1805
1806 /**
1807 * Unregister a previously registered provider.
1808 *
1809 * Removes all models belonging to the named provider and restores any
1810 * built-in models that were overridden by it. Has no effect if the provider
1811 * is not currently registered.
1812 *
1813 * Like `registerProvider`, this takes effect immediately when called after
1814 * the initial load phase.
1815 *
1816 * @example
1817 * pi.unregisterProvider("my-proxy");
1818 */
1819 unregisterProvider(name: string): void;
1820
1821 // =========================================================================
1822 // MCP Servers
1823 // =========================================================================
1824
1825 /**
1826 * Register an MCP server for this session, with the same config as an `mcpServers` entry in
1827 * `mcp.json`. The server connects next to the configured servers: on `session_start` when
1828 * registered during extension load, right away when registered later. Registering a name again
1829 * replaces the extension's earlier registration.
1830 *
1831 * The registration is not saved; register again on every load. A server of the same name in
1832 * `mcp.json` takes precedence. Throws for invalid configs and for names another extension
1833 * registered. When no loaded extension handles MCP servers (for example because another MCP
1834 * extension replaced the built-in one), the registration is reported as an extension error.
1835 *
1836 * @example
1837 * pi.registerMcpServer("jira", { url: "https://mcp.example.com/jira" });
1838 */
1839 registerMcpServer(name: string, config: McpServerConfig): void;
1840
1841 /** Remove an MCP server this extension registered and close its connection. */
1842 unregisterMcpServer(name: string): void;
1843
1844 /** Every MCP server registered by extensions. For extensions that connect MCP servers. */
1845 getMcpServers(): RegisteredMcpServer[];
1846
1847 /**
1848 * Register a virtual model: a selectable catalog entry that routes each request to a physical
1849 * model. The selection (`ctx.model`, `model_change` entries) names the virtual model; assistant
1850 * messages record the physical model and thinking level the router picked.
1851 *
1852 * `provider` may be any provider id, including one with physical models, and may list several
1853 * virtual models. Registering the same provider and id again replaces the virtual model. See
1854 * docs/virtual-models.md.
1855 */
1856 registerVirtualModel<TState = unknown>(model: ExtensionVirtualModel<TState>): void;
1857
1858 /** Remove a virtual model registered with `registerVirtualModel()`. */
1859 unregisterVirtualModel(provider: string, id: string): void;
1860
1861 /** Shared event bus for extension communication. */
1862 events: EventBus;
1863}
1864
1865// ============================================================================
1866// Provider Registration Types
1867// ============================================================================
1868
1869/** Virtual model registered via pi.registerVirtualModel(). */
1870export interface ExtensionVirtualModel<TState = unknown> extends Omit<VirtualModelDefinition<TState>, "route"> {
1871 /** Like `VirtualModelDefinition.route`, with an extension context. */
1872 route(request: ModelRouteRequest<TState>, ctx: ExtensionContext): ModelRoute<TState> | Promise<ModelRoute<TState>>;
1873}
1874
1875/** Configuration for registering a provider via pi.registerProvider(). */
1876export interface ProviderConfig {
1877 /** Display name for the provider in UI. */
1878 name?: string;
1879 /** Base URL for the API endpoint. Required when defining models. */
1880 baseUrl?: string;
1881 /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or leading !command. Required when defining models (unless oauth provided). */
1882 apiKey?: string;
1883 /** API type. Required at provider or model level when defining models. */
1884 api?: Api;
1885 /**
1886 * Optional streamSimple handler for custom APIs.
1887 * The context is a normalized transcript: read the prompt and tools from its system messages
1888 * (`getCurrentSystemPrompt(context.messages)`, `getCurrentTools(context.messages)`).
1889 * Implementations must invoke `options.onPayload` before sending the provider request and use any
1890 * returned replacement payload. They must invoke `options.onResponse` after receiving the response
1891 * and before consuming its body, matching built-in providers. Implementations may invoke
1892 * `options.onProviderStreamEvent(data, model)` with parsed stream events before normalization.
1893 * Event data is adapter-owned and must be treated as read-only.
1894 */
1895 streamSimple?: (
1896 model: Model<Api>,
1897 context: TranscriptContext,
1898 options?: SimpleStreamOptions,
1899 ) => AssistantMessageEventStream;
1900 /** Image-generation implementations keyed by image API. */
1901 images?: Partial<Record<ImageApi, ProviderImages>>;
1902 /** Classifier implementations keyed by classifier API. */
1903 classifiers?: Partial<Record<ClassifierApi, ProviderClassifier>>;
1904 /** Custom headers to include in requests. */
1905 headers?: Record<string, string>;
1906 /** If true, adds Authorization: Bearer header with the resolved API key. */
1907 authHeader?: boolean;
1908 /** Models to register. If provided, replaces all existing models for this provider. */
1909 models?: ProviderModelConfig[];
1910 /**
1911 * Refresh this provider's model list. The returned list replaces extension-provided models.
1912 * Use context.publish({ persist: entry }) when the catalog should persist across sessions.
1913 */
1914 refreshModels?(context: RefreshModelsContext): Promise<ProviderModelConfig[]>;
1915 /** OAuth provider for /login support. The `id` is set automatically from the provider name. */
1916 oauth?: {
1917 /** Display name for the provider in login UI. */
1918 name: string;
1919 /** Whether access through this auth method is backed by a provider subscription. */
1920 isSubscription?: boolean;
1921 /** @deprecated Retained for source compatibility; canonical auth flows ignore it. */
1922 usesCallbackServer?: boolean;
1923 /** Run the login flow, return credentials to persist. */
1924 login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
1925 /** Refresh expired credentials, return updated credentials to persist. */
1926 refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;
1927 /** Convert credentials to API key string for the provider. */
1928 getApiKey(credentials: OAuthCredentials): string;
1929 /** Legacy synchronous credential-dependent model projection. */
1930 modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
1931 };
1932}
1933
1934interface ProviderModelConfigBase {
1935 /** Model ID. */
1936 id: string;
1937 /** Display name. */
1938 name: string;
1939 /** API type override for this model. */
1940 api?: string;
1941 /** API endpoint URL override for this model. */
1942 baseUrl?: string;
1943 /** Supported input types. */
1944 input: ("text" | "image")[];
1945 /** Provider input limits and cache-safe image preprocessing metadata. */
1946 inputLimits?: AnyModel["inputLimits"];
1947 /** Per-million-token cost rates and optional request-wide input pricing tiers. */
1948 cost: AnyModel["cost"];
1949 /** Custom headers for this model. */
1950 headers?: Record<string, string>;
1951}
1952
1953/** Chat model configuration. Omitted `type` is normalized to `"chat"`. */
1954export interface ProviderChatModelConfig extends ProviderModelConfigBase {
1955 type?: "chat";
1956 api?: Api;
1957 /** Whether the model supports extended thinking. */
1958 reasoning: boolean;
1959 /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */
1960 thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
1961 /** Best-effort prompt cache lifetime in seconds per retention tier. Unset disables cache warming. */
1962 promptCache?: Model<Api>["promptCache"];
1963 /** Maximum context window size in tokens. */
1964 contextWindow: number;
1965 /** Maximum output tokens. */
1966 maxTokens: number;
1967 samplingParams?: Record<string, unknown>;
1968 /** OpenAI compatibility settings. */
1969 compat?: Model<Api>["compat"];
1970}
1971
1972/** Image-generation model configuration. */
1973export interface ProviderImageModelConfig extends ProviderModelConfigBase {
1974 type: "image";
1975 api?: ImageApi;
1976 output: ("text" | "image")[];
1977}
1978
1979/** Structured classifier model configuration. */
1980export interface ProviderClassifierModelConfig extends ProviderModelConfigBase {
1981 type: "classifier";
1982 api?: ClassifierApi;
1983 contextWindow: number;
1984}
1985
1986/** Configuration for a model within a provider. */
1987export type ProviderModelConfig = ProviderChatModelConfig | ProviderImageModelConfig | ProviderClassifierModelConfig;
1988
1989/** Extension factory function type. Supports both sync and async initialization. */
1990export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
1991
1992export type InlineExtension =
1993 | ExtensionFactory
1994 | {
1995 /**
1996 * Display name shown as `<inline:name>` in the startup Extensions list and errors. With
1997 * `builtin`, the extension is named `builtin:name` in errors and diagnostics.
1998 */
1999 name: string;
2000 factory: ExtensionFactory;
2001 /** Omit this extension from the startup Extensions list. */
2002 hidden?: boolean;
2003 /**
2004 * Leave this extension out when another extension registers a tool, command, or flag with a
2005 * name it registers during loading, instead of reporting a conflict. The CLI's built-in MCP,
2006 * codemode, and tool search extensions use it, so for example an MCP extension that registers
2007 * `/mcp` replaces the built-in MCP support. The factory still runs, so it should only register
2008 * tools, commands, flags, and event handlers.
2009 */
2010 replaceable?: boolean;
2011 /**
2012 * Supply the code of the `builtin:<name>` extension instead of loading as an inline extension.
2013 * `builtin:<name>` is an extension resource like a file: it loads by default, `pi config` lists
2014 * it, `-builtin:<name>` in the `extensions` setting and `--no-extensions` disable it, and
2015 * `-e builtin:<name>` loads it explicitly. It is hidden from the startup Extensions list and
2016 * loads after project trust is resolved, so it cannot handle `project_trust`. The CLI's built-in
2017 * extensions use it.
2018 */
2019 builtin?: boolean;
2020 };
2021
2022// ============================================================================
2023// Loaded Extension Types
2024// ============================================================================
2025
2026export interface RegisteredTool {
2027 definition: ToolDefinition;
2028 sourceInfo: SourceInfo;
2029}
2030
2031export interface ExtensionFlag {
2032 name: string;
2033 description?: string;
2034 type: "boolean" | "string";
2035 default?: boolean | string;
2036 extensionPath: string;
2037}
2038
2039export interface ExtensionShortcut {
2040 shortcut: KeyId;
2041 description?: string;
2042 handler: (ctx: ExtensionContext) => Promise<void> | void;
2043 extensionPath: string;
2044}
2045
2046type HandlerFn = (...args: unknown[]) => Promise<unknown>;
2047
2048export type SendMessageHandler = <T = unknown>(
2049 message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
2050 options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
2051) => void;
2052
2053export type SendUserMessageHandler = (
2054 content: string | (TextContent | ImageContent)[],
2055 options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
2056) => void;
2057
2058export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
2059
2060export type SetSessionNameHandler = (name: string) => void;
2061
2062export type GetSessionNameHandler = () => string | undefined;
2063
2064export type GetActiveToolsHandler = () => string[];
2065
2066/** Tool info with name, description, parameter schema, prompt guidelines, and source metadata. */
2067export type ToolInfo = Pick<ToolDefinition, "name" | "description" | "parameters" | "promptGuidelines"> & {
2068 exposure: ToolExposure;
2069 namespace?: ToolNamespace;
2070 annotations?: ToolAnnotations;
2071 sourceInfo: SourceInfo;
2072};
2073
2074export type GetAllToolsHandler = () => ToolInfo[];
2075
2076export type GetSettingsHandler = () => Settings;
2077
2078export type GetCommandsHandler = () => SlashCommandInfo[];
2079
2080export type SetActiveToolsHandler = (toolNames: string[]) => void;
2081
2082export type RefreshToolsHandler = () => void;
2083
2084export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
2085
2086export type GetThinkingLevelHandler = () => ThinkingLevel;
2087
2088export type SetThinkingLevelHandler = (level: ThinkingLevel) => void;
2089
2090export type SetLabelHandler = (entryId: string, label: string | undefined) => void;
2091
2092/**
2093 * Shared state created by loader, used during registration and runtime.
2094 * Contains flag values (defaults set during registration, CLI values set after).
2095 */
2096export interface ExtensionRuntimeState {
2097 flagValues: Map<string, boolean | string>;
2098 /** Legacy provider-config registrations queued during extension loading, processed when runner binds. */
2099 pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; extensionPath: string }>;
2100 /** Native pi-ai provider registrations queued during extension loading, processed when runner binds. */
2101 pendingNativeProviderRegistrations: Array<{ provider: Provider; extensionPath: string }>;
2102 /** Virtual model registrations queued during extension loading, processed when runner binds. */
2103 pendingVirtualModelRegistrations: Array<{ definition: VirtualModelDefinition; extensionPath: string }>;
2104 /** Create an extension context. Throws before the runner binds. */
2105 createContext: () => ExtensionContext;
2106 /** Throws when this extension instance is stale after runtime replacement. */
2107 assertActive: () => void;
2108 /** Marks this extension instance as stale after runtime replacement or reload. */
2109 invalidate: (message?: string) => void;
2110 /** Retain an event-bus subscription until this runtime is invalidated. */
2111 trackEventBusSubscription: (unsubscribe: () => void) => () => void;
2112 /**
2113 * Register or unregister a provider.
2114 *
2115 * Before bindCore(): queues registrations / removes from queue.
2116 * After bindCore(): calls ModelRegistry directly for immediate effect.
2117 */
2118 registerProvider: (name: string, config: ProviderConfig, extensionPath?: string) => void;
2119 registerNativeProvider: (provider: Provider, extensionPath?: string) => void;
2120 unregisterProvider: (name: string, extensionPath?: string) => void;
2121 /** Servers registered with `pi.registerMcpServer()`. */
2122 mcpServers: McpServerRegistry;
2123 registerVirtualModel: (definition: VirtualModelDefinition, extensionPath?: string) => void;
2124 unregisterVirtualModel: (provider: string, id: string) => void;
2125}
2126
2127/**
2128 * Action implementations for pi.* API methods.
2129 * Provided to runner.initialize(), copied into the shared runtime.
2130 */
2131export interface ExtensionActions {
2132 sendMessage: SendMessageHandler;
2133 sendUserMessage: SendUserMessageHandler;
2134 appendEntry: AppendEntryHandler;
2135 setSessionName: SetSessionNameHandler;
2136 getSessionName: GetSessionNameHandler;
2137 setLabel: SetLabelHandler;
2138 getActiveTools: GetActiveToolsHandler;
2139 getAllTools: GetAllToolsHandler;
2140 getSettings: GetSettingsHandler;
2141 setActiveTools: SetActiveToolsHandler;
2142 refreshTools: RefreshToolsHandler;
2143 getCommands: GetCommandsHandler;
2144 setModel: SetModelHandler;
2145 getThinkingLevel: GetThinkingLevelHandler;
2146 setThinkingLevel: SetThinkingLevelHandler;
2147}
2148
2149/**
2150 * Actions for ExtensionContext (ctx.* in event handlers).
2151 * Required by all modes.
2152 */
2153export interface ExtensionContextActions {
2154 getModel: () => Model<any> | undefined;
2155 getScopedModels: () => readonly ScopedModel[];
2156 isIdle: () => boolean;
2157 isProjectTrusted: () => boolean;
2158 getSignal: () => AbortSignal | undefined;
2159 abort: () => void;
2160 hasPendingMessages: () => boolean;
2161 shutdown: () => void;
2162 getContextUsage: () => ContextUsage | undefined;
2163 compact: (options?: CompactOptions) => void;
2164 getSystemPrompt: () => string;
2165 getSystemPromptOptions?: () => BuildSystemPromptOptions;
2166 /** Backs `ExtensionToolContext.executeTool()`. Without it, nested calls fail. */
2167 executeTool?: (
2168 callerId: string,
2169 name: string,
2170 args: unknown,
2171 options: ExecuteToolOptions,
2172 ) => Promise<AgentToolCallOutcome>;
2173 /** Backs `ExtensionToolContext.tools`. */
2174 getCallableTools?: () => readonly AgentTool[];
2175}
2176
2177/**
2178 * Actions for ExtensionCommandContext (ctx.* in command handlers).
2179 * Only needed for interactive mode where extension commands are invokable.
2180 */
2181export interface ExtensionCommandContextActions {
2182 waitForIdle: () => Promise<void>;
2183 newSession: (options?: {
2184 parentSession?: string;
2185 setup?: (sessionManager: SessionManager) => Promise<void>;
2186 withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
2187 }) => Promise<{ cancelled: boolean }>;
2188 fork: (
2189 entryId: string,
2190 options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
2191 ) => Promise<{ cancelled: boolean }>;
2192 navigateTree: (
2193 targetId: string,
2194 options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
2195 ) => Promise<{ cancelled: boolean }>;
2196 switchSession: (
2197 sessionPath: string,
2198 options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
2199 ) => Promise<{ cancelled: boolean }>;
2200 reload: () => Promise<void>;
2201}
2202
2203/**
2204 * Full runtime = state + actions.
2205 * Created by loader with throwing action stubs, completed by runner.initialize().
2206 */
2207export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {}
2208
2209/** Loaded extension with all registered items. */
2210export interface Extension {
2211 path: string;
2212 resolvedPath: string;
2213 hidden?: boolean;
2214 /** See {@link InlineExtension}. */
2215 replaceable?: boolean;
2216 sourceInfo: SourceInfo;
2217 handlers: Map<string, HandlerFn[]>;
2218 tools: Map<string, RegisteredTool>;
2219 messageRenderers: Map<string, MessageRenderer>;
2220 markdownTransformer?: MarkdownTransformer;
2221 entryRenderers?: Map<string, EntryRenderer>;
2222 commands: Map<string, RegisteredCommand>;
2223 flags: Map<string, ExtensionFlag>;
2224 shortcuts: Map<KeyId, ExtensionShortcut>;
2225}
2226
2227/** Result of loading extensions. */
2228export interface LoadExtensionsResult {
2229 extensions: Extension[];
2230 errors: Array<{ path: string; error: string }>;
2231 warnings?: Array<{ path: string; warning: string }>;
2232 /** Shared runtime - actions are throwing stubs until runner.initialize() */
2233 runtime: ExtensionRuntime;
2234}
2235
2236// ============================================================================
2237// Extension Error
2238// ============================================================================
2239
2240export interface ExtensionError {
2241 extensionPath: string;
2242 event: string;
2243 error: string;
2244 stack?: string;
2245}