Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/earendil-works/pi/llms.txt

Use this file to discover all available pages before exploring further.

The SDK gives you programmatic access to Pi’s agent capabilities from within a Node.js application. Use it to build custom UIs, integrate agent reasoning into existing workflows, create automated pipelines, or spawn sub-agents from your own tools.

Quick start

Install the package:
npm install @earendil-works/pi-coding-agent
Then create a session and subscribe to events:
import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";

const authStorage = AuthStorage.create();
const modelRegistry = ModelRegistry.create(authStorage);

const { session } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  authStorage,
  modelRegistry,
});

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?");

Core concepts

createAgentSession()

createAgentSession() is the main factory function. It creates a single AgentSession using a ResourceLoader to supply extensions, skills, prompt templates, themes, and context files. When you omit the resourceLoader option, it uses DefaultResourceLoader with standard discovery.
import { createAgentSession } from "@earendil-works/pi-coding-agent";

// Minimal: defaults with DefaultResourceLoader
const { session } = await createAgentSession();

// With overrides
const { session } = await createAgentSession({
  model: myModel,
  tools: [readTool, bashTool],
  sessionManager: SessionManager.inMemory(),
});
It returns a CreateAgentSessionResult:
interface CreateAgentSessionResult {
  session: AgentSession;
  extensionsResult: LoadExtensionsResult;
  modelFallbackMessage?: string; // set when session model couldn't be restored
}

AgentSession interface

AgentSession manages agent lifecycle, message history, model state, compaction, and event streaming.
interface AgentSession {
  // Send a prompt and wait for completion
  prompt(text: string, options?: PromptOptions): Promise<void>;

  // Queue messages during streaming
  steer(text: string): Promise<void>;
  followUp(text: string): Promise<void>;

  // 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<void>;
  setThinkingLevel(level: ThinkingLevel): void;
  cycleModel(): Promise<ModelCycleResult | undefined>;
  cycleThinkingLevel(): ThinkingLevel | undefined;

  // State access
  agent: Agent;
  model: Model | undefined;
  thinkingLevel: ThinkingLevel;
  messages: AgentMessage[];
  isStreaming: boolean;

  // In-place tree navigation
  navigateTree(targetId: string, options?: {
    summarize?: boolean;
    customInstructions?: string;
    replaceInstructions?: boolean;
    label?: string;
  }): Promise<{ editorText?: string; cancelled: boolean }>;

  // Compaction
  compact(customInstructions?: string): Promise<CompactionResult>;
  abortCompaction(): void;

  // Abort current operation
  abort(): Promise<void>;

  // Cleanup
  dispose(): void;
}
Session replacement operations — new session, resume, fork, and import — live on AgentSessionRuntime, not on AgentSession.

createAgentSessionRuntime()

Use createAgentSessionRuntime() when you need to replace the active session and rebuild cwd-bound runtime state. This is the layer used by interactive, print, and RPC modes.
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 session replacement across newSession(), switchSession(), fork(), clone flows, and importFromJsonl(). After any replacement, runtime.session points to the new session — re-subscribe to events on the new instance.
let session = runtime.session;
let unsubscribe = session.subscribe(() => {});

await runtime.newSession();

unsubscribe();
session = runtime.session;
unsubscribe = session.subscribe(() => {});

Session management

import { SessionManager } from "@earendil-works/pi-coding-agent";

// No persistence
SessionManager.inMemory()

// New persistent session
SessionManager.create(process.cwd())

// Continue most recent session
SessionManager.continueRecent(process.cwd())

// Open a specific file
SessionManager.open("/path/to/session.jsonl")

// List sessions
const current = await SessionManager.list(process.cwd());
const all = await SessionManager.listAll(process.cwd());
Sessions use a tree structure with id/parentId linking, enabling in-place branching.
const sm = SessionManager.open("/path/to/session.jsonl");

// Tree traversal
const entries = sm.getEntries();
const tree = sm.getTree();
const path = sm.getPath();
const leaf = sm.getLeafEntry();
const entry = sm.getEntry(id);
const children = sm.getChildren(id);

// Labels
const label = sm.getLabel(id);
sm.appendLabelChange(id, "checkpoint");

// Branching
sm.branch(entryId);
sm.branchWithSummary(id, "Summary...");
sm.createBranchedSession(leafId);
// Start a fresh session
await runtime.newSession();

// Switch to a saved session
await runtime.switchSession("/path/to/session.jsonl");

// Fork from a specific user entry
await runtime.fork("entry-id");

// Clone the active path at a specific entry
await runtime.fork("entry-id", { position: "at" });

Model configuration

import { getModel } from "@earendil-works/pi-ai";
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";

const authStorage = AuthStorage.create();
const modelRegistry = ModelRegistry.create(authStorage);

// Find a built-in model by provider and id
const opus = getModel("anthropic", "claude-opus-4-5");

// Find any model including custom models from models.json
const custom = modelRegistry.find("my-provider", "my-model");

// Get only models with valid API keys configured
const available = await modelRegistry.getAvailable();

const { session } = await createAgentSession({
  model: opus,
  thinkingLevel: "medium", // off, minimal, low, medium, high, xhigh

  // Models available for cycling (e.g. Ctrl+P in interactive mode)
  scopedModels: [
    { model: opus, thinkingLevel: "high" },
    { model: haiku, thinkingLevel: "off" },
  ],

  authStorage,
  modelRegistry,
});
When no model is provided, Pi tries to restore from session, falls back to the settings default, then falls back to the first available model.

API keys and OAuth

