Core tools now properly use the cwd passed to createAgentSession(). Added tool factory functions for SDK users who specify custom cwd with explicit tools. Fixes #279
22 KiB
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/ for working examples from minimal to full control.
Quick Start
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
});
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
npm install @mariozechner/pi-coding-agent
The SDK is included in the main package. No separate installation needed.
Core Concepts
createAgentSession()
The main factory function. Creates an AgentSession with configurable options.
Philosophy: "Omit to discover, provide to override."
- Omit an option → pi discovers/loads from standard locations
- Provide an option → your value is used, discovery skipped for that option
import { createAgentSession } from "@mariozechner/pi-coding-agent";
// Minimal: all defaults (discovers everything from cwd and ~/.pi/agent)
const { session } = await createAgentSession();
// Custom: override specific options
const { session } = await createAgentSession({
model: myModel,
systemPrompt: "You are helpful.",
tools: [readTool, bashTool],
sessionManager: SessionManager.inMemory(),
});
AgentSession
The session manages the agent lifecycle, message history, and event streaming.
interface AgentSession {
// Send a prompt and wait for completion
prompt(text: string, options?: PromptOptions): Promise<void>;
// Subscribe to events (returns unsubscribe function)
subscribe(listener: (event: AgentSessionEvent) => void): () => void;
// Session info
sessionFile: string | null;
sessionId: string;
// Model control
setModel(model: Model): Promise<void>;
setThinkingLevel(level: ThinkingLevel): void;
cycleModel(): Promise<ModelCycleResult | null>;
cycleThinkingLevel(): ThinkingLevel | null;
// State access
agent: Agent;
model: Model | null;
thinkingLevel: ThinkingLevel;
messages: AppMessage[];
isStreaming: boolean;
// Session management
reset(): Promise<void>;
branch(entryIndex: number): Promise<{ selectedText: string; skipped: boolean }>;
switchSession(sessionPath: string): Promise<void>;
// Compaction
compact(customInstructions?: string): Promise<CompactionResult>;
abortCompaction(): void;
// Abort current operation
abort(): Promise<void>;
// Cleanup
dispose(): void;
}
Agent and AgentState
The Agent class (from @mariozechner/pi-agent-core) handles the core LLM interaction. Access it via session.agent.
// Access current state
const state = session.agent.state;
// state.messages: AppMessage[] - conversation history
// state.model: Model - current model
// state.thinkingLevel: ThinkingLevel - current thinking level
// state.systemPrompt: string - system prompt
// state.tools: Tool[] - available tools
// Replace messages (useful for branching, restoration)
session.agent.replaceMessages(messages);
// Wait for agent to finish processing
await session.agent.waitForIdle();
Events
Subscribe to events to receive streaming output and lifecycle notifications.
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 (auto-compaction, retry)
case "auto_compaction_start":
case "auto_compaction_end":
case "auto_retry_start":
case "auto_retry_end":
break;
}
});
Options Reference
Directories
const { session } = await createAgentSession({
// Working directory for project-local discovery
cwd: process.cwd(), // default
// Global config directory
agentDir: "~/.pi/agent", // default (expands ~)
});
cwd is used for:
- Project hooks (
.pi/hooks/) - Project tools (
.pi/tools/) - Project skills (
.pi/skills/) - Project commands (
.pi/commands/) - Context files (
AGENTS.mdwalking up from cwd) - Session directory naming
agentDir is used for:
- Global hooks (
hooks/) - Global tools (
tools/) - Global skills (
skills/) - Global commands (
commands/) - Global context file (
AGENTS.md) - Settings (
settings.json) - Models (
models.json) - OAuth tokens (
oauth.json) - Sessions (
sessions/)
Model
import { findModel, discoverAvailableModels } from "@mariozechner/pi-coding-agent";
// Find specific model (returns { model, error })
const { model, error } = findModel("anthropic", "claude-sonnet-4-20250514");
if (error) throw new Error(error);
if (!model) throw new Error("Model not found");
// Or get all models with valid API keys
const available = await discoverAvailableModels();
const { session } = await createAgentSession({
model: model,
thinkingLevel: "medium", // off, minimal, low, medium, high, xhigh
// Models for cycling (Ctrl+P in interactive mode)
scopedModels: [
{ model: sonnet, thinkingLevel: "high" },
{ model: haiku, thinkingLevel: "off" },
],
});
If no model is provided:
- Tries to restore from session (if continuing)
- Uses default from settings
- Falls back to first available model
API Keys
import { defaultGetApiKey, configureOAuthStorage } from "@mariozechner/pi-coding-agent";
// Default: checks models.json, OAuth, environment variables
const { session } = await createAgentSession();
// Custom resolver
const { session } = await createAgentSession({
getApiKey: async (model) => {
// Custom logic (secrets manager, database, etc.)
if (model.provider === "anthropic") {
return process.env.MY_ANTHROPIC_KEY;
}
// Fall back to default
return defaultGetApiKey()(model);
},
});
// Use OAuth from ~/.pi/agent with custom agentDir for everything else
configureOAuthStorage(); // Must call before createAgentSession
const { session } = await createAgentSession({
agentDir: "/custom/config",
// OAuth tokens still come from ~/.pi/agent/oauth.json
});
System Prompt
const { session } = await createAgentSession({
// Replace entirely
systemPrompt: "You are a helpful assistant.",
// Or modify default (receives default, returns modified)
systemPrompt: (defaultPrompt) => {
return `${defaultPrompt}\n\n## Additional Rules\n- Be concise`;
},
});
Tools
import {
codingTools, // read, bash, edit, write (default)
readOnlyTools, // read, grep, find, ls
readTool, bashTool, editTool, writeTool,
grepTool, findTool, lsTool,
} from "@mariozechner/pi-coding-agent";
// Use built-in tool set
const { session } = await createAgentSession({
tools: readOnlyTools,
});
// Pick specific tools
const { session } = await createAgentSession({
tools: [readTool, bashTool, grepTool],
});
Tools with Custom cwd
Important: The pre-built tool instances (readTool, bashTool, etc.) use process.cwd() for path resolution. When you specify a custom cwd AND provide explicit tools, you must use the tool factory functions to ensure paths resolve correctly:
import {
createCodingTools, // Creates [read, bash, edit, write] for specific cwd
createReadOnlyTools, // Creates [read, grep, find, ls] for specific cwd
createReadTool,
createBashTool,
createEditTool,
createWriteTool,
createGrepTool,
createFindTool,
createLsTool,
} from "@mariozechner/pi-coding-agent";
const cwd = "/path/to/project";
// Use factory for tool sets
const { session } = await createAgentSession({
cwd,
tools: createCodingTools(cwd), // Tools resolve paths relative to cwd
});
// Or pick specific tools
const { session } = await createAgentSession({
cwd,
tools: [createReadTool(cwd), createBashTool(cwd), createGrepTool(cwd)],
});
When you don't need factories:
- If you omit
tools, pi automatically creates them with the correctcwd - If you use
process.cwd()as yourcwd, the pre-built instances work fine
When you must use factories:
- When you specify both
cwd(different fromprocess.cwd()) ANDtools
Custom Tools
import { Type } from "@sinclair/typebox";
import { createAgentSession, discoverCustomTools, type CustomAgentTool } from "@mariozechner/pi-coding-agent";
// Inline custom tool
const myTool: CustomAgentTool = {
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: {},
}),
};
// Replace discovery with inline tools
const { session } = await createAgentSession({
customTools: [{ tool: myTool }],
});
// Merge with discovered tools
const discovered = await discoverCustomTools();
const { session } = await createAgentSession({
customTools: [...discovered, { tool: myTool }],
});
// Add paths without replacing discovery
const { session } = await createAgentSession({
additionalCustomToolPaths: ["/extra/tools"],
});
Hooks
import { createAgentSession, discoverHooks, type HookFactory } from "@mariozechner/pi-coding-agent";
// Inline hook
const loggingHook: HookFactory = (api) => {
api.on("tool_call", async (event) => {
console.log(`Tool: ${event.toolName}`);
return undefined; // Don't block
});
api.on("tool_call", async (event) => {
// Block dangerous commands
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
return { block: true, reason: "Dangerous command" };
}
return undefined;
});
};
// Replace discovery
const { session } = await createAgentSession({
hooks: [{ factory: loggingHook }],
});
// Disable all hooks
const { session } = await createAgentSession({
hooks: [],
});
// Merge with discovered
const discovered = await discoverHooks();
const { session } = await createAgentSession({
hooks: [...discovered, { factory: loggingHook }],
});
// Add paths without replacing
const { session } = await createAgentSession({
additionalHookPaths: ["/extra/hooks"],
});
Skills
import { createAgentSession, discoverSkills, type Skill } from "@mariozechner/pi-coding-agent";
// Discover and filter
const allSkills = discoverSkills();
const filtered = allSkills.filter(s => s.name.includes("search"));
// Custom skill
const mySkill: Skill = {
name: "my-skill",
description: "Custom instructions",
filePath: "/path/to/SKILL.md",
baseDir: "/path/to",
source: "custom",
};
const { session } = await createAgentSession({
skills: [...filtered, mySkill],
});
// Disable skills
const { session } = await createAgentSession({
skills: [],
});
// Discovery with settings filter
const skills = discoverSkills(process.cwd(), undefined, {
ignoredSkills: ["browser-*"], // glob patterns to exclude
includeSkills: ["search-*"], // glob patterns to include (empty = all)
});
Context Files
import { createAgentSession, discoverContextFiles } from "@mariozechner/pi-coding-agent";
// Discover AGENTS.md files
const discovered = discoverContextFiles();
// Add custom context
const { session } = await createAgentSession({
contextFiles: [
...discovered,
{
path: "/virtual/AGENTS.md",
content: "# Guidelines\n\n- Be concise\n- Use TypeScript",
},
],
});
// Disable context files
const { session } = await createAgentSession({
contextFiles: [],
});
Slash Commands
import { createAgentSession, discoverSlashCommands, type FileSlashCommand } from "@mariozechner/pi-coding-agent";
const discovered = discoverSlashCommands();
const customCommand: FileSlashCommand = {
name: "deploy",
description: "Deploy the application",
source: "(custom)",
content: "# Deploy\n\n1. Build\n2. Test\n3. Deploy",
};
const { session } = await createAgentSession({
slashCommands: [...discovered, customCommand],
});
Session Management
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
// In-memory (no persistence)
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
});
// New persistent session
const { session } = await createAgentSession({
sessionManager: SessionManager.create(process.cwd()),
});
// Continue most recent
const { session, modelFallbackMessage } = await createAgentSession({
sessionManager: SessionManager.continueRecent(process.cwd()),
});
if (modelFallbackMessage) {
console.log("Note:", modelFallbackMessage);
}
// Open specific file
const { session } = await createAgentSession({
sessionManager: SessionManager.open("/path/to/session.jsonl"),
});
// List available sessions
const sessions = SessionManager.list(process.cwd());
for (const info of sessions) {
console.log(`${info.id}: ${info.firstMessage} (${info.messageCount} messages)`);
}
// Custom agentDir for sessions
const { session } = await createAgentSession({
agentDir: "/custom/agent",
sessionManager: SessionManager.create(process.cwd(), "/custom/agent"),
});
Settings Management
import { createAgentSession, SettingsManager, SessionManager } from "@mariozechner/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 filesSettingsManager.inMemory(settings?)- No file I/O
Project-specific settings:
Settings load from two locations and merge:
- Global:
~/.pi/agent/settings.json - Project:
<cwd>/.pi/settings.json
Project overrides global. Nested objects merge keys. Setters only modify global (project is read-only for version control).
Discovery Functions
All discovery functions accept optional cwd and agentDir parameters.
import {
discoverModels,
discoverAvailableModels,
findModel,
discoverSkills,
discoverHooks,
discoverCustomTools,
discoverContextFiles,
discoverSlashCommands,
loadSettings,
buildSystemPrompt,
} from "@mariozechner/pi-coding-agent";
// Models
const allModels = discoverModels();
const available = await discoverAvailableModels();
const { model, error } = findModel("anthropic", "claude-sonnet-4-20250514");
// Skills
const skills = discoverSkills(cwd, agentDir, skillsSettings);
// Hooks (async - loads TypeScript)
const hooks = await discoverHooks(cwd, agentDir);
// Custom tools (async - loads TypeScript)
const tools = await discoverCustomTools(cwd, agentDir);
// Context files
const contextFiles = discoverContextFiles(cwd, agentDir);
// Slash commands
const commands = discoverSlashCommands(cwd, agentDir);
// Settings (global + project merged)
const settings = loadSettings(cwd, agentDir);
// Build system prompt manually
const prompt = buildSystemPrompt({
skills,
contextFiles,
appendPrompt: "Additional instructions",
cwd,
});
Return Value
createAgentSession() returns:
interface CreateAgentSessionResult {
// The session
session: AgentSession;
// Custom tools (for UI setup)
customToolsResult: {
tools: LoadedCustomTool[];
setUIContext: (ctx, hasUI) => void;
};
// Warning if session model couldn't be restored
modelFallbackMessage?: string;
}
Complete Example
import { Type } from "@sinclair/typebox";
import {
createAgentSession,
configureOAuthStorage,
defaultGetApiKey,
findModel,
SessionManager,
SettingsManager,
readTool,
bashTool,
type HookFactory,
type CustomAgentTool,
} from "@mariozechner/pi-coding-agent";
import { getAgentDir } from "@mariozechner/pi-coding-agent/config";
// Use OAuth from default location
configureOAuthStorage(getAgentDir());
// Custom API key with fallback
const getApiKey = async (model: { provider: string }) => {
if (model.provider === "anthropic" && process.env.MY_KEY) {
return process.env.MY_KEY;
}
return defaultGetApiKey()(model as any);
};
// Inline hook
const auditHook: HookFactory = (api) => {
api.on("tool_call", async (event) => {
console.log(`[Audit] ${event.toolName}`);
return undefined;
});
};
// Inline tool
const statusTool: CustomAgentTool = {
name: "status",
label: "Status",
description: "Get system status",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: `Uptime: ${process.uptime()}s` }],
details: {},
}),
};
const { model, error } = findModel("anthropic", "claude-sonnet-4-20250514");
if (error) throw new Error(error);
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 { session } = await createAgentSession({
cwd: process.cwd(),
agentDir: "/custom/agent",
model,
thinkingLevel: "off",
getApiKey,
systemPrompt: "You are a minimal assistant. Be concise.",
tools: [readTool, bashTool],
customTools: [{ tool: statusTool }],
hooks: [{ factory: auditHook }],
skills: [],
contextFiles: [],
slashCommands: [],
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.");
RPC Mode Alternative
For subprocess-based integration, use RPC mode instead of the SDK:
pi --mode rpc --no-session
See RPC documentation 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/hooks 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:
// Factory
createAgentSession
configureOAuthStorage
// Discovery
discoverModels
discoverAvailableModels
findModel
discoverSkills
discoverHooks
discoverCustomTools
discoverContextFiles
discoverSlashCommands
// Helpers
defaultGetApiKey
loadSettings
buildSystemPrompt
// 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 CustomAgentTool
type HookFactory
type Skill
type FileSlashCommand
type Settings
type SkillsSettings
type Tool
For hook types, import from the hooks subpath:
import type { HookAPI, HookEvent, ToolCallEvent } from "@mariozechner/pi-coding-agent/hooks";
For config utilities:
import { getAgentDir } from "@mariozechner/pi-coding-agent/config";