mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-05-25 15:04:01 +00:00
1129 lines
32 KiB
Markdown
1129 lines
32 KiB
Markdown
> 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 { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
|
|
// Set up credential storage and model registry
|
|
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?");
|
|
```
|
|
|
|
## 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<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 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<CompactionResult>;
|
|
abortCompaction(): void;
|
|
|
|
// Abort current operation
|
|
abort(): Promise<void>;
|
|
|
|
// 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":
|
|
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 { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
|
|
const authStorage = AuthStorage.create();
|
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
|
|
// 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 = modelRegistry.find("my-provider", "my-model");
|
|
|
|
// Get only models that have valid API keys configured
|
|
const available = await modelRegistry.getAvailable();
|
|
|
|
const { session } = await createAgentSession({
|
|
model: opus,
|
|
thinkingLevel: "medium", // off, minimal, low, medium, high, xhigh
|
|
|
|
// Models for cycling (Ctrl+P in interactive mode)
|
|
scopedModels: [
|
|
{ model: opus, thinkingLevel: "high" },
|
|
{ model: haiku, thinkingLevel: "off" },
|
|
],
|
|
|
|
authStorage,
|
|
modelRegistry,
|
|
});
|
|
```
|
|
|
|
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
|
|
|
|
> See [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)
|
|
|
|
### API Keys and OAuth
|
|
|
|
API key resolution priority (handled by AuthStorage):
|
|
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 { 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);
|
|
|
|
const { session } = await createAgentSession({
|
|
sessionManager: SessionManager.inMemory(),
|
|
authStorage,
|
|
modelRegistry,
|
|
});
|
|
|
|
// Runtime API key override (not persisted to disk)
|
|
authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key");
|
|
|
|
// Custom auth storage location
|
|
const customAuth = AuthStorage.create("/my/app/auth.json");
|
|
const customRegistry = ModelRegistry.create(customAuth, "/my/app/models.json");
|
|
|
|
const { session } = await createAgentSession({
|
|
sessionManager: SessionManager.inMemory(),
|
|
authStorage: customAuth,
|
|
modelRegistry: customRegistry,
|
|
});
|
|
|
|
// No custom models.json (built-in models only)
|
|
const simpleRegistry = ModelRegistry.inMemory(authStorage);
|
|
```
|
|
|
|
> 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
|
|
|
|
```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"],
|
|
});
|
|
```
|
|
|
|
#### 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.
|
|
|
|
**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: `<cwd>/.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 {
|
|
AuthStorage,
|
|
createAgentSession,
|
|
DefaultResourceLoader,
|
|
defineTool,
|
|
ModelRegistry,
|
|
SessionManager,
|
|
SettingsManager,
|
|
} from "@earendil-works/pi-coding-agent";
|
|
|
|
// Set up auth storage (custom location)
|
|
const authStorage = AuthStorage.create("/custom/agent/auth.json");
|
|
|
|
// Runtime API key override (not persisted)
|
|
if (process.env.MY_KEY) {
|
|
authStorage.setRuntimeApiKey("anthropic", process.env.MY_KEY);
|
|
}
|
|
|
|
// Model registry (no custom models.json)
|
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
|
|
// 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",
|
|
authStorage,
|
|
modelRegistry,
|
|
|
|
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
|
|
AuthStorage
|
|
ModelRegistry
|
|
|
|
// Resource loading
|
|
DefaultResourceLoader
|
|
type ResourceLoader
|
|
createEventBus
|
|
|
|
// Helpers
|
|
defineTool
|
|
|
|
// Session management
|
|
SessionManager
|
|
SettingsManager
|
|
|
|
// Tool factories
|
|
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
|
|
```
|
|
|
|
For extension types, see [extensions.md](extensions.md) for the full API.
|