Skip to main content

Extension API reference

ExtensionContext

All handlers receive ctx: ExtensionContext.

ctx.ui

UI methods for user interaction. See Custom UI for full details.

ctx.hasUI

false in print mode (-p) and JSON mode. true in interactive and RPC mode. In RPC mode, dialog methods (select, confirm, input, editor) work via the extension UI sub-protocol, and fire-and-forget methods (notify, setStatus, setWidget, setTitle, setEditorText) emit requests to the client. Some TUI-specific methods are no-ops or return defaults (see RPC mode).

ctx.isPresentationOnly

true in the local presentation host when the isolated interactive engine owns execution. Keep UI registration active, but skip background execution and durable recovery in lifecycle hooks in this context to avoid starting the same work in both processes. Normal SDK, print, RPC, and engine contexts leave this flag false or unset.

ctx.cwd

Current working directory. Built-in cwd-sensitive tools (read, write, edit, search, find, ls, bash, powershell) resolve relative paths against ctx.cwd when an extension invokes them, falling back to the cwd captured when the tool was created. An extension that registers a tool and forwards its own context therefore gets paths resolved against the live session cwd rather than a stale one. Use CONFIG_DIR_NAME instead of hardcoding .atomic (or legacy .pi) when constructing project-local config paths. Rebranded distributions can use a different config directory name.

ctx.isProjectTrusted()

Returns whether project-local trust is active for the current session context. This includes temporary trust decisions and CLI trust overrides, not just saved decisions in the global trust store. Use this before reading project-local extension configuration that should only be honored for trusted projects.

ctx.sessionManager

Read-only access to session state. See Session Format for the full SessionManager API and entry types. For tool_call, this state is synchronized through the current assistant message before handlers run. In parallel tool execution mode it is still not guaranteed to include sibling tool results from the same assistant message.

ctx.modelRegistry / ctx.model / ctx.scopedModels

