> pi can help you use the SDK. Ask it to build an integration for your use case. # SDK The SDK provides programmatic access to pi's agent capabilities. Use it to embed pi in other applications, build custom interfaces, or integrate with automated workflows. **Example use cases:** - Build a custom UI (web, desktop, mobile) - Integrate agent capabilities into existing applications - Create automated pipelines with agent reasoning - Build custom tools that spawn sub-agents - Test agent behavior programmatically See [examples/sdk/](../examples/sdk/) for working examples from minimal to full control. ## Quick Start ```typescript import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent"; const modelRuntime = await ModelRuntime.create(); const { session } = await createAgentSession({ sessionManager: SessionManager.inMemory(), modelRuntime, }); session.subscribe((event) => { if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { process.stdout.write(event.assistantMessageEvent.delta); } }); await session.prompt("What files are in the current directory?"); ``` ## Installation ```bash npm install @earendil-works/pi-coding-agent ``` The SDK is included in the main package. No separate installation needed. ## Core Concepts ### createAgentSession() The main factory function for a single `AgentSession`. `createAgentSession()` uses a `ResourceLoader` to supply extensions, skills, prompt templates, themes, and context files. If you do not provide one, it uses `DefaultResourceLoader` with standard discovery. ```typescript import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent"; // Minimal: defaults with DefaultResourceLoader const { session } = await createAgentSession(); // Custom: override specific options const { session } = await createAgentSession({ model: myModel, tools: ["read", "bash"], sessionManager: SessionManager.inMemory(), }); ``` ### AgentSession The session manages agent lifecycle, message history, model state, compaction, and event streaming. ```typescript interface AgentSession { // Send a prompt and wait for completion prompt(text: string, options?: PromptOptions): Promise; // Queue messages during streaming steer(text: string): Promise; followUp(text: string): Promise; // Subscribe to events (returns unsubscribe function) subscribe(listener: (event: AgentSessionEvent) => void): () => void; // Session info sessionFile: string | undefined; sessionId: string; // Model control setModel(model: Model): Promise; setThinkingLevel(level: ThinkingLevel): void; cycleModel(): Promise; cycleThinkingLevel(): ThinkingLevel | undefined; // State access agent: Agent; model: Model | undefined; thinkingLevel: ThinkingLevel; messages: AgentMessage[]; isStreaming: boolean; // In-place tree navigation within the current session file navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }): Promise<{ editorText?: string; cancelled: boolean }>; // Compaction compact(customInstructions?: string): Promise; abortCompaction(): void; // Abort current operation abort(): Promise; // Cleanup dispose(): void; } ``` Session replacement APIs such as new-session, resume, fork, and import live on `AgentSessionRuntime`, not on `AgentSession`. ### createAgentSessionRuntime() and AgentSessionRuntime Use the runtime API when you need to replace the active session and rebuild cwd-bound runtime state. This is the same layer used by the built-in interactive, print, and RPC modes. `createAgentSessionRuntime()` takes a runtime factory plus the initial cwd/session target. The factory closes over process-global fixed inputs, recreates cwd-bound services for the effective cwd, resolves session options against those services, and returns a full runtime result. ```typescript import { type CreateAgentSessionRuntimeFactory, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent"; const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => { const services = await createAgentSessionServices({ cwd }); return { ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent, })), services, diagnostics: services.diagnostics, }; }; const runtime = await createAgentSessionRuntime(createRuntime, { cwd: process.cwd(), agentDir: getAgentDir(), sessionManager: SessionManager.create(process.cwd()), }); ``` `AgentSessionRuntime` owns replacement of the active runtime across: - `newSession()` - `switchSession()` - `fork()` - clone flows via `fork(entryId, { position: "at" })` - `importFromJsonl()` Important behavior: - `runtime.session` changes after those operations - event subscriptions are attached to a specific `AgentSession`, so re-subscribe after replacement - if you use extensions, call `runtime.session.bindExtensions(...)` again for the new session - creation returns diagnostics on `runtime.diagnostics` - if runtime creation or replacement fails, the method throws and the caller decides how to handle it ```typescript let session = runtime.session; let unsubscribe = session.subscribe(() => {}); await runtime.newSession(); unsubscribe(); session = runtime.session; unsubscribe = session.subscribe(() => {}); ``` ### Prompting and Message Queueing `PromptOptions` controls prompt expansion, queueing behavior while streaming, and prompt preflight notifications: ```typescript interface PromptOptions { expandPromptTemplates?: boolean; images?: ImageContent[]; streamingBehavior?: "steer" | "followUp"; source?: InputSource; preflightResult?: (success: boolean) => void; } ``` `preflightResult` is called once per `prompt()` invocation: - `true` when the prompt was accepted, queued, or handled immediately - `false` when prompt preflight rejected before acceptance It fires before `prompt()` resolves. `prompt()` still resolves only after the full accepted run finishes, including retries. Failures after acceptance are reported through the normal event and message stream, not through `preflightResult(false)`. The `prompt()` method handles prompt templates, extension commands, and message sending: ```typescript // Basic prompt (when not streaming) await session.prompt("What files are here?"); // With images await session.prompt("What's in this image?", { images: [{ type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } }] }); // During streaming: must specify how to queue the message await session.prompt("Stop and do this instead", { streamingBehavior: "steer" }); await session.prompt("After you're done, also check X", { streamingBehavior: "followUp" }); ``` **Behavior:** - **Extension commands** (e.g., `/mycommand`): Execute immediately, even during streaming. They manage their own LLM interaction via `pi.sendMessage()`. - **File-based prompt templates** (from `.md` files): Expanded to their content before sending or queueing. - **During streaming without `streamingBehavior`**: Throws an error. Use `steer()` or `followUp()` directly, or specify the option. - **`preflightResult(true)`**: Means the prompt was accepted, queued, or handled immediately. - **`preflightResult(false)`**: Means preflight rejected before acceptance. For explicit queueing during streaming: ```typescript // Queue a steering message for delivery after the current assistant turn finishes its tool calls await session.steer("New instruction"); // Wait for agent to finish (delivered only when agent stops) await session.followUp("After you're done, also do this"); ``` Both `steer()` and `followUp()` expand file-based prompt templates but error on extension commands (extension commands cannot be queued). ### Agent and AgentState The `Agent` class (from `@earendil-works/pi-agent-core`) handles the core LLM interaction. Access it via `session.agent`. ```typescript // Access current state const state = session.agent.state; // state.messages: AgentMessage[] - conversation history // state.model: Model - current model // state.thinkingLevel: ThinkingLevel - current thinking level // state.systemPrompt: string - system prompt // state.tools: AgentTool[] - available tools // state.streamingMessage?: AgentMessage - current partial assistant message // state.errorMessage?: string - latest assistant error // Replace messages (useful for branching or restoration) session.agent.state.messages = messages; // copies the top-level array // Replace tools session.agent.state.tools = tools; // copies the top-level array // Wait for agent to finish processing await session.agent.waitForIdle(); ``` ### Events Subscribe to events to receive streaming output and lifecycle notifications. ```typescript session.subscribe((event) => { switch (event.type) { // Streaming text from assistant case "message_update": if (event.assistantMessageEvent.type === "text_delta") { process.stdout.write(event.assistantMessageEvent.delta); } if (event.assistantMessageEvent.type === "thinking_delta") { // Thinking output (if thinking enabled) } break; // Tool execution case "tool_execution_start": console.log(`Tool: ${event.toolName}`); break; case "tool_execution_update": // Streaming tool output break; case "tool_execution_end": console.log(`Result: ${event.isError ? "error" : "success"}`); break; // Message lifecycle case "message_start": // New message starting break; case "message_end": // Message complete break; // Agent lifecycle case "agent_start": // Agent started processing prompt break; case "agent_end": // Agent finished (event.messages contains new messages) break; // Turn lifecycle (one LLM response + tool calls) case "turn_start": break; case "turn_end": // event.message: assistant response // event.toolResults: tool results from this turn break; // Session events (queue, compaction, retry) case "queue_update": console.log(event.steering, event.followUp); break; case "compaction_start": case "compaction_end": case "auto_retry_start": case "auto_retry_end": case "summarization_retry_scheduled": case "summarization_retry_attempt_start": case "summarization_retry_finished": break; } }); ``` ## Options Reference ### Directories ```typescript const { session } = await createAgentSession({ // Working directory for DefaultResourceLoader discovery cwd: process.cwd(), // default // Global config directory agentDir: "~/.pi/agent", // default (expands ~) }); ``` `cwd` is used by `DefaultResourceLoader` for: - Project extensions (`.pi/extensions/`) - Project skills: - `.pi/skills/` - `.agents/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo) - Project prompts (`.pi/prompts/`) - Context files (`AGENTS.md` walking up from cwd) - Session directory naming `agentDir` is used by `DefaultResourceLoader` for: - Global extensions (`extensions/`) - Global skills: - `skills/` under `agentDir` (for example `~/.pi/agent/skills/`) - `~/.agents/skills/` - Global prompts (`prompts/`) - Global context file (`AGENTS.md`) - Settings (`settings.json`) - Custom models (`models.json`) - Credentials (`auth.json`) - Sessions (`sessions/`) When you pass a custom `ResourceLoader`, `cwd` and `agentDir` no longer control resource discovery. They still influence session naming and tool path resolution. ### Model ```typescript import { getModel } from "@earendil-works/pi-ai"; import { ModelRuntime } from "@earendil-works/pi-coding-agent"; const modelRuntime = await ModelRuntime.create(); // create() restores cached catalogs but does not refresh them from pi.dev by default. // Opt in to a create-time network refresh and bound how long it may take: const refreshedRuntime = await ModelRuntime.create({ allowModelNetwork: true, modelRefreshTimeoutMs: 15_000, }); // Find specific built-in model (doesn't check if API key exists) const opus = getModel("anthropic", "claude-opus-4-5"); if (!opus) throw new Error("Model not found"); // Find any model by provider/id, including custom models from models.json // (doesn't check if API key exists) const customModel = modelRuntime.getModel("my-provider", "my-model"); // Get only models that have valid authentication configured const available = await modelRuntime.getAvailable(); const { session } = await createAgentSession({ model: opus, thinkingLevel: "medium", // off, minimal, low, medium, high, xhigh, max // Models for cycling (Ctrl+P in interactive mode) scopedModels: [ { model: opus, thinkingLevel: "high" }, { model: haiku, thinkingLevel: "off" }, ], modelRuntime, }); ``` If no model is provided: 1. Tries to restore from session (if continuing) 2. Uses default from settings 3. Falls back to first available model Remote catalogs are persisted locally so later runtimes can restore them without a network request. The default file is `~/.pi/agent/models-store.json`; set `modelsStorePath` to choose another location, or inject `modelsStore` to control persistence. Network refreshes are throttled to once per provider every four hours unless forced. To force an immediate refresh, call `await modelRuntime.refresh({ allowNetwork: true, force: true, signal })`. Setting `PI_OFFLINE` disables model network access. To match CLI model parsing, use the exported resolver helpers: ```typescript import { resolveCliModel, resolveModelScopeWithDiagnostics, } from "@earendil-works/pi-coding-agent"; const cliModel = resolveCliModel({ cliModel: "anthropic/claude-opus-4-5:high", modelRuntime, }); if (cliModel.error) throw new Error(cliModel.error); if (cliModel.warning) console.warn(cliModel.warning); const { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics( ["anthropic/*:high", "gpt-5"], modelRuntime, ); for (const diagnostic of diagnostics) { console.warn(diagnostic.message); } ``` `resolveCliModel()` uses all registered models so `--api-key` style first-time setup can resolve a model before stored auth exists. `resolveModelScopeWithDiagnostics()` matches `--models` and `enabledModels` semantics while returning warnings instead of printing them. > See [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts) ### API Keys and OAuth Authentication resolution priority (handled by `ModelRuntime`): 1. Runtime overrides (via `setRuntimeApiKey`, not persisted) 2. Stored credentials in `auth.json` (API keys or OAuth tokens) 3. Environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) 4. Fallback resolver (for custom provider keys from `models.json`) ```typescript import { InMemoryCredentialStore } from "@earendil-works/pi-ai"; import { createAgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent"; // Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json const modelRuntime = await ModelRuntime.create(); // Provider-owned auth methods and current status for (const provider of modelRuntime.getProviders()) { const status = await modelRuntime.checkAuth(provider.id); console.log(provider.name, provider.auth, status); } // Runtime API key override (not persisted to disk) await modelRuntime.setRuntimeApiKey("anthropic", "sk-my-temp-key"); // Custom credential and model locations const customRuntime = await ModelRuntime.create({ authPath: "/my/app/auth.json", modelsPath: "/my/app/models.json", }); // Or inject any pi-ai CredentialStore const credentials = new InMemoryCredentialStore(); const inMemoryRuntime = await ModelRuntime.create({ credentials }); const { session } = await createAgentSession({ modelRuntime: customRuntime, }); ``` `login()`, `logout()`, `setRuntimeApiKey()`, and `removeRuntimeApiKey()` resolve after the affected provider's cached/built-in catalog, composition, and availability snapshot are locally consistent. They do not wait for remote catalog freshness. If credentials were committed but local synchronization fails, they reject with the exported `CredentialSynchronizationError`; inspect its `providerId`, `operation`, `credential`, and `cause` fields instead of retrying the credential mutation blindly. Public model/auth operations and `ModelRuntime.create({ signal })` accept optional abort signals and are unbounded when omitted. SDK applications own deadline policy for remote catalog freshness: ```typescript const signal = AbortSignal.timeout(15_000); const result = await modelRuntime.refresh({ providers: ["anthropic"], signal, }); if (result.aborted) console.warn("Catalog refresh timed out; using cached models"); for (const [providerId, error] of result.errors) { console.warn(`Could not refresh ${providerId}:`, error); } ``` A failed or timed-out network refresh does not undo a successful credential operation. `refresh()` starts a new provider generation, so it does not wait behind an older stalled refresh and stale generations cannot publish afterward. > See [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts) ### System Prompt Use a `ResourceLoader` to override the system prompt: ```typescript import { createAgentSession, DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; const loader = new DefaultResourceLoader({ systemPromptOverride: () => "You are a helpful assistant.", }); await loader.reload(); const { session } = await createAgentSession({ resourceLoader: loader }); ``` > See [examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts) ### Tools Specify which built-in tools to enable: - Built-in tool names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` - Default built-ins: `read`, `bash`, `edit`, `write` - `noTools: "all"` disables all tools - `noTools: "builtin"` disables default built-ins while keeping extension and custom tools enabled - `excludeTools` disables specific built-in, extension, or custom tool names after any `tools` allowlist is applied The `edit` tool returns `details.diff` for Pi's TUI display and `details.patch` as a standard unified patch for SDK consumers. ```typescript import { createAgentSession } from "@earendil-works/pi-coding-agent"; // Read-only mode const { session } = await createAgentSession({ tools: ["read", "grep", "find", "ls"], }); // Pick specific tools const { session } = await createAgentSession({ tools: ["read", "bash", "grep"], }); // Disable one tool while keeping the rest available const { session } = await createAgentSession({ excludeTools: ["ask_question"], }); ``` #### Tools with Custom cwd When you pass a custom `cwd`, `createAgentSession()` builds selected built-in tools for that cwd. ```typescript import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent"; const cwd = "/path/to/project"; // Use default tools for custom cwd const { session } = await createAgentSession({ cwd, sessionManager: SessionManager.inMemory(cwd), }); // Or pick specific tools for custom cwd const { session } = await createAgentSession({ cwd, tools: ["read", "bash", "grep"], sessionManager: SessionManager.inMemory(cwd), }); ``` > See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts) ### Custom Tools ```typescript import { Type } from "typebox"; import { createAgentSession, defineTool } from "@earendil-works/pi-coding-agent"; // Inline custom tool const myTool = defineTool({ name: "my_tool", label: "My Tool", description: "Does something useful", parameters: Type.Object({ input: Type.String({ description: "Input value" }), }), execute: async (_toolCallId, params) => ({ content: [{ type: "text", text: `Result: ${params.input}` }], details: {}, }), }); // Pass custom tools directly const { session } = await createAgentSession({ customTools: [myTool], }); ``` Use `defineTool()` for standalone definitions and arrays like `customTools: [myTool]`. Inline `pi.registerTool({ ... })` already infers parameter types correctly. Custom tools passed via `customTools` are combined with extension-registered tools. Extensions loaded by the ResourceLoader can also register tools via `pi.registerTool()`. If you pass `tools`, include each custom or extension tool name you want enabled, for example `tools: ["read", "bash", "my_tool"]`. > See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts) ### Extensions Extensions are loaded by the `ResourceLoader`. `DefaultResourceLoader` discovers extensions from `~/.pi/agent/extensions/`, `.pi/extensions/`, and settings.json extension sources. ```typescript import { createAgentSession, DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; const loader = new DefaultResourceLoader({ additionalExtensionPaths: ["/path/to/my-extension.ts"], extensionFactories: [ (pi) => { pi.on("agent_start", () => { console.log("[Inline Extension] Agent starting"); }); }, ], }); await loader.reload(); const { session } = await createAgentSession({ resourceLoader: loader }); ``` Extensions can register tools, subscribe to events, add commands, and more. See [extensions.md](extensions.md) for the full API. **Named inline extensions:** By default, inline factories display as ``, ``, etc. in the startup Extensions list. To show a descriptive name instead, wrap the factory: ```typescript import type { InlineExtension } from "@earendil-works/pi-coding-agent"; const myProvider: InlineExtension = { name: "my-provider", factory: (pi) => { pi.on("agent_start", () => { console.log("[my-provider] Agent starting"); }); }, }; const loader = new DefaultResourceLoader({ extensionFactories: [myProvider], }); ``` This displays as `` instead of ``. Bare factory functions are still accepted for backward compatibility. **Event Bus:** Extensions can communicate via `pi.events`. Pass a shared `eventBus` to `DefaultResourceLoader` if you need to emit or listen from outside: ```typescript import { createEventBus, DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; const eventBus = createEventBus(); const loader = new DefaultResourceLoader({ eventBus, }); await loader.reload(); eventBus.on("my-extension:status", (data) => console.log(data)); ``` > See [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) and [docs/extensions.md](extensions.md) ### Skills ```typescript import { createAgentSession, DefaultResourceLoader, type Skill, } from "@earendil-works/pi-coding-agent"; const customSkill: Skill = { name: "my-skill", description: "Custom instructions", filePath: "/path/to/SKILL.md", baseDir: "/path/to", source: "custom", }; const loader = new DefaultResourceLoader({ skillsOverride: (current) => ({ skills: [...current.skills, customSkill], diagnostics: current.diagnostics, }), }); await loader.reload(); const { session } = await createAgentSession({ resourceLoader: loader }); ``` > See [examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts) ### Context Files ```typescript import { createAgentSession, DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; const loader = new DefaultResourceLoader({ agentsFilesOverride: (current) => ({ agentsFiles: [ ...current.agentsFiles, { path: "/virtual/AGENTS.md", content: "# Guidelines\n\n- Be concise" }, ], }), }); await loader.reload(); const { session } = await createAgentSession({ resourceLoader: loader }); ``` > See [examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts) ### Slash Commands ```typescript import { createAgentSession, DefaultResourceLoader, type PromptTemplate, } from "@earendil-works/pi-coding-agent"; const customCommand: PromptTemplate = { name: "deploy", description: "Deploy the application", source: "(custom)", content: "# Deploy\n\n1. Build\n2. Test\n3. Deploy", }; const loader = new DefaultResourceLoader({ promptsOverride: (current) => ({ prompts: [...current.prompts, customCommand], diagnostics: current.diagnostics, }), }); await loader.reload(); const { session } = await createAgentSession({ resourceLoader: loader }); ``` > See [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts) ### Session Management Sessions use a tree structure with `id`/`parentId` linking, enabling in-place branching. ```typescript import { type CreateAgentSessionRuntimeFactory, createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent"; // In-memory (no persistence) const { session } = await createAgentSession({ sessionManager: SessionManager.inMemory(), }); // New persistent session const { session: persisted } = await createAgentSession({ sessionManager: SessionManager.create(process.cwd()), }); // Continue most recent const { session: continued, modelFallbackMessage } = await createAgentSession({ sessionManager: SessionManager.continueRecent(process.cwd()), }); if (modelFallbackMessage) { console.log("Note:", modelFallbackMessage); } // Open specific file const { session: opened } = await createAgentSession({ sessionManager: SessionManager.open("/path/to/session.jsonl"), }); // List sessions const currentProjectSessions = await SessionManager.list(process.cwd()); const allSessions = await SessionManager.listAll(process.cwd()); // Session replacement API for /new, /resume, /fork, /clone, and import flows. const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => { const services = await createAgentSessionServices({ cwd }); return { ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent, })), services, diagnostics: services.diagnostics, }; }; const runtime = await createAgentSessionRuntime(createRuntime, { cwd: process.cwd(), agentDir: getAgentDir(), sessionManager: SessionManager.create(process.cwd()), }); // Replace the active session with a fresh one await runtime.newSession(); // Replace the active session with another saved session await runtime.switchSession("/path/to/session.jsonl"); // Replace the active session with a fork from a specific user entry await runtime.fork("entry-id"); // Clone the active path through a specific entry await runtime.fork("entry-id", { position: "at" }); ``` **SessionManager tree API:** ```typescript const sm = SessionManager.open("/path/to/session.jsonl"); // Session listing const currentProjectSessions = await SessionManager.list(process.cwd()); const allSessions = await SessionManager.listAll(process.cwd()); // Tree traversal const entries = sm.getEntries(); // All entries (excludes header) const tree = sm.getTree(); // Full tree structure const path = sm.getPath(); // Path from root to current leaf const leaf = sm.getLeafEntry(); // Current leaf entry const entry = sm.getEntry(id); // Get entry by ID const children = sm.getChildren(id); // Direct children of entry // Labels const label = sm.getLabel(id); // Get label for entry sm.appendLabelChange(id, "checkpoint"); // Set label // Branching sm.branch(entryId); // Move leaf to earlier entry sm.branchWithSummary(id, "Summary..."); // Branch with context summary sm.createBranchedSession(leafId); // Extract path to new file ``` > See [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) and [Session Format](session-format.md) ### Settings Management ```typescript import { createAgentSession, SettingsManager, SessionManager } from "@earendil-works/pi-coding-agent"; // Default: loads from files (global + project merged) const { session } = await createAgentSession({ settingsManager: SettingsManager.create(), }); // With overrides const settingsManager = SettingsManager.create(); settingsManager.applyOverrides({ compaction: { enabled: false }, retry: { enabled: true, maxRetries: 5 }, }); const { session } = await createAgentSession({ settingsManager }); // In-memory (no file I/O, for testing) const { session } = await createAgentSession({ settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }), sessionManager: SessionManager.inMemory(), }); // Custom directories const { session } = await createAgentSession({ settingsManager: SettingsManager.create("/custom/cwd", "/custom/agent"), }); ``` **Static factories:** - `SettingsManager.create(cwd?, agentDir?)` - Load from files - `SettingsManager.inMemory(settings?)` - No file I/O **Project-specific settings:** Settings load from two locations and merge: 1. Global: `~/.pi/agent/settings.json` 2. Project: `/.pi/settings.json` Project overrides global. Nested objects merge keys. Setters modify global settings by default. **Persistence and error handling semantics:** - Settings getters/setters are synchronous for in-memory state. - Setters enqueue persistence writes asynchronously. - Call `await settingsManager.flush()` when you need a durability boundary (for example, before process exit or before asserting file contents in tests). - `SettingsManager` does not print settings I/O errors. Use `settingsManager.drainErrors()` and report them in your app layer. > See [examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts) ## ResourceLoader Use `DefaultResourceLoader` to discover extensions, skills, prompts, themes, and context files. ```typescript import { DefaultResourceLoader, getAgentDir, } from "@earendil-works/pi-coding-agent"; const loader = new DefaultResourceLoader({ cwd, agentDir: getAgentDir(), }); await loader.reload(); const extensions = loader.getExtensions(); const skills = loader.getSkills(); const prompts = loader.getPrompts(); const themes = loader.getThemes(); const contextFiles = loader.getAgentsFiles().agentsFiles; ``` ## Return Value `createAgentSession()` returns: ```typescript interface CreateAgentSessionResult { // The session session: AgentSession; // Extensions result (for runner setup) extensionsResult: LoadExtensionsResult; // Warning if session model couldn't be restored modelFallbackMessage?: string; } interface LoadExtensionsResult { extensions: Extension[]; errors: Array<{ path: string; error: string }>; runtime: ExtensionRuntime; } ``` ## Complete Example ```typescript import { getModel } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { createAgentSession, DefaultResourceLoader, defineTool, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent"; const modelRuntime = await ModelRuntime.create({ authPath: "/custom/agent/auth.json", modelsPath: "/custom/agent/models.json", }); if (process.env.MY_KEY) { await modelRuntime.setRuntimeApiKey("anthropic", process.env.MY_KEY); } // Inline tool const statusTool = defineTool({ name: "status", label: "Status", description: "Get system status", parameters: Type.Object({}), execute: async () => ({ content: [{ type: "text", text: `Uptime: ${process.uptime()}s` }], details: {}, }), }); const model = getModel("anthropic", "claude-opus-4-5"); if (!model) throw new Error("Model not found"); // In-memory settings with overrides const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: true, maxRetries: 2 }, }); const loader = new DefaultResourceLoader({ cwd: process.cwd(), agentDir: "/custom/agent", settingsManager, systemPromptOverride: () => "You are a minimal assistant. Be concise.", }); await loader.reload(); const { session } = await createAgentSession({ cwd: process.cwd(), agentDir: "/custom/agent", model, thinkingLevel: "off", modelRuntime, tools: ["read", "bash", "status"], customTools: [statusTool], resourceLoader: loader, sessionManager: SessionManager.inMemory(), settingsManager, }); session.subscribe((event) => { if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { process.stdout.write(event.assistantMessageEvent.delta); } }); await session.prompt("Get status and list files."); ``` ## Run Modes The SDK exports run mode utilities for building custom interfaces on top of `createAgentSession()`: ### InteractiveMode Full TUI interactive mode with editor, chat history, and all built-in commands: ```typescript import { type CreateAgentSessionRuntimeFactory, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, InteractiveMode, SessionManager, } from "@earendil-works/pi-coding-agent"; const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => { const services = await createAgentSessionServices({ cwd }); return { ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })), services, diagnostics: services.diagnostics, }; }; const runtime = await createAgentSessionRuntime(createRuntime, { cwd: process.cwd(), agentDir: getAgentDir(), sessionManager: SessionManager.create(process.cwd()), }); const mode = new InteractiveMode(runtime, { migratedProviders: [], modelFallbackMessage: undefined, initialMessage: "Hello", initialImages: [], initialMessages: [], }); await mode.run(); ``` ### runPrintMode Single-shot mode: send prompts, output result, exit: ```typescript import { type CreateAgentSessionRuntimeFactory, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, runPrintMode, SessionManager, } from "@earendil-works/pi-coding-agent"; const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => { const services = await createAgentSessionServices({ cwd }); return { ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })), services, diagnostics: services.diagnostics, }; }; const runtime = await createAgentSessionRuntime(createRuntime, { cwd: process.cwd(), agentDir: getAgentDir(), sessionManager: SessionManager.create(process.cwd()), }); await runPrintMode(runtime, { mode: "text", initialMessage: "Hello", initialImages: [], messages: ["Follow up"], }); ``` ### runRpcMode JSON-RPC mode for subprocess integration: ```typescript import { type CreateAgentSessionRuntimeFactory, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, runRpcMode, SessionManager, } from "@earendil-works/pi-coding-agent"; const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => { const services = await createAgentSessionServices({ cwd }); return { ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })), services, diagnostics: services.diagnostics, }; }; const runtime = await createAgentSessionRuntime(createRuntime, { cwd: process.cwd(), agentDir: getAgentDir(), sessionManager: SessionManager.create(process.cwd()), }); await runRpcMode(runtime); ``` See [RPC documentation](rpc.md) for the JSON protocol. ## RPC Mode Alternative For subprocess-based integration without building with the SDK, use the CLI directly: ```bash pi --mode rpc --no-session ``` See [RPC documentation](rpc.md) for the JSON protocol. The SDK is preferred when: - You want type safety - You're in the same Node.js process - You need direct access to agent state - You want to customize tools/extensions programmatically RPC mode is preferred when: - You're integrating from another language - You want process isolation - You're building a language-agnostic client ## Exports The main entry point exports: ```typescript // Factory createAgentSession createAgentSessionRuntime AgentSessionRuntime // Auth and Models ModelRuntime // implements pi-ai Models and owns credential storage ModelRegistry // synchronous extension compatibility facade CredentialSynchronizationError resolveCliModel resolveModelScopeWithDiagnostics // Resource loading DefaultResourceLoader type ResourceLoader createEventBus // Constants and helpers CONFIG_DIR_NAME defineTool getAgentDir getPackageDir getReadmePath getDocsPath getExamplesPath // Session management SessionManager SettingsManager // Tool factories createCodingTools createReadOnlyTools createReadTool, createBashTool, createEditTool, createWriteTool createGrepTool, createFindTool, createLsTool // Types type CreateAgentSessionOptions type CreateAgentSessionResult type ExtensionFactory type InlineExtension type ExtensionAPI type ToolDefinition type Skill type PromptTemplate type Tool ``` For extension types, see [extensions.md](extensions.md) for the full API.