# SDK The SDK is the in-process integration surface for `@oh-my-pi/pi-coding-agent`. Use it when you want direct access to agent state, event streaming, tool wiring, and session control from your own Bun/Node process. If you need cross-language/process isolation, use RPC mode instead. ## Installation ```bash bun add @oh-my-pi/pi-coding-agent ``` ## Entry points `@oh-my-pi/pi-coding-agent` exports the SDK APIs from the package root (and also via `@oh-my-pi/pi-coding-agent/sdk`). Core exports for embedders: - `createAgentSession` - `SessionManager` - `Settings` - `AuthStorage` - `ModelRegistry` - `discoverAuthStorage` - Discovery helpers (`discoverExtensions`, `discoverSkills`, `discoverContextFiles`, `discoverPromptTemplates`, `discoverSlashCommands`, `discoverCustomTSCommands`, `discoverMCPServers`) - Tool factory surface (`createTools`, `BUILTIN_TOOLS`, tool classes) ## Quick start (auto-discovery defaults) ```ts import { createAgentSession } from "@oh-my-pi/pi-coding-agent"; const { session, modelFallbackMessage } = await createAgentSession(); if (modelFallbackMessage) { process.stderr.write(`${modelFallbackMessage}\n`); } const unsubscribe = session.subscribe((event) => { if ( event.type === "message_update" && event.assistantMessageEvent.type === "text_delta" ) { process.stdout.write(event.assistantMessageEvent.delta); } }); await session.prompt("Summarize this repository in 3 bullets."); unsubscribe(); await session.dispose(); ``` ## What `createAgentSession()` discovers by default `createAgentSession()` follows “provide to override, omit to discover”. If omitted, it resolves: - `cwd`: `getProjectDir()` - `agentDir`: `~/.omp/agent` (via `getAgentDir()`) - `authStorage`: `discoverAuthStorage(agentDir)` - `modelRegistry`: `new ModelRegistry(authStorage)` + background `refreshInBackground()` when the registry is not provided - `settings`: `await Settings.init({ cwd, agentDir })` - `sessionManager`: `SessionManager.create(cwd)` (file-backed) - skills/context files/prompt templates/slash commands/extensions/custom TS commands - built-in tools via `createTools(...)` - MCP tools (enabled by default; Exa MCP servers are folded into native Exa integration, and browser automation MCP servers are filtered when the built-in browser tool is enabled) - LSP integration (enabled by default) - `eventBus`: new `EventBus()` unless supplied ### Required vs optional inputs Typically you must provide only what you want to control: - **Must provide**: nothing for a minimal session - **Usually provide explicitly** in embedders: - `sessionManager` (if you need in-memory or custom location) - `authStorage` + `modelRegistry` (if you own credential/model lifecycle) - `model` or `modelPattern` (if deterministic model selection matters) - `settings` (if you need isolated/test config) ## Session manager behavior (persistent vs in-memory) `AgentSession` always uses a `SessionManager`; behavior depends on which factory you use. ### File-backed (default) ```ts import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent"; const { session } = await createAgentSession({ sessionManager: SessionManager.create(process.cwd()), }); console.log(session.sessionFile); // absolute .jsonl path ``` - Persists conversation/messages/state deltas to session files. - Supports resume/open/list/fork workflows. - `session.sessionFile` is defined. ### In-memory ```ts import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent"; const { session } = await createAgentSession({ sessionManager: SessionManager.inMemory(), }); console.log(session.sessionFile); // undefined ``` - No filesystem persistence. - Useful for tests, ephemeral workers, request-scoped agents. - Session methods still work, but persistence-specific behaviors (file resume/fork paths) are naturally limited. ### Resume/open/list helpers ```ts import { SessionManager } from "@oh-my-pi/pi-coding-agent"; const recent = await SessionManager.continueRecent(process.cwd()); const listed = await SessionManager.list(process.cwd()); const opened = listed[0] ? await SessionManager.open(listed[0].path) : null; ``` ## Model and auth wiring `createAgentSession()` uses `ModelRegistry` + `AuthStorage` for model selection and API key resolution. ### Explicit wiring ```ts import { createAgentSession, discoverAuthStorage, ModelRegistry, SessionManager, } from "@oh-my-pi/pi-coding-agent"; const authStorage = await discoverAuthStorage(); const modelRegistry = new ModelRegistry(authStorage); await modelRegistry.refresh(); const available = modelRegistry.getAvailable(); if (available.length === 0) throw new Error("No authenticated models available"); const { session } = await createAgentSession({ authStorage, modelRegistry, model: available[0], thinkingLevel: "medium", sessionManager: SessionManager.inMemory(), }); ``` ### Selection order when `model` is omitted When no explicit `model`/`modelPattern` is provided: 1. restore model from existing session (if restorable + key available) 2. settings default model role (`default`) 3. first available model with valid auth If restore fails, `modelFallbackMessage` explains fallback. ### Auth priority `AuthStorage.getApiKey(...)` resolves in this order: 1. runtime override (`setRuntimeApiKey`, used by CLI `--api-key`) 2. config-sourced API key override (`models.yml` provider `apiKey`) 3. stored API-key credential in `agent.db` / broker-backed storage 4. stored OAuth credential, including refresh when needed 5. provider environment variables 6. custom-provider resolver fallback ## Event subscription model Subscribe with `session.subscribe(listener)`; it returns an unsubscribe function. ```ts const unsubscribe = session.subscribe((event) => { switch (event.type) { case "agent_start": case "turn_start": case "tool_execution_start": break; case "message_update": if (event.assistantMessageEvent.type === "text_delta") { process.stdout.write(event.assistantMessageEvent.delta); } break; } }); ``` `AgentSessionEvent` includes core `AgentEvent` plus session-level events: - `auto_compaction_start` / `auto_compaction_end` - `auto_retry_start` / `auto_retry_end` - `retry_fallback_applied` / `retry_fallback_succeeded` - `ttsr_triggered` - `todo_reminder` / `todo_auto_clear` - `irc_message` ## Prompt lifecycle `session.prompt(text, options?)` is the primary entry point. Behavior: 1. optional command/template expansion (`/` commands, custom commands, file slash commands, prompt templates) 2. if currently streaming: - `streamingBehavior: "steer" | "followUp"` chooses how `prompt()` queues - extension `sendUserMessage(content)` defaults to steer when `deliverAs` is omitted - queued messages are preserved instead of throwing work away 3. if idle: - validates model + API key - appends user message - starts agent turn Related APIs: - `sendUserMessage(content, { deliverAs? })` - `steer(text, images?)` - `followUp(text, images?)` - `sendCustomMessage({ customType, content, ... }, { deliverAs?, triggerTurn? })` - `abort()` ## Tools and extension integration ### Built-ins and filtering - Built-ins come from `createTools(...)` and `BUILTIN_TOOLS`. - `toolNames` acts as an allowlist for built-ins. - `customTools` and extension-registered tools are still included. - Hidden tools (for example `yield`) are opt-in unless required by options. ```ts const { session } = await createAgentSession({ toolNames: ["read", "search", "find", "write"], requireYieldTool: true, }); ``` ### Extensions - `extensions`: inline `ExtensionFactory[]` - `additionalExtensionPaths`: load extra extension files - `disableExtensionDiscovery`: disable automatic extension scanning - `preloadedExtensions`: reuse already loaded extension set ### Runtime tool set changes `AgentSession` supports runtime activation updates: - `getActiveToolNames()` - `getAllToolNames()` - `setActiveToolsByName(names)` - `refreshMCPTools(mcpTools)` System prompt is rebuilt to reflect active tool changes. ## Discovery helpers Use these when you want partial control without recreating internal discovery logic: - `discoverAuthStorage(agentDir?)` - `discoverExtensions(cwd?)` - `discoverSkills(cwd?, _agentDir?, settings?)` - `discoverContextFiles(cwd?, _agentDir?)` - `discoverPromptTemplates(cwd?, agentDir?)` - `discoverSlashCommands(cwd?)` - `discoverCustomTSCommands(cwd?, agentDir?)` - `discoverMCPServers(cwd?)` - `buildSystemPrompt(options?)` ## Subagent-oriented options For SDK consumers building orchestrators (similar to task executor flow): - `outputSchema`: passes structured output expectation into tool context - `requireYieldTool`: forces `yield` tool inclusion - `taskDepth`: recursion-depth context for nested task sessions - `parentTaskPrefix`: artifact naming prefix for nested task outputs These are optional for normal single-agent embedding. ## `createAgentSession()` return value ```ts type CreateAgentSessionResult = { session: AgentSession; extensionsResult: LoadExtensionsResult; setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void; mcpManager?: MCPManager; modelFallbackMessage?: string; lspServers?: Array<{ name: string; status: "connecting" | "ready" | "error" | "available"; fileTypes: string[]; error?: string; }>; eventBus: EventBus; }; ``` Use `setToolUIContext(...)` only if your embedder provides UI capabilities that tools/extensions should call into. ## Startup performance `createAgentSession()` runs two background optimizations to overlap I/O with the rest of session setup: - **Model-host preconnect.** As soon as the model is resolved, the SDK fires a best-effort `fetch.preconnect(model.baseUrl)` so DNS + TCP + TLS + HTTP/2 to the provider's host happens in parallel with extension/skill load, tool registry build, and system-prompt assembly. The first real `fetch(...)` then reuses the warm connection, saving 100–300 ms on transcontinental hops (e.g. residential IP → `api.anthropic.com`). Implementation lives in `preconnectModelHost()` in `packages/coding-agent/src/sdk.ts`. If `fetch.preconnect` is unavailable (non-Bun runtime) or the call throws, the optimization is silently skipped — never a hard dependency. Applies to every mode (interactive, print, RPC, ACP). - **Conditional LSP warmup.** Startup LSP servers (those returned by `discoverStartupLspServers(cwd)`) are only warmed when **all** of these hold: - `enableLsp !== false` on the session options, **and** - `options.hasUI === true` (interactive TUI), **and** - the `lsp.lazy` setting is disabled (it defaults to `true`). With `lsp.lazy` enabled — the default — no language servers are launched at startup at all; each server cold-starts on first use, i.e. when the agent invokes the `lsp` tool or an edit/write touches a file whose extension matches the server's `fileTypes`. Print / script / RPC / ACP invocations (`hasUI=false`) skip the warmup regardless of the setting: they don't render the warmup status indicator and typically finish before the language servers would stabilize, so warming them just spends CPU parsing big `initialize` responses concurrently with the LLM stream consumer and jitters perceived latency. Tools that actually need an LSP server still spin one up on demand through `getOrCreateClient()` — only the _startup_ warmup is skipped. The returned `lspServers` field in `CreateAgentSessionResult` is still populated for UI sessions in lazy mode — recognized servers are discovered (no processes spawned) and reported with status `"available"` so the welcome screen and `/status` can list them; it is `undefined` only when `enableLsp === false` or `hasUI === false`. ## Minimal controlled embed example ```ts import { createAgentSession, discoverAuthStorage, ModelRegistry, SessionManager, Settings, } from "@oh-my-pi/pi-coding-agent"; const authStorage = await discoverAuthStorage(); const modelRegistry = new ModelRegistry(authStorage); await modelRegistry.refresh(); const settings = Settings.isolated({ "compaction.enabled": true, "retry.enabled": true, }); const { session } = await createAgentSession({ authStorage, modelRegistry, settings, sessionManager: SessionManager.inMemory(), toolNames: ["read", "search", "find", "edit", "write"], enableMCP: false, enableLsp: true, }); session.subscribe((event) => { if ( event.type === "message_update" && event.assistantMessageEvent.type === "text_delta" ) { process.stdout.write(event.assistantMessageEvent.delta); } }); await session.prompt("Find all TODO comments in this repo and propose fixes."); await session.dispose(); ```