ππ Agent

extensions-types.ts

58 KB1695 lines
extensions-types.ts
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 AgentToolResult,
14 AgentToolUpdateCallback,
15 ThinkingLevel,
16 ToolExecutionMode,
17} from "@earendil-works/pi-agent-core";
18import type {
19 Api,
20 AssistantMessageEvent,
21 AssistantMessageEventStream,
22 ConstrainedSamplingConfig,
23 Context,
24 ImageContent,
25 Model,
26 OAuthCredentials,
27 OAuthLoginCallbacks,
28 Provider,
29 ProviderHeaders,
30 RefreshModelsContext,
31 SimpleStreamOptions,
32 TextContent,
33 ToolResultMessage,
34 Usage,
35} from "@earendil-works/pi-ai";
36import type {
37 AutocompleteItem,
38 AutocompleteProvider,
39 Component,
40 EditorComponent,
41 EditorTheme,
42 KeyId,
43 OverlayHandle,
44 OverlayOptions,
45 TUI,
46} from "@earendil-works/pi-tui";
47import type { Static, TSchema } from "typebox";
48import type { Theme } from "../../modes/interactive/theme/theme.ts";
49import type { BashResult } from "../bash-executor.ts";
50import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts";
51import type { EventBus } from "../event-bus.ts";
52import type { ExecOptions, ExecResult } from "../exec.ts";
53import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts";
54import type { KeybindingsManager } from "../keybindings.ts";
55import type { CustomMessage } from "../messages.ts";
56import type { ModelRegistry } from "../model-registry.ts";
57import type {
58 BranchSummaryEntry,
59 CompactionEntry,
60 CustomEntry,
61 ReadonlySessionManager,
62 SessionEntry,
63 SessionManager,
64} from "../session-manager.ts";
65import type { SlashCommandInfo } from "../slash-commands.ts";
66import type { SourceInfo } from "../source-info.ts";
67import type { BuildSystemPromptOptions } from "../system-prompt.ts";
68import type { BashOperations } from "../tools/bash.ts";
69import type { EditToolDetails } from "../tools/edit.ts";
70import type {
71 BashToolDetails,
72 BashToolInput,
73 EditToolInput,
74 FindToolDetails,
75 FindToolInput,
76 GrepToolDetails,
77 GrepToolInput,
78 LsToolDetails,
79 LsToolInput,
80 ReadToolDetails,
81 ReadToolInput,
82 WriteToolInput,
83} from "../tools/index.ts";
84
85export type { ExecOptions, ExecResult } from "../exec.ts";
86export type { BuildSystemPromptOptions } from "../system-prompt.ts";
87export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode };
88export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts";
89
90// ============================================================================
91// UI Context
92// ============================================================================
93
94/** Options for extension UI dialogs. */
95export interface ExtensionUIDialogOptions {
96 /** AbortSignal to programmatically dismiss the dialog. */
97 signal?: AbortSignal;
98 /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */
99 timeout?: number;
100}
101
102/** Placement for extension widgets. */
103export type WidgetPlacement = "aboveEditor" | "belowEditor";
104
105/** Options for extension widgets. */
106export interface ExtensionWidgetOptions {
107 /** Where the widget is rendered. Defaults to "aboveEditor". */
108 placement?: WidgetPlacement;
109}
110
111/** Raw terminal input listener for extensions. */
112export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: string } | undefined;
113
114/** Working indicator configuration for the interactive streaming loader. */
115export interface WorkingIndicatorOptions {
116 /** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */
117 frames?: string[];
118 /** Frame interval in milliseconds for animated indicators. */
119 intervalMs?: number;
120}
121
122/** Wrap the current autocomplete provider with additional behavior. */
123export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
124export type EditorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent;
125
126/**
127 * UI context for extensions to request interactive UI.
128 * Each mode (interactive, RPC, print) provides its own implementation.
129 */
130export interface ExtensionUIContext {
131 /** Show a selector and return the user's choice. */
132 select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
133
134 /** Show a confirmation dialog. */
135 confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise<boolean>;
136
137 /** Show a text input dialog. */
138 input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
139
140 /** Show a notification to the user. */
141 notify(message: string, type?: "info" | "warning" | "error"): void;
142
143 /** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */
144 onTerminalInput(handler: TerminalInputHandler): () => void;
145
146 /** Set status text in the footer/status bar. Pass undefined to clear. */
147 setStatus(key: string, text: string | undefined): void;
148
149 /** Set the working/loading message shown during streaming. Call with no argument to restore default. */
150 setWorkingMessage(message?: string): void;
151
152 /** Show or hide the built-in interactive working loader row during streaming. */
153 setWorkingVisible(visible: boolean): void;
154
155 /**
156 * Configure the interactive working indicator shown during streaming.
157 *
158 * - Omit the argument to restore the default animated spinner.
159 * - Use `frames: ["●"]` for a static indicator.
160 * - Use `frames: []` to hide the indicator entirely.
161 * - Custom frames are rendered as provided, so extensions must add their own colors.
162 */
163 setWorkingIndicator(options?: WorkingIndicatorOptions): void;
164
165 /** Set the label shown for hidden thinking blocks. Call with no argument to restore default. */
166 setHiddenThinkingLabel(label?: string): void;
167
168 /** Set a widget to display above or below the editor. Accepts string array or component factory. */
169 setWidget(key: string, content: string[] | undefined, options?: ExtensionWidgetOptions): void;
170 setWidget(
171 key: string,
172 content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined,
173 options?: ExtensionWidgetOptions,
174 ): void;
175
176 /** Set a custom footer component, or undefined to restore the built-in footer.
177 *
178 * The factory receives a FooterDataProvider for data not otherwise accessible:
179 * git branch and extension statuses from setStatus(). Token stats, model info,
180 * etc. are available via ctx.sessionManager and ctx.model.
181 */
182 setFooter(
183 factory:
184 | ((tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
185 | undefined,
186 ): void;
187
188 /** Set a custom header component (shown at startup, above chat), or undefined to restore the built-in header. */
189 setHeader(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
190
191 /** Set the terminal window/tab title. */
192 setTitle(title: string): void;
193
194 /** Show a custom component with keyboard focus. */
195 custom<T>(
196 factory: (
197 tui: TUI,
198 theme: Theme,
199 keybindings: KeybindingsManager,
200 done: (result: T) => void,
201 ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
202 options?: {
203 overlay?: boolean;
204 /** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */
205 overlayOptions?: OverlayOptions | (() => OverlayOptions);
206 /** Called with the overlay handle after the overlay is shown. Use to control visibility. */
207 onHandle?: (handle: OverlayHandle) => void;
208 },
209 ): Promise<T>;
210
211 /** Paste text into the editor, triggering paste handling (collapse for large content). */
212 pasteToEditor(text: string): void;
213
214 /** Set the text in the core input editor. */
215 setEditorText(text: string): void;
216
217 /** Get the current text from the core input editor. */
218 getEditorText(): string;
219
220 /** Show a multi-line editor for text editing. */
221 editor(title: string, prefill?: string): Promise<string | undefined>;
222
223 /** Stack additional autocomplete behavior on top of the built-in provider. */
224 addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
225
226 /**
227 * Set a custom editor component via factory function.
228 * Pass undefined to restore the default editor.
229 *
230 * The factory receives:
231 * - `theme`: EditorTheme for styling borders and autocomplete
232 * - `keybindings`: KeybindingsManager for app-level keybindings
233 *
234 * For full app keybinding support (escape, ctrl+d, model switching, etc.),
235 * extend `CustomEditor` from `@earendil-works/pi-coding-agent` and call
236 * `super.handleInput(data)` for keys you don't handle.
237 *
238 * @example
239 * ```ts
240 * import { CustomEditor } from "@earendil-works/pi-coding-agent";
241 *
242 * class VimEditor extends CustomEditor {
243 * private mode: "normal" | "insert" = "insert";
244 *
245 * handleInput(data: string): void {
246 * if (this.mode === "normal") {
247 * // Handle vim normal mode keys...
248 * if (data === "i") { this.mode = "insert"; return; }
249 * }
250 * super.handleInput(data); // App keybindings + text editing
251 * }
252 * }
253 *
254 * ctx.ui.setEditorComponent((tui, theme, keybindings) =>
255 * new VimEditor(tui, theme, keybindings)
256 * );
257 * ```
258 */
259 setEditorComponent(factory: EditorFactory | undefined): void;
260
261 /** Get the currently configured custom editor factory, or undefined when using the default editor. */
262 getEditorComponent(): EditorFactory | undefined;
263
264 /** Get the current theme for styling. */
265 readonly theme: Theme;
266
267 /** Get all available themes with their names and file paths. */
268 getAllThemes(): { name: string; path: string | undefined }[];
269
270 /** Load a theme by name without switching to it. Returns undefined if not found. */
271 getTheme(name: string): Theme | undefined;
272
273 /** Set the current theme by name or Theme object. */
274 setTheme(theme: string | Theme): { success: boolean; error?: string };
275
276 /** Get current tool output expansion state. */
277 getToolsExpanded(): boolean;
278
279 /** Set tool output expansion state. */
280 setToolsExpanded(expanded: boolean): void;
281}
282
283// ============================================================================
284// Extension Context
285// ============================================================================
286
287export interface ContextUsage {
288 /** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */
289 tokens: number | null;
290 contextWindow: number;
291 /** Context usage as percentage of context window, or null if tokens is unknown. */
292 percent: number | null;
293}
294
295export interface CompactOptions {
296 customInstructions?: string;
297 onComplete?: (result: CompactionResult) => void;
298 onError?: (error: Error) => void;
299}
300
301/**
302 * Context passed to extension event handlers.
303 */
304export type ExtensionMode = "tui" | "rpc" | "json" | "print";
305
306export interface ExtensionContext {
307 /** UI methods for user interaction */
308 ui: ExtensionUIContext;
309 /** Current run mode. Use "tui" to guard terminal-only UI such as custom components. */
310 mode: ExtensionMode;
311 /** Whether dialog-capable UI is available (true in TUI and RPC modes) */
312 hasUI: boolean;
313 /** Current working directory */
314 cwd: string;
315 /** Session manager (read-only) */
316 sessionManager: ReadonlySessionManager;
317 /** Model registry for API key resolution */
318 modelRegistry: ModelRegistry;
319 /** Current model (may be undefined) */
320 model: Model<any> | undefined;
321 /** Current thinking level, when provided by the session runtime. */
322 thinkingLevel?: ThinkingLevel;
323 /** Whether the agent is idle (not streaming) */
324 isIdle(): boolean;
325 /** Whether project-local trust is active for this context. */
326 isProjectTrusted(): boolean;
327 /** The current abort signal, or undefined when the agent is not streaming. */
328 signal: AbortSignal | undefined;
329 /** Abort the current agent operation */
330 abort(): void;
331 /** Whether there are queued messages waiting */
332 hasPendingMessages(): boolean;
333 /** Gracefully shutdown pi and exit. Available in all contexts. */
334 shutdown(): void;
335 /** Get current context usage for the active model. */
336 getContextUsage(): ContextUsage | undefined;
337 /** Trigger compaction without awaiting completion. */
338 compact(options?: CompactOptions): void;
339 /** Get the current effective system prompt. */
340 getSystemPrompt(): string;
341}
342
343/**
344 * Extended context for command handlers.
345 * Includes session control methods only safe in user-initiated commands.
346 */
347export interface ExtensionCommandContext extends ExtensionContext {
348 /** Get the current base system-prompt construction options. */
349 getSystemPromptOptions(): BuildSystemPromptOptions;
350
351 /** Wait for the agent to finish streaming */
352 waitForIdle(): Promise<void>;
353
354 /** Start a new session, optionally with initialization. */
355 newSession(options?: {
356 parentSession?: string;
357 setup?: (sessionManager: SessionManager) => Promise<void>;
358 withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
359 }): Promise<{ cancelled: boolean }>;
360
361 /** Fork from a specific entry, creating a new session file. */
362 fork(
363 entryId: string,
364 options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
365 ): Promise<{ cancelled: boolean }>;
366
367 /** Navigate to a different point in the session tree. */
368 navigateTree(
369 targetId: string,
370 options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
371 ): Promise<{ cancelled: boolean }>;
372
373 /** Switch to a different session file. */
374 switchSession(
375 sessionPath: string,
376 options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
377 ): Promise<{ cancelled: boolean }>;
378
379 /** Reload extensions, skills, prompts, themes, and context files. */
380 reload(): Promise<void>;
381}
382
383/**
384 * Fresh command-capable context bound to the replacement session after a session switch.
385 *
386 * This is passed to `withSession()` callbacks on `newSession()`, `fork()`, and `switchSession()`.
387 */
388export interface ReplacedSessionContext extends ExtensionCommandContext {
389 sendMessage<T = unknown>(
390 message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
391 options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
392 ): Promise<void>;
393
394 sendUserMessage(
395 content: string | (TextContent | ImageContent)[],
396 options?: { deliverAs?: "steer" | "followUp" },
397 ): Promise<void>;
398}
399
400// ============================================================================
401// Tool Types
402// ============================================================================
403
404/** Rendering options for tool results */
405export interface ToolRenderResultOptions {
406 /** Whether the result view is expanded */
407 expanded: boolean;
408 /** Whether this is a partial/streaming result */
409 isPartial: boolean;
410}
411
412/** Context passed to tool renderers. */
413export interface ToolRenderContext<TState = any, TArgs = any> {
414 /** Current tool call arguments. Shared across call/result renders for the same tool call. */
415 args: TArgs;
416 /** Unique id for this tool execution. Stable across call/result renders for the same tool call. */
417 toolCallId: string;
418 /** Invalidate just this tool execution component for redraw. */
419 invalidate: () => void;
420 /** Previously returned component for this render slot, if any. */
421 lastComponent: Component | undefined;
422 /** Shared renderer state for this tool row. Initialized by tool-execution.ts. */
423 state: TState;
424 /** Working directory for this tool execution. */
425 cwd: string;
426 /** Whether the tool execution has started. */
427 executionStarted: boolean;
428 /** Whether the tool call arguments are complete. */
429 argsComplete: boolean;
430 /** Whether the tool result is partial/streaming. */
431 isPartial: boolean;
432 /** Whether the result view is expanded. */
433 expanded: boolean;
434 /** Whether inline images are currently shown in the TUI. */
435 showImages: boolean;
436 /** Whether the current result is an error. */
437 isError: boolean;
438}
439
440/**
441 * Tool definition for registerTool().
442 */
443export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any> {
444 /** Tool name (used in LLM tool calls) */
445 name: string;
446 /** Human-readable label for UI */
447 label: string;
448 /** Description for LLM */
449 description: string;
450 /** 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. */
451 promptSnippet?: string;
452 /** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */
453 promptGuidelines?: string[];
454 /** Parameter schema (TypeBox) */
455 parameters: TParams;
456 /** Optional provider-side constrained sampling request for this tool. Set false to explicitly disable it, equivalent to leaving it undefined. */
457 constrainedSampling?: false | ConstrainedSamplingConfig;
458 /** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
459 renderShell?: "default" | "self";
460
461 /** Optional compatibility shim to prepare raw tool call arguments before schema validation. Must return an object conforming to TParams. */
462 prepareArguments?: (args: unknown) => Static<TParams>;
463
464 /**
465 * Per-tool execution mode override.
466 * - "sequential": this tool must execute one at a time with other tool calls.
467 * - "parallel": this tool can execute concurrently with other tool calls.
468 *
469 * If omitted, the default execution mode applies.
470 */
471 executionMode?: ToolExecutionMode;
472
473 /** Execute the tool. */
474 execute(
475 toolCallId: string,
476 params: Static<TParams>,
477 signal: AbortSignal | undefined,
478 onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
479 ctx: ExtensionContext,
480 ): Promise<AgentToolResult<TDetails>>;
481
482 /** Custom rendering for tool call display */
483 renderCall?: (args: Static<TParams>, theme: Theme, context: ToolRenderContext<TState, Static<TParams>>) => Component;
484
485 /** Custom rendering for tool result display */
486 renderResult?: (
487 result: AgentToolResult<TDetails>,
488 options: ToolRenderResultOptions,
489 theme: Theme,
490 context: ToolRenderContext<TState, Static<TParams>>,
491 ) => Component;
492}
493
494type AnyToolDefinition = ToolDefinition<any, any, any>;
495
496/**
497 * Preserve parameter inference for standalone tool definitions.
498 *
499 * Use this when assigning a tool to a variable or passing it through arrays such
500 * as `customTools`, where contextual typing would otherwise widen params to
501 * `unknown`.
502 */
503export function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(
504 tool: ToolDefinition<TParams, TDetails, TState>,
505): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition {
506 return tool as ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition;
507}
508
509// ============================================================================
510// Startup/Resource Events
511// ============================================================================
512
513export interface ProjectTrustEvent {
514 type: "project_trust";
515 cwd: string;
516}
517
518export type ProjectTrustEventDecision = "yes" | "no" | "undecided";
519
520export interface ProjectTrustEventResult {
521 trusted: ProjectTrustEventDecision;
522 remember?: boolean;
523}
524
525export interface ProjectTrustContext {
526 cwd: string;
527 mode: ExtensionMode;
528 hasUI: boolean;
529 ui: Pick<ExtensionUIContext, "select" | "confirm" | "input" | "notify">;
530}
531
532export type ProjectTrustHandler = (
533 event: ProjectTrustEvent,
534 ctx: ProjectTrustContext,
535) => Promise<ProjectTrustEventResult> | ProjectTrustEventResult;
536
537/** Fired after session_start to allow extensions to provide additional resource paths. */
538export interface ResourcesDiscoverEvent {
539 type: "resources_discover";
540 cwd: string;
541 reason: "startup" | "reload";
542}
543
544/** Result from resources_discover event handler */
545export interface ResourcesDiscoverResult {
546 skillPaths?: string[];
547 promptPaths?: string[];
548 themePaths?: string[];
549}
550
551// ============================================================================
552// Session Events
553// ============================================================================
554
555/** Fired when a session is started, loaded, or reloaded */
556export interface SessionStartEvent {
557 type: "session_start";
558 /** Why this session start happened. */
559 reason: "startup" | "reload" | "new" | "resume" | "fork";
560 /** Previously active session file. Present for "new", "resume", and "fork". */
561 previousSessionFile?: string;
562}
563
564/** Fired when the current session metadata changes. */
565export interface SessionInfoChangedEvent {
566 type: "session_info_changed";
567 /** Current normalized session name. Undefined when the name is cleared. */
568 name: string | undefined;
569}
570
571/** Fired before switching to another session (can be cancelled) */
572export interface SessionBeforeSwitchEvent {
573 type: "session_before_switch";
574 reason: "new" | "resume";
575 targetSessionFile?: string;
576}
577
578/** Fired before forking a session (can be cancelled) */
579export interface SessionBeforeForkEvent {
580 type: "session_before_fork";
581 entryId: string;
582 position: "before" | "at";
583}
584
585/** Fired before context compaction (can be cancelled or customized) */
586export interface SessionBeforeCompactEvent {
587 type: "session_before_compact";
588 preparation: CompactionPreparation;
589 branchEntries: SessionEntry[];
590 customInstructions?: string;
591 /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
592 reason: "manual" | "threshold" | "overflow";
593 /** True when the aborted turn is retried after this compaction (overflow recovery) */
594 willRetry: boolean;
595 signal: AbortSignal;
596}
597
598/** Fired after context compaction */
599export interface SessionCompactEvent {
600 type: "session_compact";
601 compactionEntry: CompactionEntry;
602 fromExtension: boolean;
603 /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
604 reason: "manual" | "threshold" | "overflow";
605 /** True when the aborted turn is retried after this compaction (overflow recovery) */
606 willRetry: boolean;
607}
608
609/** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */
610export interface SessionShutdownEvent {
611 type: "session_shutdown";
612 reason: "quit" | "reload" | "new" | "resume" | "fork";
613 /** Destination session file when shutting down due to session replacement. */
614 targetSessionFile?: string;
615}
616
617/** Preparation data for tree navigation */
618export interface TreePreparation {
619 targetId: string;
620 oldLeafId: string | null;
621 commonAncestorId: string | null;
622 entriesToSummarize: SessionEntry[];
623 userWantsSummary: boolean;
624 /** Custom instructions for summarization */
625 customInstructions?: string;
626 /** If true, customInstructions replaces the default prompt instead of being appended */
627 replaceInstructions?: boolean;
628 /** Label to attach to the branch summary entry */
629 label?: string;
630}
631
632/** Fired before navigating in the session tree (can be cancelled) */
633export interface SessionBeforeTreeEvent {
634 type: "session_before_tree";
635 preparation: TreePreparation;
636 signal: AbortSignal;
637}
638
639/** Fired after navigating in the session tree */
640export interface SessionTreeEvent {
641 type: "session_tree";
642 newLeafId: string | null;
643 oldLeafId: string | null;
644 summaryEntry?: BranchSummaryEntry;
645 fromExtension?: boolean;
646}
647
648export type SessionEvent =
649 | SessionStartEvent
650 | SessionInfoChangedEvent
651 | SessionBeforeSwitchEvent
652 | SessionBeforeForkEvent
653 | SessionBeforeCompactEvent
654 | SessionCompactEvent
655 | SessionShutdownEvent
656 | SessionBeforeTreeEvent
657 | SessionTreeEvent;
658
659// ============================================================================
660// Agent Events
661// ============================================================================
662
663/** Fired before each LLM call. Can modify messages. */
664export interface ContextEvent {
665 type: "context";
666 messages: AgentMessage[];
667}
668
669/** Fired before a provider request is sent. Can replace the payload. */
670export interface BeforeProviderRequestEvent {
671 type: "before_provider_request";
672 payload: unknown;
673}
674
675/**
676 * Fired after request headers are assembled, before the provider HTTP call.
677 * Handlers mutate `headers` in place (e.g. to inject tracing/session headers);
678 * the return value is ignored. A `null` value deletes that header.
679 */
680export interface BeforeProviderHeadersEvent {
681 type: "before_provider_headers";
682 headers: ProviderHeaders;
683}
684
685/** Fired after a provider response is received and before the response stream is consumed. */
686export interface AfterProviderResponseEvent {
687 type: "after_provider_response";
688 status: number;
689 headers: Record<string, string>;
690}
691
692/** Fired after user submits prompt but before agent loop. */
693export interface BeforeAgentStartEvent {
694 type: "before_agent_start";
695 /** The raw user prompt text (after expansion). */
696 prompt: string;
697 /** Images attached to the user prompt, if any. */
698 images?: ImageContent[];
699 /** The fully assembled system prompt string. */
700 systemPrompt: string;
701 /** Structured options used to build the system prompt. Extensions can inspect this to understand what Pi loaded without re-discovering resources. */
702 systemPromptOptions: BuildSystemPromptOptions;
703}
704
705/** Fired when an agent loop starts */
706export interface AgentStartEvent {
707 type: "agent_start";
708}
709
710/** Fired when an agent loop ends */
711export interface AgentEndEvent {
712 type: "agent_end";
713 messages: AgentMessage[];
714}
715
716/** Fired after an agent run has fully settled and no automatic retry, compaction, or queued continuation will run. */
717export interface AgentSettledEvent {
718 type: "agent_settled";
719}
720
721/** Fired at the start of each turn */
722export interface TurnStartEvent {
723 type: "turn_start";
724 turnIndex: number;
725 timestamp: number;
726}
727
728/** Fired at the end of each turn */
729export interface TurnEndEvent {
730 type: "turn_end";
731 turnIndex: number;
732 message: AgentMessage;
733 toolResults: ToolResultMessage[];
734}
735
736/** Fired when a message starts (user, assistant, or toolResult) */
737export interface MessageStartEvent {
738 type: "message_start";
739 message: AgentMessage;
740}
741
742/** Fired during assistant message streaming with token-by-token updates */
743export interface MessageUpdateEvent {
744 type: "message_update";
745 message: AgentMessage;
746 assistantMessageEvent: AssistantMessageEvent;
747}
748
749/** Fired when a message ends */
750export interface MessageEndEvent {
751 type: "message_end";
752 message: AgentMessage;
753}
754
755/** Fired when a tool starts executing */
756export interface ToolExecutionStartEvent {
757 type: "tool_execution_start";
758 toolCallId: string;
759 toolName: string;
760 args: any;
761}
762
763/** Fired during tool execution with partial/streaming output */
764export interface ToolExecutionUpdateEvent {
765 type: "tool_execution_update";
766 toolCallId: string;
767 toolName: string;
768 args: any;
769 partialResult: any;
770}
771
772/** Fired when a tool finishes executing */
773export interface ToolExecutionEndEvent {
774 type: "tool_execution_end";
775 toolCallId: string;
776 toolName: string;
777 result: any;
778 isError: boolean;
779}
780
781// ============================================================================
782// Model Events
783// ============================================================================
784
785export type ModelSelectSource = "set" | "cycle" | "restore";
786
787/** Fired when a new model is selected */
788export interface ModelSelectEvent {
789 type: "model_select";
790 model: Model<any>;
791 previousModel: Model<any> | undefined;
792 source: ModelSelectSource;
793}
794
795/** Fired when a new thinking level is selected */
796export interface ThinkingLevelSelectEvent {
797 type: "thinking_level_select";
798 level: ThinkingLevel;
799 previousLevel: ThinkingLevel;
800}
801
802// ============================================================================
803// User Bash Events
804// ============================================================================
805
806/** Fired when user executes a bash command via ! or !! prefix */
807export interface UserBashEvent {
808 type: "user_bash";
809 /** The command to execute */
810 command: string;
811 /** True if !! prefix was used (excluded from LLM context) */
812 excludeFromContext: boolean;
813 /** Current working directory */
814 cwd: string;
815}
816
817// ============================================================================
818// Input Events
819// ============================================================================
820
821/** Source of user input */
822export type InputSource = "interactive" | "rpc" | "extension";
823
824/** Fired when user input is received, before agent processing */
825export interface InputEvent {
826 type: "input";
827 /** The input text */
828 text: string;
829 /** Attached images, if any */
830 images?: ImageContent[];
831 /** Where the input came from */
832 source: InputSource;
833 /** How the input will be delivered during streaming, or undefined when idle */
834 streamingBehavior?: "steer" | "followUp";
835}
836
837/** Result from input event handler */
838export type InputEventResult =
839 | { action: "continue" }
840 | { action: "transform"; text: string; images?: ImageContent[] }
841 | { action: "handled" };
842
843// ============================================================================
844// Tool Events
845// ============================================================================
846
847interface ToolCallEventBase {
848 type: "tool_call";
849 toolCallId: string;
850}
851
852export interface BashToolCallEvent extends ToolCallEventBase {
853 toolName: "bash";
854 input: BashToolInput;
855}
856
857export interface ReadToolCallEvent extends ToolCallEventBase {
858 toolName: "read";
859 input: ReadToolInput;
860}
861
862export interface EditToolCallEvent extends ToolCallEventBase {
863 toolName: "edit";
864 input: EditToolInput;
865}
866
867export interface WriteToolCallEvent extends ToolCallEventBase {
868 toolName: "write";
869 input: WriteToolInput;
870}
871
872export interface GrepToolCallEvent extends ToolCallEventBase {
873 toolName: "grep";
874 input: GrepToolInput;
875}
876
877export interface FindToolCallEvent extends ToolCallEventBase {
878 toolName: "find";
879 input: FindToolInput;
880}
881
882export interface LsToolCallEvent extends ToolCallEventBase {
883 toolName: "ls";
884 input: LsToolInput;
885}
886
887export interface CustomToolCallEvent extends ToolCallEventBase {
888 toolName: string;
889 input: Record<string, unknown>;
890}
891
892/**
893 * Fired before a tool executes. Can block.
894 *
895 * `event.input` is mutable. Mutate it in place to patch tool arguments before execution.
896 * Later `tool_call` handlers see earlier mutations. No re-validation is performed after mutation.
897 */
898export type ToolCallEvent =
899 | BashToolCallEvent
900 | ReadToolCallEvent
901 | EditToolCallEvent
902 | WriteToolCallEvent
903 | GrepToolCallEvent
904 | FindToolCallEvent
905 | LsToolCallEvent
906 | CustomToolCallEvent;
907
908interface ToolResultEventBase {
909 type: "tool_result";
910 toolCallId: string;
911 input: Record<string, unknown>;
912 content: (TextContent | ImageContent)[];
913 isError: boolean;
914 /** Usage from the tool execution itself, if available. */
915 usage?: Usage;
916}
917
918export interface BashToolResultEvent extends ToolResultEventBase {
919 toolName: "bash";
920 details: BashToolDetails | undefined;
921}
922
923export interface ReadToolResultEvent extends ToolResultEventBase {
924 toolName: "read";
925 details: ReadToolDetails | undefined;
926}
927
928export interface EditToolResultEvent extends ToolResultEventBase {
929 toolName: "edit";
930 details: EditToolDetails | undefined;
931}
932
933export interface WriteToolResultEvent extends ToolResultEventBase {
934 toolName: "write";
935 details: undefined;
936}
937
938export interface GrepToolResultEvent extends ToolResultEventBase {
939 toolName: "grep";
940 details: GrepToolDetails | undefined;
941}
942
943export interface FindToolResultEvent extends ToolResultEventBase {
944 toolName: "find";
945 details: FindToolDetails | undefined;
946}
947
948export interface LsToolResultEvent extends ToolResultEventBase {
949 toolName: "ls";
950 details: LsToolDetails | undefined;
951}
952
953export interface CustomToolResultEvent extends ToolResultEventBase {
954 toolName: string;
955 details: unknown;
956}
957
958/** Fired after a tool executes. Can modify result. */
959export type ToolResultEvent =
960 | BashToolResultEvent
961 | ReadToolResultEvent
962 | EditToolResultEvent
963 | WriteToolResultEvent
964 | GrepToolResultEvent
965 | FindToolResultEvent
966 | LsToolResultEvent
967 | CustomToolResultEvent;
968
969// Type guards for ToolResultEvent
970export function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {
971 return e.toolName === "bash";
972}
973export function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {
974 return e.toolName === "read";
975}
976export function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {
977 return e.toolName === "edit";
978}
979export function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {
980 return e.toolName === "write";
981}
982export function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {
983 return e.toolName === "grep";
984}
985export function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {
986 return e.toolName === "find";
987}
988export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {
989 return e.toolName === "ls";
990}
991
992/**
993 * Type guard for narrowing ToolCallEvent by tool name.
994 *
995 * Built-in tools narrow automatically (no type params needed):
996 * ```ts
997 * if (isToolCallEventType("bash", event)) {
998 * event.input.command; // string
999 * }
1000 * ```
1001 *
1002 * Custom tools require explicit type parameters:
1003 * ```ts
1004 * if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
1005 * event.input.action; // typed
1006 * }
1007 * ```
1008 *
1009 * Note: Direct narrowing via `event.toolName === "bash"` doesn't work because
1010 * CustomToolCallEvent.toolName is `string` which overlaps with all literals.
1011 */
1012export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent;
1013export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent;
1014export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent;
1015export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent;
1016export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent;
1017export function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent;
1018export function isToolCallEventType(toolName: "ls", event: ToolCallEvent): event is LsToolCallEvent;
1019export function isToolCallEventType<TName extends string, TInput extends Record<string, unknown>>(
1020 toolName: TName,
1021 event: ToolCallEvent,
1022): event is ToolCallEvent & { toolName: TName; input: TInput };
1023export function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean {
1024 return event.toolName === toolName;
1025}
1026
1027/** Union of all event types */
1028export type ExtensionEvent =
1029 | ProjectTrustEvent
1030 | ResourcesDiscoverEvent
1031 | SessionEvent
1032 | ContextEvent
1033 | BeforeProviderRequestEvent
1034 | BeforeProviderHeadersEvent
1035 | AfterProviderResponseEvent
1036 | BeforeAgentStartEvent
1037 | AgentStartEvent
1038 | AgentEndEvent
1039 | AgentSettledEvent
1040 | TurnStartEvent
1041 | TurnEndEvent
1042 | MessageStartEvent
1043 | MessageUpdateEvent
1044 | MessageEndEvent
1045 | ToolExecutionStartEvent
1046 | ToolExecutionUpdateEvent
1047 | ToolExecutionEndEvent
1048 | ModelSelectEvent
1049 | ThinkingLevelSelectEvent
1050 | UserBashEvent
1051 | InputEvent
1052 | ToolCallEvent
1053 | ToolResultEvent;
1054
1055// ============================================================================
1056// Event Results
1057// ============================================================================
1058
1059export interface ContextEventResult {
1060 messages?: AgentMessage[];
1061}
1062
1063export type BeforeProviderRequestEventResult = unknown;
1064
1065export interface ToolCallEventResult {
1066 /** Block tool execution. To modify arguments, mutate `event.input` in place instead. */
1067 block?: boolean;
1068 reason?: string;
1069}
1070
1071/** Result from user_bash event handler */
1072export interface UserBashEventResult {
1073 /** Custom operations to use for execution */
1074 operations?: BashOperations;
1075 /** Full replacement: extension handled execution, use this result */
1076 result?: BashResult;
1077}
1078
1079export interface ToolResultEventResult {
1080 content?: (TextContent | ImageContent)[];
1081 details?: unknown;
1082 isError?: boolean;
1083 usage?: Usage;
1084}
1085
1086export interface MessageEndEventResult {
1087 /** Replace the finalized message. The replacement must keep the original message role. */
1088 message?: AgentMessage;
1089}
1090
1091export interface BeforeAgentStartEventResult {
1092 message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
1093 /** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */
1094 systemPrompt?: string;
1095}
1096
1097export interface SessionBeforeSwitchResult {
1098 cancel?: boolean;
1099}
1100
1101export interface SessionBeforeForkResult {
1102 cancel?: boolean;
1103 skipConversationRestore?: boolean;
1104}
1105
1106export interface SessionBeforeCompactResult {
1107 cancel?: boolean;
1108 compaction?: CompactionResult;
1109}
1110
1111export interface SessionBeforeTreeResult {
1112 cancel?: boolean;
1113 summary?: {
1114 summary: string;
1115 details?: unknown;
1116 usage?: Usage;
1117 };
1118 /** Override custom instructions for summarization */
1119 customInstructions?: string;
1120 /** Override whether customInstructions replaces the default prompt */
1121 replaceInstructions?: boolean;
1122 /** Override label to attach to the branch summary entry */
1123 label?: string;
1124}
1125
1126// ============================================================================
1127// Message and Entry Rendering
1128// ============================================================================
1129
1130export interface MessageRenderOptions {
1131 expanded: boolean;
1132 /** Horizontal padding configured by the outputPad setting. */
1133 outputPad: number;
1134}
1135
1136export interface EntryRenderOptions {
1137 expanded: boolean;
1138}
1139
1140export type MessageRenderer<T = unknown> = (
1141 message: CustomMessage<T>,
1142 options: MessageRenderOptions,
1143 theme: Theme,
1144) => Component | undefined;
1145
1146export type EntryRenderer<T = unknown> = (
1147 entry: CustomEntry<T>,
1148 options: EntryRenderOptions,
1149 theme: Theme,
1150) => Component | undefined;
1151
1152// ============================================================================
1153// Command Registration
1154// ============================================================================
1155
1156export interface RegisteredCommand {
1157 name: string;
1158 sourceInfo: SourceInfo;
1159 description?: string;
1160 getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
1161 handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
1162}
1163
1164export interface ResolvedCommand extends RegisteredCommand {
1165 invocationName: string;
1166}
1167
1168// ============================================================================
1169// Extension API
1170// ============================================================================
1171
1172/** Handler function type for events */
1173// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements
1174export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;
1175
1176/**
1177 * ExtensionAPI passed to extension factory functions.
1178 */
1179export interface ExtensionAPI {
1180 // =========================================================================
1181 // Event Subscription
1182 // =========================================================================
1183
1184 on(event: "project_trust", handler: ProjectTrustHandler): void;
1185 on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
1186 on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
1187 on(event: "session_info_changed", handler: ExtensionHandler<SessionInfoChangedEvent>): void;
1188 on(
1189 event: "session_before_switch",
1190 handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>,
1191 ): void;
1192 on(event: "session_before_fork", handler: ExtensionHandler<SessionBeforeForkEvent, SessionBeforeForkResult>): void;
1193 on(
1194 event: "session_before_compact",
1195 handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
1196 ): void;
1197 on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
1198 on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
1199 on(event: "session_before_tree", handler: ExtensionHandler<SessionBeforeTreeEvent, SessionBeforeTreeResult>): void;
1200 on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>): void;
1201 on(event: "context", handler: ExtensionHandler<ContextEvent, ContextEventResult>): void;
1202 on(
1203 event: "before_provider_request",
1204 handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>,
1205 ): void;
1206 on(event: "before_provider_headers", handler: ExtensionHandler<BeforeProviderHeadersEvent>): void;
1207 on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>): void;
1208 on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
1209 on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): void;
1210 on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
1211 on(event: "agent_settled", handler: ExtensionHandler<AgentSettledEvent>): void;
1212 on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
1213 on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
1214 on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
1215 on(event: "message_update", handler: ExtensionHandler<MessageUpdateEvent>): void;
1216 on(event: "message_end", handler: ExtensionHandler<MessageEndEvent, MessageEndEventResult>): void;
1217 on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): void;
1218 on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): void;
1219 on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): void;
1220 on(event: "model_select", handler: ExtensionHandler<ModelSelectEvent>): void;
1221 on(event: "thinking_level_select", handler: ExtensionHandler<ThinkingLevelSelectEvent>): void;
1222 on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
1223 on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
1224 on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
1225 on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
1226
1227 // =========================================================================
1228 // Tool Registration
1229 // =========================================================================
1230
1231 /** Register a tool that the LLM can call. */
1232 registerTool<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>(
1233 tool: ToolDefinition<TParams, TDetails, TState>,
1234 ): void;
1235
1236 // =========================================================================
1237 // Command, Shortcut, Flag Registration
1238 // =========================================================================
1239
1240 /** Register a custom command. */
1241 registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void;
1242
1243 /** Register a keyboard shortcut. */
1244 registerShortcut(
1245 shortcut: KeyId,
1246 options: {
1247 description?: string;
1248 handler: (ctx: ExtensionContext) => Promise<void> | void;
1249 },
1250 ): void;
1251
1252 /** Register a CLI flag. */
1253 registerFlag(
1254 name: string,
1255 options: {
1256 description?: string;
1257 type: "boolean" | "string";
1258 default?: boolean | string;
1259 },
1260 ): void;
1261
1262 /** Get the value of a registered CLI flag. */
1263 getFlag(name: string): boolean | string | undefined;
1264
1265 // =========================================================================
1266 // Message Rendering
1267 // =========================================================================
1268
1269 /** Register a custom renderer for CustomMessageEntry. */
1270 registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
1271
1272 /** Register a custom renderer for CustomEntry. Custom entries do not participate in LLM context. */
1273 registerEntryRenderer<T = unknown>(customType: string, renderer: EntryRenderer<T>): void;
1274
1275 // =========================================================================
1276 // Actions
1277 // =========================================================================
1278
1279 /** Send a custom message to the session. */
1280 sendMessage<T = unknown>(
1281 message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
1282 options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
1283 ): void;
1284
1285 /**
1286 * Send a user message to the agent. Always triggers a turn.
1287 * When the agent is streaming, use deliverAs to specify how to queue the message.
1288 */
1289 sendUserMessage(
1290 content: string | (TextContent | ImageContent)[],
1291 options?: { deliverAs?: "steer" | "followUp" },
1292 ): void;
1293
1294 /** Append a custom entry to the session for state persistence (not sent to LLM). */
1295 appendEntry<T = unknown>(customType: string, data?: T): void;
1296
1297 // =========================================================================
1298 // Session Metadata
1299 // =========================================================================
1300
1301 /** Set the session display name (shown in session selector). */
1302 setSessionName(name: string): void;
1303
1304 /** Get the current session name, if set. */
1305 getSessionName(): string | undefined;
1306
1307 /** Set or clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */
1308 setLabel(entryId: string, label: string | undefined): void;
1309
1310 /** Execute a shell command. */
1311 exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
1312
1313 /** Get the list of currently active tool names. */
1314 getActiveTools(): string[];
1315
1316 /** Get all configured tools with parameter schema, prompt guidelines, and source metadata. */
1317 getAllTools(): ToolInfo[];
1318
1319 /** Set the active tools by name. */
1320 setActiveTools(toolNames: string[]): void;
1321
1322 /** Get available slash commands in the current session. */
1323 getCommands(): SlashCommandInfo[];
1324
1325 // =========================================================================
1326 // Model and Thinking Level
1327 // =========================================================================
1328
1329 /** Set the current model. Returns false if no API key available. */
1330 setModel(model: Model<any>): Promise<boolean>;
1331
1332 /** Get current thinking level. */
1333 getThinkingLevel(): ThinkingLevel;
1334
1335 /** Set thinking level (clamped to model capabilities). */
1336 setThinkingLevel(level: ThinkingLevel): void;
1337
1338 // =========================================================================
1339 // Provider Registration
1340 // =========================================================================
1341
1342 /**
1343 * Register or override a model provider.
1344 *
1345 * If `models` is provided: replaces all existing models for this provider.
1346 * If only `baseUrl` is provided: overrides the URL for existing models.
1347 * If `oauth` is provided: registers OAuth provider for /login support.
1348 * If `streamSimple` is provided: registers a custom API stream handler.
1349 *
1350 * During initial extension load this call is queued and applied once the
1351 * runner has bound its context. After that it takes effect immediately, so
1352 * it is safe to call from command handlers or event callbacks without
1353 * requiring a `/reload`.
1354 *
1355 * @example
1356 * // Register a new provider with custom models
1357 * pi.registerProvider("my-proxy", {
1358 * baseUrl: "https://proxy.example.com",
1359 * apiKey: "$PROXY_API_KEY",
1360 * api: "anthropic-messages",
1361 * models: [
1362 * {
1363 * id: "claude-sonnet-4-20250514",
1364 * name: "Claude 4 Sonnet (proxy)",
1365 * reasoning: false,
1366 * input: ["text", "image"],
1367 * cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1368 * contextWindow: 200000,
1369 * maxTokens: 16384
1370 * }
1371 * ]
1372 * });
1373 *
1374 * @example
1375 * // Override baseUrl for an existing provider
1376 * pi.registerProvider("anthropic", {
1377 * baseUrl: "https://proxy.example.com"
1378 * });
1379 *
1380 * @example
1381 * // Register provider with OAuth support
1382 * pi.registerProvider("corporate-ai", {
1383 * baseUrl: "https://ai.corp.com",
1384 * api: "openai-responses",
1385 * models: [...],
1386 * oauth: {
1387 * name: "Corporate AI (SSO)",
1388 * async login(callbacks) { ... },
1389 * async refreshToken(credentials) { ... },
1390 * getApiKey(credentials) { return credentials.access; }
1391 * }
1392 * });
1393 */
1394 registerProvider(provider: Provider): void;
1395 registerProvider(name: string, config: ProviderConfig): void;
1396
1397 /**
1398 * Unregister a previously registered provider.
1399 *
1400 * Removes all models belonging to the named provider and restores any
1401 * built-in models that were overridden by it. Has no effect if the provider
1402 * is not currently registered.
1403 *
1404 * Like `registerProvider`, this takes effect immediately when called after
1405 * the initial load phase.
1406 *
1407 * @example
1408 * pi.unregisterProvider("my-proxy");
1409 */
1410 unregisterProvider(name: string): void;
1411
1412 /** Shared event bus for extension communication. */
1413 events: EventBus;
1414}
1415
1416// ============================================================================
1417// Provider Registration Types
1418// ============================================================================
1419
1420/** Configuration for registering a provider via pi.registerProvider(). */
1421export interface ProviderConfig {
1422 /** Display name for the provider in UI. */
1423 name?: string;
1424 /** Base URL for the API endpoint. Required when defining models. */
1425 baseUrl?: string;
1426 /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or leading !command. Required when defining models (unless oauth provided). */
1427 apiKey?: string;
1428 /** API type. Required at provider or model level when defining models. */
1429 api?: Api;
1430 /** Optional streamSimple handler for custom APIs. */
1431 streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
1432 /** Custom headers to include in requests. */
1433 headers?: Record<string, string>;
1434 /** If true, adds Authorization: Bearer header with the resolved API key. */
1435 authHeader?: boolean;
1436 /** Models to register. If provided, replaces all existing models for this provider. */
1437 models?: ProviderModelConfig[];
1438 /**
1439 * Refresh this provider's model list. The returned list replaces extension-provided models.
1440 * Use context.store explicitly when the catalog should persist across sessions.
1441 */
1442 refreshModels?(context: RefreshModelsContext): Promise<ProviderModelConfig[]>;
1443 /** OAuth provider for /login support. The `id` is set automatically from the provider name. */
1444 oauth?: {
1445 /** Display name for the provider in login UI. */
1446 name: string;
1447 /** @deprecated Retained for source compatibility; canonical auth flows ignore it. */
1448 usesCallbackServer?: boolean;
1449 /** Run the login flow, return credentials to persist. */
1450 login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
1451 /** Refresh expired credentials, return updated credentials to persist. */
1452 refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials>;
1453 /** Convert credentials to API key string for the provider. */
1454 getApiKey(credentials: OAuthCredentials): string;
1455 /** Legacy synchronous credential-dependent model projection. */
1456 modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
1457 };
1458}
1459
1460/** Configuration for a model within a provider. */
1461export interface ProviderModelConfig {
1462 /** Model ID (e.g., "claude-sonnet-4-20250514"). */
1463 id: string;
1464 /** Display name (e.g., "Claude 4 Sonnet"). */
1465 name: string;
1466 /** API type override for this model. */
1467 api?: Api;
1468 /** API endpoint URL override for this model. */
1469 baseUrl?: string;
1470 /** Whether the model supports extended thinking. */
1471 reasoning: boolean;
1472 /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */
1473 thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
1474 /** Supported input types. */
1475 input: ("text" | "image")[];
1476 /** Per-million-token cost rates and optional request-wide input pricing tiers. */
1477 cost: Model<Api>["cost"];
1478 /** Maximum context window size in tokens. */
1479 contextWindow: number;
1480 /** Maximum output tokens. */
1481 maxTokens: number;
1482 /** Custom headers for this model. */
1483 headers?: Record<string, string>;
1484 /** OpenAI compatibility settings. */
1485 compat?: Model<Api>["compat"];
1486}
1487
1488/** Extension factory function type. Supports both sync and async initialization. */
1489export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
1490
1491export type InlineExtension =
1492 | ExtensionFactory
1493 | {
1494 /** Display name shown as `<inline:name>` in the startup Extensions list. */
1495 name: string;
1496 factory: ExtensionFactory;
1497 /** Omit this extension from the startup Extensions list. */
1498 hidden?: boolean;
1499 };
1500
1501// ============================================================================
1502// Loaded Extension Types
1503// ============================================================================
1504
1505export interface RegisteredTool {
1506 definition: ToolDefinition;
1507 sourceInfo: SourceInfo;
1508}
1509
1510export interface ExtensionFlag {
1511 name: string;
1512 description?: string;
1513 type: "boolean" | "string";
1514 default?: boolean | string;
1515 extensionPath: string;
1516}
1517
1518export interface ExtensionShortcut {
1519 shortcut: KeyId;
1520 description?: string;
1521 handler: (ctx: ExtensionContext) => Promise<void> | void;
1522 extensionPath: string;
1523}
1524
1525type HandlerFn = (...args: unknown[]) => Promise<unknown>;
1526
1527export type SendMessageHandler = <T = unknown>(
1528 message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
1529 options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
1530) => void;
1531
1532export type SendUserMessageHandler = (
1533 content: string | (TextContent | ImageContent)[],
1534 options?: { deliverAs?: "steer" | "followUp" },
1535) => void;
1536
1537export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
1538
1539export type SetSessionNameHandler = (name: string) => void;
1540
1541export type GetSessionNameHandler = () => string | undefined;
1542
1543export type GetActiveToolsHandler = () => string[];
1544
1545/** Tool info with name, description, parameter schema, prompt guidelines, and source metadata. */
1546export type ToolInfo = Pick<ToolDefinition, "name" | "description" | "parameters" | "promptGuidelines"> & {
1547 sourceInfo: SourceInfo;
1548};
1549
1550export type GetAllToolsHandler = () => ToolInfo[];
1551
1552export type GetCommandsHandler = () => SlashCommandInfo[];
1553
1554export type SetActiveToolsHandler = (toolNames: string[]) => void;
1555
1556export type RefreshToolsHandler = () => void;
1557
1558export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
1559
1560export type GetThinkingLevelHandler = () => ThinkingLevel;
1561
1562export type SetThinkingLevelHandler = (level: ThinkingLevel) => void;
1563
1564export type SetLabelHandler = (entryId: string, label: string | undefined) => void;
1565
1566/**
1567 * Shared state created by loader, used during registration and runtime.
1568 * Contains flag values (defaults set during registration, CLI values set after).
1569 */
1570export interface ExtensionRuntimeState {
1571 flagValues: Map<string, boolean | string>;
1572 /** Legacy provider-config registrations queued during extension loading, processed when runner binds. */
1573 pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; extensionPath: string }>;
1574 /** Native pi-ai provider registrations queued during extension loading, processed when runner binds. */
1575 pendingNativeProviderRegistrations: Array<{ provider: Provider; extensionPath: string }>;
1576 /** Throws when this extension instance is stale after runtime replacement. */
1577 assertActive: () => void;
1578 /** Marks this extension instance as stale after runtime replacement or reload. */
1579 invalidate: (message?: string) => void;
1580 /**
1581 * Register or unregister a provider.
1582 *
1583 * Before bindCore(): queues registrations / removes from queue.
1584 * After bindCore(): calls ModelRegistry directly for immediate effect.
1585 */
1586 registerProvider: (name: string, config: ProviderConfig, extensionPath?: string) => void;
1587 registerNativeProvider: (provider: Provider, extensionPath?: string) => void;
1588 unregisterProvider: (name: string, extensionPath?: string) => void;
1589}
1590
1591/**
1592 * Action implementations for pi.* API methods.
1593 * Provided to runner.initialize(), copied into the shared runtime.
1594 */
1595export interface ExtensionActions {
1596 sendMessage: SendMessageHandler;
1597 sendUserMessage: SendUserMessageHandler;
1598 appendEntry: AppendEntryHandler;
1599 setSessionName: SetSessionNameHandler;
1600 getSessionName: GetSessionNameHandler;
1601 setLabel: SetLabelHandler;
1602 getActiveTools: GetActiveToolsHandler;
1603 getAllTools: GetAllToolsHandler;
1604 setActiveTools: SetActiveToolsHandler;
1605 refreshTools: RefreshToolsHandler;
1606 getCommands: GetCommandsHandler;
1607 setModel: SetModelHandler;
1608 getThinkingLevel: GetThinkingLevelHandler;
1609 setThinkingLevel: SetThinkingLevelHandler;
1610}
1611
1612/**
1613 * Actions for ExtensionContext (ctx.* in event handlers).
1614 * Required by all modes.
1615 */
1616export interface ExtensionContextActions {
1617 getModel: () => Model<any> | undefined;
1618 isIdle: () => boolean;
1619 isProjectTrusted: () => boolean;
1620 getSignal: () => AbortSignal | undefined;
1621 abort: () => void;
1622 hasPendingMessages: () => boolean;
1623 shutdown: () => void;
1624 getContextUsage: () => ContextUsage | undefined;
1625 compact: (options?: CompactOptions) => void;
1626 getSystemPrompt: () => string;
1627 getSystemPromptOptions?: () => BuildSystemPromptOptions;
1628}
1629
1630/**
1631 * Actions for ExtensionCommandContext (ctx.* in command handlers).
1632 * Only needed for interactive mode where extension commands are invokable.
1633 */
1634export interface ExtensionCommandContextActions {
1635 waitForIdle: () => Promise<void>;
1636 newSession: (options?: {
1637 parentSession?: string;
1638 setup?: (sessionManager: SessionManager) => Promise<void>;
1639 withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
1640 }) => Promise<{ cancelled: boolean }>;
1641 fork: (
1642 entryId: string,
1643 options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
1644 ) => Promise<{ cancelled: boolean }>;
1645 navigateTree: (
1646 targetId: string,
1647 options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
1648 ) => Promise<{ cancelled: boolean }>;
1649 switchSession: (
1650 sessionPath: string,
1651 options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
1652 ) => Promise<{ cancelled: boolean }>;
1653 reload: () => Promise<void>;
1654}
1655
1656/**
1657 * Full runtime = state + actions.
1658 * Created by loader with throwing action stubs, completed by runner.initialize().
1659 */
1660export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {}
1661
1662/** Loaded extension with all registered items. */
1663export interface Extension {
1664 path: string;
1665 resolvedPath: string;
1666 hidden?: boolean;
1667 sourceInfo: SourceInfo;
1668 handlers: Map<string, HandlerFn[]>;
1669 tools: Map<string, RegisteredTool>;
1670 messageRenderers: Map<string, MessageRenderer>;
1671 entryRenderers?: Map<string, EntryRenderer>;
1672 commands: Map<string, RegisteredCommand>;
1673 flags: Map<string, ExtensionFlag>;
1674 shortcuts: Map<KeyId, ExtensionShortcut>;
1675}
1676
1677/** Result of loading extensions. */
1678export interface LoadExtensionsResult {
1679 extensions: Extension[];
1680 errors: Array<{ path: string; error: string }>;
1681 /** Shared runtime - actions are throwing stubs until runner.initialize() */
1682 runtime: ExtensionRuntime;
1683}
1684
1685// ============================================================================
1686// Extension Error
1687// ============================================================================
1688
1689export interface ExtensionError {
1690 extensionPath: string;
1691 event: string;
1692 error: string;
1693 stack?: string;
1694}
1695