Access models, auth state, and provider-aware requests. Use ctx.modelRegistry.complete() for an extension model request that must use Atomic’s provider composition. It dispatches through the active ModelRuntime, retaining registered custom providers and resolved request auth: the credential-specific baseUrl, headers (including null suppression markers), and environment values. Use ctx.modelRegistry.generateImages(model, context, options) to run an image model with request-time authentication. It resolves to the generated image blocks and usage, and reports provider errors through stopReason and errorMessage instead of rejecting. For streaming requests, use ctx.modelRegistry.streamSimple(model, context, options) with provider-neutral options, or stream() with API-specific options. Both use configured providers and request-time authentication, including extension registrations. Iterate the returned AssistantMessageEventStream for events and await .result() for the final message. Setup failures produce error events and error results. The global compatibility streaming functions do not see extension provider registrations.
Use getApiKeyAndHeaders() only when an extension must inspect auth before dispatch; normal requests do not need to resolve or overlay auth themselves. OpenRouter Chat Completions and Anthropic Messages requests send x-session-id by default when sessionId is supplied and prompt caching is enabled. Set the model’s compat.sendSessionAffinityHeaders to false to opt out, or set cacheRetention: "none" on the request to disable cache-related affinity. Explicit request headers override generated headers. await ctx.modelRegistry.refresh(options) returns { aborted, errors }, not just completion. errors is a per-provider map, so extensions can report partial refresh failures; aborted reports cancellation. Host integrations that call ModelRuntime.setRuntimeApiKey(providerId, apiKey, options) must note that it records the runtime credential but does not refresh the catalog; call refresh({ providers: [providerId], signal }) explicitly when a fresh catalog is needed. ctx.scopedModels is the read-only list of models scoped to the current session — the same set the /scoped-models command shows. It is resolved from the --models CLI flag and the enabledModels setting, matched against the available catalogue. It is empty when no scoping is configured, meaning every available model is usable. Each entry is { model, thinkingLevel? }, where thinkingLevel is set only when a pattern pinned it (for example anthropic/*:high). Use it to populate a model picker that mirrors the built-in one instead of enumerating the whole catalogue. The value is resolved at access time, so it tracks session replacement. Under the isolated interactive engine it reflects the engine’s catalogue rather than a stale host snapshot. ctx.scopedModels reports scope but cannot change it. It is a getter with no setter, typed readonly ScopedModel[], so assigning to it or pushing an entry is a compile error. Each read also returns fresh, frozen copies of the array, every { model, thinkingLevel } entry, and each entry’s model. Even JavaScript extensions or code that casts away readonly cannot widen the scope or replace a selected model. Those mutations throw. Change scope through the commands and settings that own it.
Both types are exported: ScopedModel for one entry, ExtensionScopedModels for the accessor’s own type. They are declared at the public extension type path (core/extensions/types.ts) and re-exported from the package root, so an extension never reaches into an internal module to describe what it just read.

ctx.signal

The current agent abort signal, or undefined when no agent turn is active. Use this for abort-aware nested work started by extension handlers, for example:
  • fetch(..., { signal: ctx.signal })
  • model calls that accept signal
  • file or process helpers that accept AbortSignal
ctx.signal is typically defined during active turn events such as tool_call, tool_result, message_update, and turn_end. It is usually undefined in idle or non-turn contexts such as session events, extension commands, and shortcuts fired while Atomic is idle.

ctx.sessionSignal

A read-only AbortSignal for the session lifetime, available even while the agent is idle. It aborts when reload or shutdown begins, before Atomic drains admitted commands and emits session_shutdown. Unlike ctx.signal, it does not end when an agent turn finishes. A replacement session generation receives a new signal. Custom UI mounted through ctx.ui.custom is dismissed automatically when the session ends; an explicit caller signal can still dismiss it earlier. Use ctx.sessionSignal for other long-running command resources, such as browser windows, instead of waiting for session_shutdown, which cannot run until those commands settle. Cancellation should stop waiting for input, not abandon a session write already started: keep awaiting that write so teardown can drain it safely.

ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()

Control flow helpers. ctx.isIdle() reports whether the agent is streaming a response. It stays true while the session compacts, because a sendUserMessage() sent then waits for compaction to end instead of starting a turn.

ctx.isProjectTrusted()

Returns whether project-local trust is active for the current extension context. Use this before reading project-local config, loading project-local resources, or exposing actions that should only run after the user has trusted the cwd.

ctx.shutdown()

Request a graceful shutdown of Atomic.
  • Interactive mode: Deferred until the agent becomes idle (after processing all queued steering and follow-up messages).
  • RPC mode: Runs when the session is idle and all command responses have been written. An idle request does not need another client command to trigger it. Requests during startup wait for input setup; requests during a turn wait for it to settle. If an extension starts compaction outside a turn, shutdown waits for the next command or turn after compaction finishes.
  • Print mode: No-op. The process exits automatically when all prompts are processed.
Emits session_shutdown event to all extensions before exiting. Available in all contexts (event handlers, tools, commands, shortcuts).

ctx.getContextUsage()

Returns current context usage for the active model. Uses last assistant usage when available, then estimates tokens for trailing messages.

ctx.compact()

Trigger Atomic’s verbatim line compactor without awaiting completion. The planner emits numbered deleted-line ranges only; Atomic validates them and reconstructs retained text mechanically. Use compression_ratio (fraction of compactable lines to keep), client-side preserve_recent (an exact context-visible message count), and query to tune the run, compactionModel to pick an exact compaction model for this run without changing settings, and onComplete/onError for follow-up actions.
The planner cannot author context text: only validated line ranges enter the mechanical reconstruction path. The query parameter guides relevance selection inside the fixed prompt; it is not replacement prose. Extensions that need an offline replacement can return compactedText from session_before_compact.

ctx.getSystemPrompt()

Returns Atomic’s current system prompt string.
  • During before_agent_start, this reflects chained system-prompt changes made so far for the current turn.
  • It does not include later context message mutations.
  • It does not include before_provider_request payload rewrites.
  • If later-loaded extensions run after yours, they can still change what is ultimately sent.

ctx.getSkillCatalog()

Returns the current loader-owned skill catalog when the host provides one. Use it to resolve exact skill selectors, including source-qualified names such as tdd@builtin, without falling back to the bare precedence winner.
pi.getCommands() already includes the same advertised /skill:name and /skill:name@source names. See Skill Commands.

ExtensionToolContext

Tool execute callbacks receive an ExtensionToolContext, which extends ExtensionContext with tools and executeTool(name, args, options?). Event and command contexts do not provide these tool-only methods. See structured results and nested tools for permissions, cancellation, result hooks, and exclusive-call requirements.

ExtensionCommandContext

Command handlers receive ExtensionCommandContext, which extends ExtensionContext with session control methods. These are only available in commands because they can deadlock if called from event handlers.

ctx.waitForIdle()

Wait for the agent to finish streaming:

ctx.newSession(options?)

Create a new session:
Options:
  • parentSession: parent session ID to record in the new session header
  • setup: mutate the new session’s SessionManager before withSession runs
  • withSession: run post-switch work against a fresh replacement-session context. Do not use captured old pi / command ctx; see Session replacement lifecycle and footguns.

ctx.fork(entryId, options?)

Fork from a specific entry, creating a new session file:
Options:
  • position: "before" (default) forks before the selected user message, restoring that prompt into the editor
  • position: "at" duplicates the active path through the selected entry without restoring editor text
  • withSession: run post-switch work against a fresh replacement-session context. Do not use captured old pi / command ctx; see Session replacement lifecycle and footguns.

ctx.navigateTree(targetId, options?)

Navigate to a different point in the session tree. Navigation rejects while a response, compaction, or branch summarization is active, even with summarize: false. Rejection leaves the active branch unchanged. Wait for the active operation to finish and retry.
Options:
  • summarize: Whether to generate a summary of the abandoned branch
  • customInstructions: Custom instructions for the summarizer
  • replaceInstructions: If true, customInstructions replaces the default prompt instead of being appended
  • label: Label to attach to the branch summary entry (or target entry if not summarizing)

ctx.switchSession(sessionId, options?)

Switch to a different session by its exact ID:
Options: To discover available sessions, use the static SessionManager.list() or SessionManager.listAll() methods:

Session replacement lifecycle and footguns

withSession receives a fresh ReplacedSessionContext, which extends ExtensionCommandContext with async sendMessage() and sendUserMessage() helpers bound to the replacement session. Lifecycle and footguns:
  • withSession runs only after the old session has emitted session_shutdown, the old runtime has been torn down, the replacement session has been rebound, and the new extension instance has already received session_start.
  • The callback still executes in the original closure, not inside the new extension instance. That means your old extension instance may already have run its shutdown cleanup before withSession starts.
  • Captured old pi / old command ctx session-bound objects are stale after replacement and will throw if used. Use only the ctx passed to withSession for session-bound work.
  • Previously extracted raw objects are still your responsibility. For example, if you capture const sm = ctx.sessionManager before replacement, sm is still the old SessionManager object. Do not reuse it after replacement.
  • Code in withSession should assume any state invalidated by your session_shutdown handler is already gone. Only capture plain data that survives shutdown cleanly, such as strings, ids, and serialized config.
  • Long-lived callbacks that need to classify a stale API error should use isStaleExtensionContextError(error) from @bastani/atomic, not match the host’s error message.
Safe pattern:
Unsafe pattern:

ctx.reload()

Run the same reload flow as /reload.
Important behavior:
  • await ctx.reload() emits session_shutdown for the current extension runtime
  • It then reloads resources and emits session_start with reason: "reload" and resources_discover with reason "reload"
  • The currently running command handler still continues in the old call frame
  • Code after await ctx.reload() still runs from the pre-reload version
  • Code after await ctx.reload() must not assume old in-memory extension state is still valid
  • After the handler returns, future commands/events/tool calls use the new extension version
For predictable behavior, treat reload as terminal for that handler (await ctx.reload(); return;). Tools run with ExtensionContext, so they cannot call ctx.reload() directly. Use a command as the reload entrypoint, then expose a tool that queues that command as a follow-up user message. Example tool the LLM can call to trigger reload:

ExtensionAPI Methods

pi.on(event, handler)

Subscribe to events. See Events for event types and return values.

pi.registerTool(definition)

Register a custom tool callable by the LLM. See Custom Tools for full details. pi.registerTool() works both during extension load and after startup. You can call it inside session_start, command handlers, or other event handlers. New tools are refreshed immediately in the same session, so they appear in pi.getAllTools() and are callable by the LLM without /reload. Use pi.setActiveTools() to enable or disable registered tools, including dynamically added tools and Intercom. Tools suppressed by a session allowlist or exclusions cannot be activated this way. Use promptSnippet to opt a custom tool into a one-line entry in Available tools, and promptGuidelines to append tool-specific bullets to the default Guidelines section when the tool is active. Important: promptGuidelines bullets are appended flat to the Guidelines section with no tool name prefix. Each guideline must name the tool it refers to — avoid “Use this tool when…” because the LLM cannot tell which tool “this” means. Write “Use my_tool when…” instead. See dynamic-tools.ts for a full example.

Built-in tool prompt contributions

Atomic exports immutable prompt metadata for its built-in coding tools. Use these constants when a custom harness or tool registry needs the same prompt entries as the built-in factories:
Each contribution has a readonly snippet for the Available tools section and readonly guidelines for the active tool’s Guidelines entries. The seven exports are bash, edit, find, ls, read, search, and write; Atomic’s public search export is the corresponding surface for pi’s upstream grep tool. The built-in factories use these values directly, so consumers do not need to duplicate prompt text. Use Atomic’s export rather than importing StringEnum directly from Pi. It preserves Pi’s Google-compatible runtime schema while keeping the schema typed against Atomic’s direct TypeBox version.

pi.sendMessage(message, options?)

Inject a custom message into the session. The call returns void | Promise<void> for compatibility with synchronous hosts; use await Promise.resolve(pi.sendMessage(...)) when admission or routing failure must be observed. Atomic’s AgentSession runtime returns an admission receipt: it settles after the message is accepted by the local queue or workflow late-message route, without waiting for the resulting model turn to finish.
Options:
  • deliverAs - Delivery mode:
    • "steer" (default) - Queues the message while streaming. Delivered after the current assistant turn finishes executing its tool calls, before the next LLM call.
    • "followUp" - Waits for agent to finish. Delivered only when agent has no more tool calls.
    • "nextTurn" - Queued for next user prompt. Does not interrupt or trigger anything.
    • "interrupt" - With triggerTurn: true, aborts an active streaming turn and immediately starts a new turn with the custom message. When idle, behaves like a triggered custom message.
  • triggerTurn: true - If agent is idle, trigger an LLM response immediately. Required for "interrupt"; ignored for "nextTurn".
  • excludeFromContext: true - Render and persist the custom message without adding it to LLM context. With no deliverAs, this remains display-only even while the agent is streaming.
  • interruptAbortMessage - Optional text used to replace generic abort results (for example Operation aborted) when deliverAs: "interrupt" aborts an active turn.

pi.sendMessages(messages, options?)

Atomically admit a batch of custom messages in array order. The call returns void | Promise<void> for compatibility with synchronous hosts; use await Promise.resolve(pi.sendMessages(...)) when admission or routing failure must be observed. The promise is an admission receipt and does not wait for the resulting model turn. Admission is indivisible; use this when a prelude and terminal notice must stay contiguous without globally serializing other extension work.
The batch supports triggerTurn, excludeFromContext, and deliverAs: "steer" | "followUp" | "nextTurn". Interrupt delivery remains a single-message operation.

pi.sendUserMessage(content, options?)

Send a user message to the agent. Unlike sendMessage() which sends custom messages, this sends an actual user message that appears as if typed by the user. Always triggers a turn.
Options:
  • deliverAs - Required when agent is streaming:
    • "steer" - Queues the message for delivery after the current assistant turn finishes executing its tool calls
    • "followUp" - Waits for agent to finish all tools
  • expandPromptTemplates - Dispatch extension commands and expand skill commands and prompt templates instead of sending the text literally. Defaults to false, so an extension-authored message is sent as-is unless it opts in; an unknown command falls through to a literal send.
When not streaming, the message is sent immediately and triggers a new turn. When streaming without deliverAs, throws an error. While the session compacts, the message waits, shows as queued, and is sent once compaction ends, after messages sent before it; if one of those has started a turn by then, a message without deliverAs is queued behind it as steering. See send-user-message.ts for a complete example.

pi.appendEntry(customType, data?)

Persist extension state (does NOT participate in LLM context).
Await pi.appendEntry before using the new entry. The promise resolves after the write commits and rejects with the store’s typed error if it fails. Setup entries also commit before the promise resolves. Appending emits entry_appended with the committed entry. This lets extensions react to session entries without polling. During a transactional reload’s session_start handler, pi.appendEntry, pi.setSessionName, and pi.setLabel stage their writes instead. Awaiting them confirms staging; the writes and their notifications are published in call order only after reload succeeds. Rejected reloads discard these changes. Do not depend on reading the staged changes back during that handler.

pi.registerEntryRenderer(customType, renderer)

Register a TUI renderer for durable custom entries created by pi.appendEntry(). These entries render in the transcript but do not enter model context. SDK hosts can inspect the original callback with session.extensionRunner.getEntryRenderer(customType). To render through the owning session, call session.extensionRunner.renderEntry(customType, entry, { expanded }, theme); retired runners reject rendering.

pi.setSessionName(name)

Set the session display name (shown in session selector instead of first message).

pi.getSessionName()

Get the current session name, if set.

pi.setLabel(entryId, label)

Set or clear a label on an entry. Labels are user-defined markers for bookmarking and navigation (shown in /tree selector).
Labels persist in the session and survive restarts. Use them to mark important points (turns, checkpoints) in the conversation tree.

pi.registerCommand(name, options)

Register a command. If multiple extensions register the same command name, Atomic keeps them all and assigns numeric invocation suffixes in load order, for example /review:1 and /review:2.
Optional: add argument auto-completion for /command ...:

pi.getCommands()

Get the slash commands available for invocation via prompt in the current session. Includes extension commands, prompt templates, and skill commands. The list matches the RPC get_commands ordering: extensions first, then templates, then skills.
Each entry has this shape:
Use sourceInfo as the canonical provenance field. Do not infer ownership from command names or from ad hoc path parsing. Built-in interactive commands (like /model and /settings) are not included here. They are handled only in interactive mode and would not execute if sent via prompt.

pi.registerMessageRenderer(customType, renderer)

Register a custom TUI renderer for messages with your customType. The renderer options contain expanded and the current numeric outputPad, so custom output can align with built-in messages. The same options are provided in normal and isolated-engine rendering. See Custom UI.

pi.registerMarkdownTransformer(transformer)

Register a synchronous, display-only transformer for Markdown before Atomic renders it in interactive mode. Atomic runs transformers in extension load order. Each extension retains one transformer, so a later call from that extension replaces its prior transformer. Each transformer receives the Markdown returned by the prior transformer, then Atomic renders the final value with its built-in Markdown renderer. The optional second argument, pi.registerMarkdownTransformer(transformer, options), takes options.messageTypes. By default a transformer runs on normal user text, assistant text, and thinking blocks; messageTypes lists the surfaces it runs on instead: The transformer receives the Markdown string and a context with:
  • messageType — one of the surfaces above
  • isStreaming — true for partial assistant updates; false otherwise
  • availableWidth — exact terminal columns available for the transformed Markdown content
  • messageId — the session entry id when the message is rendered from the saved session (resume, /tree, rebuilds); undefined for live messages and the changelog
  • timestamp — the session entry’s ISO timestamp under the same conditions; undefined for live messages and the changelog
Return the transformed Markdown:
If a transformer throws, Atomic keeps the Markdown produced so far and continues with the next transformer. The hook never changes the original message, session transcript, or model context. It runs for new messages, assistant streaming updates, restored session messages, and terminal-width changes, so keep transformers synchronous and inexpensive. Isolated-engine rendering does not run host-side display transformers.

pi.registerShortcut(shortcut, options)

Register a keyboard shortcut. See Keybindings for the shortcut format and built-in keybindings.
Set preferEditor: true to skip a shortcut when another resolved editor binding uses that key. Omitting it preserves the usual extension conflict policy. Optional keybinding selects the keys for an existing named action from keybindings.json instead of the literal registration key; an empty action binding disables it. The builtin workflow extension uses app.workflows.scrollUp and app.workflows.scrollDown with preferEditor: true, so custom Vim editor bindings take precedence.

pi.registerFlag(name, options)

Register a CLI flag.

pi.getWorkflowHostModules()

Returns a promise for the running host’s in-memory module namespaces. The workflows adapter uses this capability to avoid re-evaluating the host’s package graphs for each workflow file. The returned object has exactly these keys:
  • @bastani/atomic
  • @bastani/pi-ai, backed by the host’s compatibility API
  • @bastani/pi-ai/providers/all
These are shared host instances, not isolated copies. Workflow imports use the host’s versions rather than project-local copies of these exact specifiers, preserving identity for exports such as AuthStorage and sharing module state. This also supports standalone-binary projects without those packages in node_modules. Other deep subpaths and third-party dependencies still resolve from disk. Workflow files and their relative imports are evaluated fresh on /workflow reload. getWorkflowHostModules() is required on the current Atomic ExtensionAPI. It is optional on the workflows adapter’s host interface so older or alternative hosts can omit it. Integrations that support those hosts should check for the method before calling it, for example await pi.getWorkflowHostModules?.(). Without it, the workflows adapter retains filesystem package resolution and needs resolvable packages for these imports.

pi.exec(command, args, options?)

Execute a command without a shell.
Options: signal and timeout terminate the command, cwd sets the working directory, and maxBuffer limits stdout and stderr combined (default 256 MiB). When output exceeds maxBuffer, Atomic terminates the command and returns the output up to the limit with truncated: true, killed: true, and a nonzero code. killed is true whenever Atomic terminated the command: timeout, abort, or maxBuffer. A command that ignores SIGTERM gets SIGKILL after 5 seconds.

pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)

Manage active tools. This works for both built-in tools and dynamically registered tools. pi.getActiveTools() returns the active tool names as string[]; pi.getAllTools() returns metadata for all configured tools.
pi.getAllTools() returns name, description, parameters, promptGuidelines, exposure, namespace, annotations, and sourceInfo. See tool exposure before changing activation for script-callable tools. Typical sourceInfo.source values:
  • builtin for built-in tools
  • sdk for tools passed via createAgentSession({ customTools })
  • extension source metadata for tools registered by extensions

pi.setModel(model)

Set the model for the current session. The change is recorded in session history and restored when that session is resumed, but it does not change the configured defaultProvider or defaultModel used by new sessions. Returns false if authentication is not configured for the model’s provider. See Custom models for configuring custom models.

pi.getThinkingLevel() / pi.setThinkingLevel(level)

Get the current thinking level. Level is clamped to model capabilities (non-reasoning models always use "off"; "xhigh" and "max" require model support). Changes emit thinking_level_select. pi.setThinkingLevel() changes the thinking level for the current session. The change is recorded in session history and restored when that session is resumed, but it does not change the configured default used by new sessions.

pi.events

Shared event bus for communication between active extensions. A subscription made with on() is removed automatically when its extension reloads or the session disposes. Use the returned function if you need to stop listening sooner:
pi.events belongs to the extension instance that received it. Register listeners again when that instance reloads, and do not retain the object for later use: calling on() or emit() through a captured handle after reload or disposal throws. To keep in-memory state across /reload, pass this facade to sessionScopedExtensionState; do not capture the facade itself. If you implement an ExtensionRuntime for an embedded host, provide trackEventBusSubscription(unsubscribe) and retain each returned subscription until that extension runtime is reloaded or disposed. ExtensionUIContext.getChatRenderSettings() must return markdownTransformers; it may also return renderLatex to control terminal math rendering. These fields keep event subscriptions and display transforms scoped to the active extension instance.

Native providers

In addition to registerProvider(name, config), extensions can register a complete native Provider from @bastani/pi-ai with pi.registerProvider(provider). Use the native overload for provider-owned authentication, catalog refresh, and transport behavior; use the config overload for ordinary proxies and custom endpoints.

pi.registerProvider(name, config)

Register or override a model provider dynamically. Await registration, aliases, and removal before starting dependent work. Useful for proxies, custom endpoints, or team-wide model configurations. Calls made during the extension factory function are queued and applied once the runner initialises. Calls made after that — for example from a command handler following a user setup flow — take effect immediately without requiring a /reload. If you need to discover models from a remote endpoint, prefer an async extension factory over deferring the fetch to session_start. Atomic waits for the factory before startup continues, so the registered models are available immediately, including to atomic --list-models.
OAuth refreshToken receives a signal that Atomic does not abort on a timeout: once the token request is sent, the provider may already have rotated the refresh token, and only its response carries the replacement. The caller stops waiting after 60 seconds, but the exchange keeps running and Atomic saves its result when it arrives. When Atomic exits normally, it waits up to three seconds for an exchange still in flight. Cancelling a request or model refresh likewise stops only that caller’s wait. Do not add your own timeout or abort to the token request, and do not send the same refresh token again after the provider may have received it. Return the rotated credentials, even when the caller that started the refresh has stopped waiting. Config options:
  • name - Display name for the provider in UI such as /login.
  • baseUrl - API endpoint URL. Required when defining models.
  • apiKey - API key literal or explicit environment variable reference ($ENV_VAR or ${ENV_VAR}). Required when defining models (unless oauth provided).
  • api - API type: "anthropic-messages", "openai-completions", "openai-responses", etc.
  • headers - Custom headers to include in requests.
  • authHeader - If true, adds Authorization: Bearer header automatically.
  • models - Array of model definitions. If provided, replaces all existing models for this provider. Model definitions can set baseUrl to override the provider endpoint for that model.
  • oauth - OAuth provider config for /login support. When provided, the provider appears in the login menu.
  • auth.apiKey - Provider-owned API-key or connection setup for /login. Its name appears in the provider list and login({ signal, prompt }) returns the credential Atomic persists. Extension providers registered only in the isolated interactive engine child are synchronized into the host’s /login list; their login callback and credential-dependent model refresh still execute in the child, while prompts are rendered by the terminal host.
  • streamSimple - Custom streaming implementation for non-standard APIs.
See Custom providers for advanced topics: custom streaming APIs, OAuth details, model definition reference.

pi.unregisterProvider(name)

Remove a previously registered provider and its models. Built-in models that were overridden by the provider are restored. Has no effect if the provider was not registered. Like registerProvider, this takes effect immediately when called after the initial load phase, so a /reload is not required.

pi.registerMcpServer(name, config)

Contribute an MCP server whose configuration is only known at runtime, such as the URL of a local service the extension starts. config uses the same schema as an entry in mcp.json, including url, command/args/env/cwd, headers, OAuth settings, exposure, and per-request timeout in seconds. URLs support ${VAR} and $env:VAR interpolation.
Call it from the extension factory or from session_start. Load order does not matter: registrations made after the MCP adapter has started are picked up without a reload. Registering the same name again replaces the earlier registration. Registered servers are the lowest-precedence MCP layer. They replace a same-named server from a package manifest, but a user or trusted-project MCP config entry replaces them. Set "enabled": false on a complete file-configured entry to keep it without connecting. /mcp shows the registering extension next to the server name. pi.getMcpServerContributions() returns the package-manifest and registered servers for the session, each with its name, config, origin ("package" or "extension"), and sourceInfo.

Error Handling

  • Extension errors are logged, agent continues
  • tool_call errors block the tool (fail-safe)
  • Tool execute errors must be signaled by throwing; the thrown error is caught, reported to the LLM with isError: true, and execution continues