API key resolution priority (handled by AuthStorage):
  1. Runtime overrides via setRuntimeApiKey() — not persisted to disk
  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
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";

// Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json
const authStorage = AuthStorage.create();
const modelRegistry = ModelRegistry.create(authStorage);

// Runtime override — not persisted
authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key");

// Custom storage location
const customAuth = AuthStorage.create("/my/app/auth.json");
const customRegistry = ModelRegistry.create(customAuth, "/my/app/models.json");

// Built-in models only, no models.json
const simpleRegistry = ModelRegistry.inMemory(authStorage);

Tools

Built-in tool sets

import {
  codingTools,    // [read, bash, edit, write] — default
  readOnlyTools,  // [read, grep, find, ls]
  readTool, bashTool, editTool, writeTool,
  grepTool, findTool, lsTool,
} from "@earendil-works/pi-coding-agent";

// Use a built-in tool set
const { session } = await createAgentSession({ tools: readOnlyTools });

// Pick individual tools
const { session } = await createAgentSession({ tools: [readTool, bashTool, grepTool] });

Tools with a custom cwd

The pre-built tool instances (readTool, bashTool, etc.) use process.cwd() for path resolution. When you specify a custom cwd and provide explicit tools, use the factory functions instead:
import {
  createCodingTools,
  createReadOnlyTools,
  createReadTool, createBashTool, createEditTool, createWriteTool,
  createGrepTool, createFindTool, createLsTool,
} from "@earendil-works/pi-coding-agent";

const cwd = "/path/to/project";

const { session } = await createAgentSession({
  cwd,
  tools: createCodingTools(cwd),
});

// Or pick specific tools
const { session } = await createAgentSession({
  cwd,
  tools: [createReadTool(cwd), createBashTool(cwd), createGrepTool(cwd)],
});
If you omit tools, Pi automatically creates them with the correct cwd. You only need factory functions when you specify both a custom cwd and explicit tools.

Custom tools

import { Type } from "typebox";
import { createAgentSession, defineTool } from "@earendil-works/pi-coding-agent";

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: {},
  }),
});

const { session } = await createAgentSession({
  customTools: [myTool],
});
Custom tools passed via customTools are combined with tools registered by extensions via pi.registerTool().

Extensions

Extensions are loaded by the ResourceLoader. DefaultResourceLoader discovers extensions from ~/.pi/agent/extensions/, .pi/extensions/, and any extension sources in settings.json.
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("[Extension] Agent starting");
      });
    },
  ],
});
await loader.reload();

const { session } = await createAgentSession({ resourceLoader: loader });
To communicate with extensions from outside, pass a shared event bus:
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 extensions for the full extension API.

System prompt override

Use DefaultResourceLoader with systemPromptOverride to replace the system prompt:
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 });

Events

Subscribe to AgentSession events to receive streaming output and lifecycle notifications.
session.subscribe((event) => {
  switch (event.type) {
    // Streaming text delta from the assistant
    case "message_update":
      if (event.assistantMessageEvent.type === "text_delta") {
        process.stdout.write(event.assistantMessageEvent.delta);
      }
      if (event.assistantMessageEvent.type === "thinking_delta") {
        // Thinking output (when thinking is enabled)
      }
      break;

    // Tool execution
    case "tool_execution_start":
      console.log(`Tool started: ${event.toolName}`);
      break;
    case "tool_execution_update":
      // Streaming tool output
      break;
    case "tool_execution_end":
      console.log(`Tool finished: ${event.isError ? "error" : "success"}`);
      break;

    // Message lifecycle
    case "message_start":
    case "message_end":
      break;

    // Agent lifecycle
    case "agent_start":
      break;
    case "agent_end":
      // 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;

    // Queue, compaction, and 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":
      break;
  }
});
Full list of message_update delta types:
Delta typeDescription
startMessage generation started
text_startText content block started
text_deltaText content chunk
text_endText content block ended
thinking_startThinking block started
thinking_deltaThinking content chunk
thinking_endThinking block ended
toolcall_startTool call started
toolcall_deltaTool call arguments chunk
toolcall_endTool call ended (includes full toolCall object)
doneMessage complete ("stop", "length", or "toolUse")
errorError occurred ("aborted" or "error")

Run modes

The SDK exports three run mode utilities for building custom interfaces on top of createAgentSession().
Full TUI with editor, chat history, and all built-in commands:
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();

SDK vs RPC mode

Use the SDK when

  • You want type safety and IDE autocomplete
  • You’re in the same Node.js process
  • You need direct access to agent state and messages
  • You want to customize tools or extensions programmatically

Use RPC mode when

  • You’re integrating from another language
  • You want process isolation
  • You’re building a language-agnostic client

Full exports

// Factory
createAgentSession
createAgentSessionRuntime
AgentSessionRuntime

// Auth and models
AuthStorage
ModelRegistry

// Resource loading
DefaultResourceLoader
type ResourceLoader
createEventBus

// Helpers
defineTool

// Session management
SessionManager
SettingsManager

// Built-in tools (use process.cwd())
codingTools
readOnlyTools
readTool, bashTool, editTool, writeTool
grepTool, findTool, lsTool

// Tool factories (for custom cwd)
createCodingTools
createReadOnlyTools
createReadTool, createBashTool, createEditTool, createWriteTool
createGrepTool, createFindTool, createLsTool

// Types
type CreateAgentSessionOptions
type CreateAgentSessionResult
type ExtensionFactory
type ExtensionAPI
type ToolDefinition
type Skill
type PromptTemplate
type Tool

Build docs developers (and LLMs) love