API reference
A curated map of core and experimental Pi package entry points at version 1.0.0.
This page was reviewed against Pi v1.0.0, commit a13d35a742c6ef8462812a28fbe1d8c8b7431c32.
This curated integration reference omits exhaustive specialist and UI exports. It targets upstream commit a13d35a742c6ef8462812a28fbe1d8c8b7431c32, the package roots at 1.0.0, and Node.js 22.19 or newer.
@earendil-works/pi-aiowns provider collections, model metadata, authentication, messages, and LLM streams.@earendil-works/pi-agent-coreadds the agent loop, tool execution, state, queues, and lifecycle events.@earendil-works/pi-coding-agentassembles sessions, settings, resources, extensions, coding tools, and CLI or SDK runtimes.@earendil-works/pi-client,@earendil-works/pi-protocol, and@earendil-works/pi-serverexpose the optional experimental routed-service boundary.
The root of pi-ai is side-effect free. Provider factories live under providers/*, wire-protocol implementations under api/*, and the retired global catalog helpers under compat. New integrations should not use compat.
@earendil-works/pi-ai
Provider collections
createModels() returns an empty mutable Models collection. Add only the providers you ship, or use builtinModels() from providers/all when bundle size is not a concern.
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
const models = createModels();
models.setProvider(anthropicProvider());
const model = models.getModel("anthropic", "claude-sonnet-4-6");
if (!model) throw new Error("Model is not in the registered catalog");MutableModels adds setProvider(), deleteProvider(), and clearProviders() to the read and request methods on Models. Providers, not a process-wide registry, own catalogs and request routing.
Catalog lookup and authentication
getProviders(), getProvider(), getModels(), and getModel() are synchronous reads of the last-known catalog. refresh() restores or refreshes dynamic providers and returns { aborted, errors }; it does not reject merely because one provider failed. checkAuth(), getAuth(), and getAvailable() resolve provider-scoped availability. login() and logout() use the collection's credential store.
import type { Api, Model } from "@earendil-works/pi-ai";
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const models = builtinModels();
const cached: readonly Model<Api>[] = models.getModels("anthropic");
const refresh = await models.refresh({
providers: ["radius"],
allowNetwork: false,
});
const available = await models.getAvailable("anthropic");
console.log(cached.length, refresh.errors.size, available.length);Provider factories normally resolve stored credentials first and then provider environment variables or ambient credentials. Applications that need persistence supply a CredentialStore and, for dynamic catalogs, a ModelsStore to createModels().
Provider factories and adapters
Use a factory from providers/* to add an existing provider. Use createProvider() only when defining a provider or combining a catalog with a wire-protocol implementation. The following local OpenAI-compatible provider uses the public lazy API subpath and standard environment-key resolver.
import {
createModels,
createProvider,
envApiKeyAuth,
type Model,
} from "@earendil-works/pi-ai";
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
const localModel = {
id: "local-chat",
name: "Local Chat",
api: "openai-completions",
provider: "local",
baseUrl: "http://127.0.0.1:8080/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 32_768,
maxTokens: 4_096,
} satisfies Model<"openai-completions">;
const local = createProvider({
id: "local",
auth: { apiKey: envApiKeyAuth("Local API key", ["LOCAL_API_KEY"]) },
models: [localModel],
api: openAICompletionsApi(),
});
const models = createModels();
models.setProvider(local);A native Provider supplies identity, auth, getModels(), optional refreshModels(), and stream methods. createProvider({ fetchModels }) handles a dynamic overlay. A new protocol adapter must return an AssistantMessageEventStream and follow its terminal-event contract.
ProviderStreams.stream() and streamSimple() receive a normalized TranscriptContext. Only Pi normalization through normalizeContext() produces this branded type; callers must not cast a raw Context into it. Models.stream*() accepts the public Context shorthand and performs normalization before dispatching to the provider.
The transcript's system messages carry prompt and Tool declarations. Replay them in order: content adds instructions, sections replaces or removes named prompt sections, and toolsAdded / toolsRemoved changes the available Tool set. Use getCurrentSystemPrompt(context.messages) and getCurrentTools(context.messages) to derive current request state. Preserve system-message positions when the transport supports them; Pi's transcript helpers can collapse that state for APIs that do not.
import {
getCurrentSystemPrompt,
getCurrentTools,
type TranscriptContext,
} from "@earendil-works/pi-ai";
function inspectProviderContext(context: TranscriptContext) {
return {
systemPrompt: getCurrentSystemPrompt(context.messages),
tools: getCurrentTools(context.messages),
};
}Streaming and completion
stream() and complete() accept API-specific options. streamSimple() and completeSimple() accept portable reasoning, retry, transport, abort, and payload/response hooks, then translate those options for the selected API.
import type { Context } from "@earendil-works/pi-ai";
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const models = builtinModels();
const model = models.getModel("openai", "gpt-4o-mini");
if (!model) throw new Error("Model not found");
const context: Context = {
messages: [{ role: "user", content: "Reply in one sentence.", timestamp: Date.now() }],
};
const stream = models.streamSimple(model, context);
for await (const event of stream) {
if (event.type === "text_delta") process.stdout.write(event.delta);
}
const finalMessage = await stream.result();
console.log(finalMessage.stopReason, finalMessage.usage.cost.total);The stream itself encodes request failures: inspect the final error event or the resolved message's stopReason and errorMessage. Pass an AbortSignal in the options to cancel a request.
Model metadata
Model<TApi> is the request descriptor. Required fields are id, name, api, provider, baseUrl, reasoning, supported input, per-million-token cost, contextWindow, and maxTokens. Optional metadata includes thinkingLevelMap, samplingParams, headers, and API-specific compat flags.
import type { Model } from "@earendil-works/pi-ai";
const model = {
id: "local-chat",
name: "Local Chat",
api: "openai-completions",
provider: "local",
baseUrl: "http://127.0.0.1:8080/v1",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 32_768,
maxTokens: 4_096,
compat: { supportsDeveloperRole: false },
} satisfies Model<"openai-completions">;
console.log(model.provider, model.contextWindow);Google exposes two public thinking-level types from @earendil-works/pi-ai. GoogleApiThinkingLevel is the API-facing union "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LOW" | "MEDIUM" | "HIGH", matching the enum-like values accepted by GoogleOptions.thinking.level and GoogleVertexOptions.thinking.level. ResolvedGoogleThinkingLevel is the normalized adapter union "minimal" | "low" | "medium" | "high"; it represents the result after model-level resolution and is appropriate for internal mapping tables, not request options.
import type {
GoogleApiThinkingLevel,
GoogleOptions,
ResolvedGoogleThinkingLevel,
} from "@earendil-works/pi-ai";
const apiLevel: GoogleApiThinkingLevel = "HIGH";
const options = {
thinking: { enabled: true, level: apiLevel },
} satisfies GoogleOptions;
const normalizedBudgets: Record<ResolvedGoogleThinkingLevel, number> = {
minimal: 1_024,
low: 2_048,
medium: 8_192,
high: 16_384,
};
void [options, normalizedBudgets];The following selected declarations preserve the exact optional member signatures published by Pi 1.0.0; they do not reproduce the interfaces' other members:
export interface OpenAICompletionsCompat {
vllmPriority?: number;
}
export interface OpenAIResponsesCompat {
supportsMaxOutputTokens?: boolean;
}
export interface AnthropicMessagesCompat {
supportsMidConvoEffort?: boolean;
}vllmPriority belongs only to OpenAICompletionsCompat: lower values are handled earlier, the vLLM server default is 0, and the field matters only with --scheduling-policy priority. It is off by default and is not set on the generated model catalog.
supportsMaxOutputTokens belongs to OpenAIResponsesCompat and defaults to true; set it to false when a Responses-compatible gateway rejects max_output_tokens. supportsMidConvoEffort belongs to AnthropicMessagesCompat and defaults to false. For built-in models in the Pi 1.0.0 generated catalog, automatic detection lowercases modelId first, then strips one optional prefix matching ^~?anthropic/ (anthropic/ or ~anthropic/). Pi auto-enables the flag only when provider is exactly anthropic or openrouter. The normalized ID must match exactly ^claude-opus-5(?:-\d{8})?$ or ^claude-(?:fable|mythos)-5(?:[.-]1)(?:-\d{8})?$. The exact supported model must still use a faithful Anthropic Messages transport; this is not support for all Anthropic-compatible providers or an API that merely imitates the Messages shape.
The accepted normalized variants are claude-opus-5, optionally followed by -YYYYMMDD; claude-fable-5.1 or claude-fable-5-1, each optionally dated; and claude-mythos-5.1 or claude-mythos-5-1, each optionally dated.
For a custom Model using anthropic-messages, set compat.supportsMidConvoEffort: true only after verifying both a faithful Anthropic Messages transport and the same exact compatible Claude model family. A provider name outside the generated-catalog allowlist does not by itself forbid manual configuration; never generalize this exception to arbitrary Anthropic-compatible providers or models.
Cost tiers, when present, compare input + cacheRead + cacheWrite with inputTokensAbove; the highest matching threshold prices the whole request.
Context, messages, and tools
Context contains an optional systemPrompt, Message[], and optional Tool[]. Message is the union of system, user, assistant, and tool-result messages. Provider implementations receive those messages inside TranscriptContext. Tool parameters are TypeBox schemas; validate arguments before executing tools when working below Agent Core.
import { Type, type Context, type Tool } from "@earendil-works/pi-ai";
const search = {
name: "search",
description: "Search indexed documents",
parameters: Type.Object({ query: Type.String() }),
} satisfies Tool;
const context: Context = {
systemPrompt: "Cite the matching document.",
messages: [{ role: "user", content: "Find the release note.", timestamp: Date.now() }],
tools: [search],
};
console.log(context.tools?.[0]?.name);AssistantMessage content contains text, thinking, or tool-call blocks and carries usage, cost, stop reason, and optional error or deferred-response metadata. Persist opaque signatures unchanged when replaying a conversation.
In Pi 1.0.0, ToolCall.arguments is a JsonObject, and ToolResultMessage.details contains JSON-compatible data. Keep custom Tool inputs and persisted details serializable: encode dates as strings and keep functions, class instances, and process handles outside the transcript. The Tool's schema still determines which JSON argument shapes it accepts.
ToolResultMessage<TDetails = JsonValue> is a conditional type. For a compatible detail type it exposes details?: JsonRepresentation<TDetails>; an incompatible type resolves to never. Use a concrete JSON-compatible detail type and handle an absent details value. The runtime AgentToolResult<TDetails> remains a separate generic contract; assigning a detail type there does not prove that it can be persisted as a Tool result message.
JsonValue includes readonly JsonValue[]. Consumers must copy an array before mutating it, or accept a readonly parameter. Exhaustive TypeScript handling must cover null, primitives, readonly arrays, and objects; use a type guard that narrows to readonly JsonValue[] when needed. These declarations add no runtime validation or freezing: still check untrusted values and reject cycles or other data your serialization cannot represent.
Stream events
AssistantMessageEventStream is both an async iterable and a holder for result(). A conforming stream starts once and terminates once.
| Event | Payload and use |
|---|---|
start | Initial partial assistant message |
text_start / text_delta / text_end | Text block lifecycle and incremental text |
thinking_start / thinking_delta / thinking_end | Thinking block lifecycle when the model emits it |
toolcall_start / toolcall_delta / toolcall_end | Partial arguments and the parsed, completed Tool call; Agent Core validates arguments before execution |
done | Successful terminal event with reason stop, length, toolUse, or deferred |
error | Terminal error or aborted assistant message |
@earendil-works/pi-agent-core
Agent
Agent is the stateful wrapper for the low-level loop. It owns the transcript, tool execution, steering and follow-up queues, and event delivery. Its streamFn can be models.streamSimple.bind(models).
import { Agent } from "@earendil-works/pi-agent-core";
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const models = builtinModels();
const model = models.getModel("anthropic", "claude-sonnet-4-6");
if (!model) throw new Error("Model not found");
const agent = new Agent({
initialState: { systemPrompt: "Be concise.", model },
streamFn: models.streamSimple.bind(models),
});
await agent.prompt("Explain this module.");prompt() starts a run; continue() resumes when the last message is user or tool-result. Use steer() for the next turn and followUp() after the loop would otherwise stop.
agentLoop()
agentLoop(prompts, context, config, signal, streamFn) is stateless with respect to your application. It returns EventStream<AgentEvent, AgentMessage[]>; iterate events, then await result() for the new messages. agentLoopContinue() reuses a context whose last message can continue.
import { agentLoop, type AgentContext } from "@earendil-works/pi-agent-core";
import type { Message } from "@earendil-works/pi-ai";
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const models = builtinModels();
const model = models.getModel("openai", "gpt-4o-mini");
if (!model) throw new Error("Model not found");
const rawContext: AgentContext = {
messages: [
{
role: "system",
content: "Be exact.",
timestamp: Date.now(),
},
],
tools: [],
};
const prompt = { role: "user" as const, content: "Summarize the API.", timestamp: Date.now() };
const events = agentLoop(
[prompt],
rawContext,
{
model,
convertToLlm: (messages) =>
messages.filter(
(message): message is Message =>
message.role === "system" || message.role === "user" || message.role === "assistant" || message.role === "toolResult",
),
},
undefined,
models.streamSimple.bind(models),
);
for await (const event of events) console.log(event.type);
const newMessages = await events.result();AgentContext.tools contains executable AgentTool implementations. It is distinct from the Tool declarations recorded by the leading SystemMessage; convertToLlm must preserve that system message so the provider receives the prompt and transcript Tool state through TranscriptContext.
AgentLoopConfig
The required fields are model and convertToLlm. Optional hooks transform context, resolve keys, prepare or finalize a turn, or intercept Tool calls. Portable stream options, queue sources, retry limits, and toolExecution: "parallel" | "sequential" are also accepted. finishTurn runs before turn_end, while its decision takes effect afterward. Error and aborted responses remain hard exits.
import type { FinishTurn } from "@earendil-works/pi-agent-core";
declare const shouldEnd: (message: Parameters<FinishTurn>[0]["message"]) => boolean;
const finishTurn: FinishTurn = ({ message }) => {
if (message.stopReason === "error" || message.stopReason === "aborted") {
return undefined;
}
return shouldEnd(message) ? { action: "end" } : undefined;
};import type { AgentLoopConfig } from "@earendil-works/pi-agent-core";
import type { Message } from "@earendil-works/pi-ai";
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const models = builtinModels();
const model = models.getModel("openai", "gpt-4o-mini");
if (!model) throw new Error("Model not found");
const config = {
model,
convertToLlm: (messages) =>
messages.filter(
(message): message is Message =>
message.role === "system" || message.role === "user" || message.role === "assistant" || message.role === "toolResult",
),
toolExecution: "parallel",
finishTurn: ({ message }) =>
message.stopReason === "error" || message.stopReason === "aborted"
? undefined
: { action: "end" },
} satisfies AgentLoopConfig;
console.log(config.toolExecution);convertToLlm must filter or convert custom AgentMessage variants and must not reject. The same safe-fallback rule applies to transformContext and dynamic key resolution.
AgentTool
AgentTool extends the Pi AI tool schema with a UI label and execute(toolCallId, params, signal, onUpdate). Return model-visible content and structured details; throw to produce an error tool result. onUpdate emits partial progress.
import { Type } from "@earendil-works/pi-ai";
import type { AgentTool } from "@earendil-works/pi-agent-core";
const parameters = Type.Object({ path: Type.String() });
const inspectFile: AgentTool<typeof parameters, { path: string }> = {
name: "inspect_file",
label: "Inspect file",
description: "Inspect one file",
parameters,
async execute(_toolCallId, { path }, signal, onUpdate) {
signal?.throwIfAborted();
onUpdate?.({ content: [{ type: "text", text: `Opening ${path}` }], details: { path } });
return { content: [{ type: "text", text: `Inspected ${path}` }], details: { path } };
},
};
console.log(inspectFile.name);Multiple calls run in parallel by default, but a tool can request sequential execution. Completion events may arrive out of source order; final tool-result messages remain in assistant source order.
State, control, and events
agent.state exposes the current system prompt, model, thinking level, tools, messages, streaming message, pending tool-call IDs, and latest error. subscribe() accepts sync or async listeners and returns an unsubscribe function; Agent awaits listeners in registration order.
import type { Agent } from "@earendil-works/pi-agent-core";
export function observe(agent: Agent): () => void {
return agent.subscribe(async (event, signal) => {
if (signal.aborted) return;
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
if (event.type === "tool_execution_end") console.log(event.toolCallId, event.isError);
});
}Runs emit agent_start/agent_end; each model turn emits turn_start/turn_end; messages emit start/update/end; tools emit execution start/update/end. abort() signals the active run. waitForIdle() settles after the final awaited listener. Agent Core also exports harness and session primitives; use Coding Agent's SessionManager when you need Pi's JSONL coding-session format.
@earendil-works/pi-coding-agent
createAgentSession() and AgentSession
createAgentSession(options?) resolves a ModelRuntime, SessionManager, SettingsManager, ResourceLoader, tools, and extensions, then returns { session, extensionsResult, modelFallbackMessage? }. Prompting requires a configured model and credential.
import {
createAgentSession,
ModelRuntime,
SessionManager,
} from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const { session, modelFallbackMessage } = await createAgentSession({
modelRuntime,
sessionManager: SessionManager.inMemory(process.cwd()),
tools: ["read"],
});
const unsubscribe = session.subscribe((event) => {
if (event.type === "agent_settled") console.log("idle");
});
try {
if (modelFallbackMessage) console.warn(modelFallbackMessage);
await session.prompt("Describe the current directory.");
} finally {
await session.abort();
unsubscribe();
session.dispose();
}An AgentSession adds synchronous subscriptions, persistence, compaction, retries, bash execution, model selection, reload, and extension dispatch around Agent. Await abort() to cancel retry and agent work and wait for idle; call dispose() when the host is finished.
Persistence, settings, and resources
SessionManager.create(cwd, sessionDir?, options?),continueRecent(cwd, sessionDir?),open(path, sessionDir?, cwdOverride?),inMemory(cwd?, options?, entries?),forkFrom(sourcePath, targetCwd, sessionDir?, options?), andlist(cwd, sessionDir?, onProgress?)manage append-only JSONL sessions and their trees. UselistAll(onProgress?)orlistAll(sessionDir?, onProgress?)across projects.SettingsManager.create(cwd, agentDir?)merges global and trusted project settings;SettingsManager.inMemory()is useful for embedded hosts and tests.new DefaultResourceLoader({ cwd, agentDir, settingsManager? })constructs a loader that discovers context files, system prompts, extensions, skills, prompt templates, and themes afterreload().ModelRuntime.create()owns the provider catalog and synchronized credentials used by Coding Agent.
External-session restoration
FileEntry, SessionHeader, SessionEntry, and NewSessionOptions are public package-root types. The declarations relevant to restoring application-owned entries are exactly:
export interface NewSessionOptions {
id?: string;
parentSession?: string;
}
export type FileEntry = SessionHeader | SessionEntry;
export declare class SessionManager {
static inMemory(
cwd?: string,
options?: NewSessionOptions,
entries?: FileEntry[],
): SessionManager;
}Passing entries restores the parent-linked tree without enabling Pi file persistence. The caller owns validation, durable writes, and synchronization with its external store. parseSessionEntries() and migrateSessionEntries() are also exported, but parsing skips malformed JSON lines and migration mutates its array; neither is a general trust-boundary schema validator.
Canonical session context and append-only edits
SessionManager owns the canonical projection used for future provider requests. Use session.navigateTree() for tree movement. After an application appends directly through session.sessionManager, call session.refreshContext(); assigning a message array on the underlying Agent does not replace the manager projection.
| Operation | Stored change | Projection effect |
|---|---|---|
appendContextEdit(targetEntryId, null) | append context_edit | omit target from future provider context |
appendContextEdit(targetEntryId, { content }) | append context_edit | replace target content in future provider context |
raw transcript / UI history | append-only | unchanged |
returned editId | new context_edit entry | not target entry |
ContextEditEntry is part of the exported SessionEntry union, so exhaustive switches must handle context_edit. The replacement object has the exact shape { content }; null means omission. Both forms append an edit entry and leave the target entry intact. appendCompaction(summary, null, tokensBefore) is the retain-none form: the generated compaction entry ID becomes the effective retained boundary, so no preceding entry is kept in the next projection.
Compaction exports and failure boundary
The package root exports DEFAULT_COMPACTION_SETTINGS, shouldCompact(), compact(), generateSummary(), generateSummaryWithUsage(), generateBranchSummary(), and the related public result, settings, preparation, and file-operation types. It does not export the internal getSummarizationFailure() helper. Built-in compaction, turn-prefix, and branch-summary generation nevertheless apply that check internally: a response ending with stopReason: "length" is incomplete and is not persisted as a summary checkpoint. Branch summary generation now requests at most 4,096 output tokens, further bounded by a smaller positive model limit.
The Settings fragment below is a selected exact source-level internal settings.json shape; it is not exported or importable public API, while SettingsManager and selected settings types are the public importable surface of @earendil-works/pi-coding-agent 1.0.0.
interface Settings {
defaultThinkingLevel?: ThinkingLevel;
modelThinkingLevels?: Record<string, ThinkingLevel>;
showCacheMissNotices?: boolean;
}
declare class SettingsManager {
getDefaultThinkingLevel(): ThinkingLevel | undefined;
setDefaultThinkingLevel(level: ThinkingLevel): void;
getModelThinkingLevel(provider: string, modelId: string): ThinkingLevel | undefined;
getAllModelThinkingLevels(): Record<string, ThinkingLevel>;
setModelThinkingLevel(provider: string, modelId: string, level: ThinkingLevel): void;
removeModelThinkingLevel(provider: string, modelId: string): void;
}modelThinkingLevels stores per-model startup choices keyed by provider/modelId; the corresponding SettingsManager methods accept the provider and model ID separately. defaultThinkingLevel remains the global startup fallback, can be saved with Ctrl+S in /thinking, and is distinct from provider request fields.
When showCacheMissNotices is enabled, the transcript can also surface provider recovery diagnostics such as dropped Anthropic thinking blocks, in addition to significant cache misses and summary usage.
The package root also exports CacheWarmingMode, CompactionModelOverride, and CompactionSettings. SettingsManager.getCacheWarmingMode() resolves "off" | "streaming" | "idle"; setCacheWarmingMode() changes that global, cost-bearing setting. getCompactionSettings(model?) resolves reserveTokens and keepRecentTokens through compaction.modelOverrides for the exact model, then the ordinary compaction setting, then the built-in default.
Direct SDK hosts own cwd, trust, storage, and cleanup policy. Do not mutate session JSONL while a manager is active, and do not assume that SettingsManager.create() reproduces CLI trust resolution without the host supplying that decision.
Image MIME detection
detectSupportedImageMimeTypeFromFile() is exported from the @earendil-works/pi-coding-agent package root with this exact published signature:
declare function detectSupportedImageMimeTypeFromFile(
filePath: string,
): Promise<string | null>;detectSupportedImageMimeTypeFromFile() opens the filename supplied by filePath, reads at most the first 4,100 bytes, and checks file-header signatures plus the limited structural fields needed to detect exactly image/jpeg, image/png for non-animated PNG, image/gif, image/webp, or image/bmp. It returns null for unsupported or undetectable content; filesystem open or read failures still reject the promise. This detection is based on file content, not the filename extension, and does not decode, resize, or fully validate the image. Use the result as an input-format gate, not as proof that an image is fully decodable or safe.
Extensions and managed tools
An extension is an ExtensionFactory receiving the exported ExtensionAPI. Register tools, commands, shortcuts, flags, providers, and event handlers through that object; there is no global registerExtension().
import { Type } from "@earendil-works/pi-ai";
import { defineTool, type ExtensionFactory } from "@earendil-works/pi-coding-agent";
const inspectPath = defineTool({
name: "inspect_path",
label: "Inspect path",
description: "Return the requested path",
parameters: Type.Object({ path: Type.String() }),
async execute(_toolCallId, { path }) {
return { content: [{ type: "text", text: path }], details: { path } };
},
});
const extension: ExtensionFactory = (pi) => {
pi.registerTool(inspectPath);
pi.on("before_agent_start", (event) => ({
systemPrompt: `${event.systemPrompt}\nKeep file paths exact.`,
}));
};
export default extension;Every pi.on() overload returns an unsubscribe function. Pi snapshots matching handlers before a dispatch, so adding a handler or calling its unsubscribe function during that dispatch affects later dispatches rather than the snapshot already running. cache_warming_decision is an actionable hook: before a scheduled refresh, a handler may return { action: "warm" } or { action: "stop" }.
ExtensionContext.modelRegistry exposes ctx.modelRegistry.stream() for API-specific options and ctx.modelRegistry.streamSimple() for provider-neutral options. Both methods call the configured provider with resolved authentication at request time; the matching complete() method waits for the full assistant message. This facade keeps Extensions on Coding Agent's provider and credential path instead of reading secrets directly.
Actionable lifecycle boundaries
TurnEndEvent now includes turnIndex, messageEntryId, toolResultEntryIds, outcome, entries, continue, and a context preview in addition to the finalized message and Tool results. AgentBeforeSettleEvent is part of the exported ExtensionEvent union. Both turn_end and agent_before_settle handlers may return append-only entry drafts and continue: true; a host dispatches these actionable events through emitBoundary(baseEvent, buildContext).
Continuation from turn_end applies only to a normally completed Turn. Low-level error and aborted outcomes hard-exit through agent_end even if the handler returns continue: true. After retry, compaction, and queue policy have run, agent_before_settle is the later recovery boundary; handlers should inspect event.outcome and event.context.canContinue before requesting another provider call.
turn_end + outcome=completed + continue=true + context.canContinue=true -> next provider request
turn_end + outcome=error|aborted + continue=true -> agent_end
retry|compaction|queue policy -> agent_before_settle
agent_before_settle + continue=true + context.canContinue=true -> next provider requestThe ordinary context event receives conversation messages without system messages; Pi restores the leading prompt and Tool state after each handler. context_with_system runs afterward with the full transcript and sends returned messages verbatim. Removing its leading system message therefore removes the provider prompt and initial Tool declarations. A run requested inside an agent_settled handler is deferred until every settled handler has finished.
pi.setModel() changes the current session's model. A successful selection 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. The Promise resolves to false when the selected provider lacks authentication.
pi.setThinkingLevel() computes the effective capability-clamped level and records a session-history change only when it differs from the current value; not every requested choice produces a history entry. Pi persists and restores that effective change for the current session, but it does not change the configured default used by new sessions.
The default editor automatically embeds its working indicator in the editor border. Custom editors built from CustomEditor keep the standalone working indicator unless they opt in: pass { embedWorkingStatus: true } as the fourth constructor argument to embed the same status in the border. The option changes status placement, not Agent settlement or Tool execution.
| Managed tool | Availability |
|---|---|
read, bash, edit, write | Built in and active by default unless settings or SDK options change the selection |
powershell | Optional Windows built-in; select it explicitly or use its exported factory |
grep, find, ls | Built in; activate through tools or use their exported factories |
Extension or customTools entries | Registered by the host; still filtered by tools, excludeTools, and noTools |
The built-in read, bash, powershell, edit, and write definitions request strict-prefer JSON Schema constrained sampling. Supported providers enforce the schema, while unsupported providers can fall back to ordinary Tool calling. An Extension that deliberately replaces a definition can disable that request with constrainedSampling: false.
Tool access is an application policy. The current SDK does not expose the baseline --yolo switch.
user_bash event
user_bash intercepts user-entered ! / !! commands. A handler returns undefined only to continue propagation. A handled response is exactly one valid { operations } or { result } object: operations supplies BashOperations, while result supplies a complete BashResult. If all handlers return undefined, Pi may execute the command locally.
For user_bash, an exception or invalid defined value aborts the command; no later handler or local execution may run after that failure. Values such as null, false, {}, or an object containing both alternatives are invalid. This Extension event has its own failure boundary. The built-in Bash Tool follows AgentTool.execute: its execution failures become error Tool results as described above.
RPC queue and cancellation
The headless RPC protocol accepts an optional correlation ID. Its exact clear_queue request and successful response types are:
{ id?: string; type: "clear_queue" }
{
id?: string;
type: "response";
command: "clear_queue";
success: true;
data: { steering: string[]; followUp: string[] };
}Direct RPC steer and follow_up commands go through Extension input handlers before anything is queued. Both input events set source to "rpc". When the session is already streaming, steer sets streamingBehavior to "steer", while follow_up sets streamingBehavior to "followUp"; the field is undefined while idle. A handler can therefore transform or handle RPC input before Pi adds it to the corresponding queue.
| RPC command | Session state | source | streamingBehavior | Handler order |
|---|---|---|---|---|
steer | streaming | "rpc" | "steer" | Extension input → queue |
follow_up | streaming | "rpc" | "followUp" | Extension input → queue |
steer | idle | "rpc" | undefined | Extension input → queue |
follow_up | idle | "rpc" | undefined | Extension input → queue |
clear_queue atomically removes queued work and returns the removed text, keeping steering and follow-up messages separate. RPC abort cancels the active operation, now including active manual compaction, and waits until the session is idle before responding. Queued work can continue unless clear_queue removed it; cancellation and queue disposal are distinct operations.
For an interactive Escape flow, issue clear_queue before abort, then decide whether to restore the returned steering and followUp strings in the editor. This ordering prevents queued continuation work from starting while the abort command waits for idle.
PowerShell Tool factory and operations
The package root publicly exports the PowerShell factory and types; no deep import into dist/ or src/ is required:
import {
createPowerShellTool,
type PowerShellOperations,
type PowerShellToolOptions,
} from "@earendil-works/pi-coding-agent";The published declaration is:
declare function createPowerShellTool(
cwd: string,
options?: PowerShellToolOptions,
): ReturnType<typeof createBashTool>;In compact signature form, this is createPowerShellTool(cwd: string, options?: PowerShellToolOptions). The returned AgentTool accepts { command: string, timeout?: number }, streams partial results through the normal Tool update callback, and resolves to the shared shell-tool detail shape. Creating the Tool does not execute a command; execution begins only when its execute method is invoked by the host.
PowerShellToolOptions is declared as a Pick of the shared shell options. Its effective public shape is:
interface PowerShellToolOptions {
operations?: PowerShellOperations;
exposeSessionEnvironment?: boolean;
spawnHook?: PowerShellSpawnHook;
}PowerShellSpawnHook receives and returns { command: string; cwd: string; env: NodeJS.ProcessEnv }. The default exposeSessionEnvironment is true; the hook runs after Pi builds the command environment. Unlike BashToolOptions, this type does not expose commandPrefix or shellPath.
PowerShellOperations is a public alias of BashOperations, not a private PowerShell process class. A custom backend implements exactly one streamed method:
interface PowerShellOperations {
exec: (
command: string,
cwd: string,
options: {
onData: (data: Buffer) => void;
signal?: AbortSignal;
timeout?: number;
env?: NodeJS.ProcessEnv;
},
) => Promise<{ exitCode: number | null }>;
}The Tool wrapper owns argument validation, progress/result formatting, and bounded output. The operations backend owns actual execution: it must stream stdout/stderr bytes through onData, honor cancellation and timeout, return null when killed, and clean up process trees, transports, timers, and listeners. createLocalPowerShellOperations() is also public and supplies Pi's native Windows backend, but its executable discovery and process lifecycle are implementation behavior rather than an API for private process handles.
Runtime and CLI integration
For one fixed session, use createAgentSession(). For new, switch, fork, clone, or import flows, use createAgentSessionRuntime() and read runtime.session again after replacement; subscriptions belong to the old session. Lower-level hosts can use createAgentSessionServices(), createAgentSessionFromServices(), runPrintMode(), runRpcMode(), RpcClient, parseArgs(), or main().
| CLI flag | Current purpose |
|---|---|
--provider, --model, --models | Select one model or a model-cycle scope; --provider requires --model and is invalid alone |
--tui-mode regular / --tui-mode fullscreen | Select terminal UI mode for one run; fullscreen is the default and regular is the opt-out |
--thinking | Select off, minimal, low, medium, high, xhigh, or max |
--system-prompt <text> | Replace the base prompt source with literal text or the contents of an existing file path |
--append-system-prompt <text> | Add literal text or an existing file as an ordered append source; repeat the flag to add more sources |
--tools, --exclude-tools, --no-tools, --no-builtin-tools | Select the initial tool surface |
--session, --session-id, --session-dir, --continue, --resume, --fork, --no-session | Choose persistence or restoration behavior |
--extension, --no-extensions, --skill, --no-skills, --no-context-files | Control discovered or explicit resources |
--mode text / --mode json / --mode rpc, --print | Select the host protocol or non-interactive output |
Extensions can register additional flags, so parseArgs() retains unknown flags for extension resolution. Use --help from the installed pi binary as the complete CLI inventory for that exact version.
Experimental routed-service packages
The following package-root exports are the current 1.0.0 boundary, not a stable remote-Agent recipe. Applications still own service contracts, transport authentication, Session discovery, worker lifecycle, and retry policy. Subpath exports such as @earendil-works/pi-client/unix, @earendil-works/pi-server/unix, and @earendil-works/pi-server/testing are separate from the roots summarized here.
@earendil-works/pi-client
The root exports Client and createClientServiceTransport; ClientDisposedError, DisconnectedError, and ServerError; the transport contracts ByteTransport, ByteTransportFactory, and ByteTransportHandlers; and the client types AttachmentChangeListener, ClientOptions, ConnectionState, ConnectionStateChange, ListenerErrorHandler, ServiceSubscription, and Unsubscribe.
Client is transport-neutral and operates on explicit RpcTarget values. createClientServiceTransport(client, getTarget) adapts a lazily resolved target to Chord's RemoteServiceTransport. It does not manufacture typed services. On disconnect or disposal, pending requests reject locally and the live attachment is cleared; the client does not reconnect or replay requests automatically, even though accepted work may finish remotely.
@earendil-works/pi-protocol
The root exports PROTOCOL_VERSION (value 8), isServerId, the message and target types ClientMessage, ServerMessage, RpcTarget, ServerId, and SessionTarget, plus the individual hello, request, cancellation, response, service-event, attachment, and protocol-error types. Encoding and validation entry points include parseClientMessage, parseServerMessage, encodeClientMessage, encodeServerMessage, ClientMessageDecoder, ServerMessageDecoder, isSupportedProtocolVersion, and ProtocolValidationError.
The same root re-exports CBOR and framing primitives: encodeCbor, decodeCbor, CborError, CBOR limit constants and options, encodeFrame, FrameDecoder, FrameError, FrameDecoderOptions, and DEFAULT_MAX_FRAME_LENGTH. These APIs validate strict envelopes, framing, and opaque strict-JSON values; Chord owns service-control parsing, subscriptions, bindings, and replicated-state semantics.
@earendil-works/pi-server
The root exports Server, ServerListener, ServerOptions, ServerHost, RoutedServerPresentation, RoutedServerServiceAttachment, RoutedServerServiceHost, RoutedSessionAttachment, RoutedSessionHandle, and MaybePromise. It also exports ServerError, WrongServerError, SessionNotFoundError, SessionAmbiguousError, SessionNotAttachedError, ServerDrainingError, and INTERNAL_SERVER_ERROR_MESSAGE.
The server routes server-scoped and attachment-scoped Session services; it does not export the application's service catalogue or move an open Session or application-owned Harness over the wire. A server target is { serverId }, whereas a live Session target is { serverId, sessionId, attachmentId }. Constructing listeners and providing routed service hosts are application responsibilities, so this reference intentionally does not present an end-to-end launch recipe as stable.
The client, protocol, and server packages are experimental and have no compatibility guarantee. Pin their versions together and treat reconnect, replay, authentication, and lifecycle behavior as explicit application policy.
Next
- Review runtime settings: Configuration reference
- Review credentials and paths: Environment variables
- Add a provider or protocol adapter: Plug in a new model
- Build an Agent Core tool: Add a custom tool
- Render event streams: Stream output
- Store and branch coding sessions: Persist sessions