# SDK examples

> Copy-paste SDK recipes: minimal agent, custom model, skills, tools, extensions, sessions, settings, and full-control setups.

- Repository: earendil-works/pi
- GitHub: https://github.com/earendil-works/pi
- Human docs: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1
- Complete Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/llms-full.txt

## Source Files

- `packages/coding-agent/examples/sdk/README.md`
- `packages/coding-agent/examples/sdk/01-minimal.ts`
- `packages/coding-agent/examples/sdk/11-sessions.ts`
- `packages/coding-agent/examples/sdk/05-tools.ts`
- `packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts`
- `packages/coding-agent/examples/sdk/12-full-control.ts`

---

---
title: "SDK examples"
description: "Copy-paste SDK recipes: minimal agent, custom model, skills, tools, extensions, sessions, settings, and full-control setups."
---

`createAgentSession()` is the primary embed surface for `@earendil-works/pi-coding-agent`. The recipes under `packages/coding-agent/examples/sdk/` construct a session, optionally wire `ModelRuntime`, `SessionManager`, `SettingsManager`, and a `ResourceLoader`, then run prompts through `session.subscribe()` / `session.prompt()` and tear down with `session.dispose()`. A companion path, `createAgentSessionRuntime()`, appears in the runtime example for recreating cwd-bound services when the active session cwd changes.

## Example catalog

| File | Recipe |
|------|--------|
| `01-minimal.ts` | All defaults: discovery from `cwd` and `~/.pi/agent`, model from settings or first available |
| `02-custom-model.ts` | Select model and thinking level |
| `03-custom-prompt.ts` | Replace or modify system prompt |
| `04-skills.ts` | Discover, filter, or replace skills |
| `05-tools.ts` | Built-in tool allowlists and custom `cwd` |
| `06-extensions.ts` | Logging, blocking, result modification; custom tools via `pi.registerTool()` |
| `07-context-files.ts` | `AGENTS.md` context files |
| `08-slash-commands.ts` | File-based slash commands |
| `09-api-keys-and-oauth.ts` | API key resolution and OAuth via `ModelRuntime` |
| `10-settings.ts` | Override compaction, retry, terminal settings |
| `11-sessions.ts` | In-memory, new file, continue recent, list/open |
| `12-full-control.ts` | Explicit configuration, no discovery |
| `13-session-runtime.ts` | Runtime-backed session replacement |

## Run an example

```bash
cd packages/coding-agent
npx tsx examples/sdk/01-minimal.ts
```

Replace the filename with any entry in the catalog.

## Minimal agent

Uses package defaults: discovers skills, extensions, tools, and context files from `cwd` and `~/.pi/agent`; model comes from settings or the first available model.

```typescript
import { createAgentSession } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession();

try {
  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?");
  session.state.messages.forEach((msg) => {
    console.log(msg);
  });
} finally {
  session.dispose();
}
```

With an explicit runtime (same defaults otherwise):

```typescript
import { createAgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({ modelRuntime });
```

## Custom model and thinking level

```typescript
import { getModel } from "@earendil-works/pi-ai";
import { createAgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const model = getModel("anthropic", "claude-opus-4-5");
const { session } = await createAgentSession({
  model,
  thinkingLevel: "high",
  modelRuntime,
});
```

`thinkingLevel` values: `off`, `low`, `medium`, `high`. Default is from settings, otherwise `"off"`.

In the full-control recipe, model resolution uses `@earendil-works/pi-ai/compat`:

```typescript
import { getModel } from "@earendil-works/pi-ai/compat";

const model = getModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Model not found");
```

## System prompt override

`DefaultResourceLoader` accepts `systemPromptOverride`. Call `reload()` before passing the loader into `createAgentSession`.

```typescript
import {
  createAgentSession,
  DefaultResourceLoader,
  ModelRuntime,
} from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const loader = new DefaultResourceLoader({
  systemPromptOverride: (base) => `${base}\n\nBe concise.`,
});
await loader.reload();
const { session } = await createAgentSession({ resourceLoader: loader, modelRuntime });
```

Replace the entire prompt by returning a fixed string from the override:

```typescript
systemPromptOverride: () => "You are helpful.",
```

## Skills

Disable discovery and supply an empty skill set via `skillsOverride` on `DefaultResourceLoader`:

```typescript
const resourceLoader = new DefaultResourceLoader({
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
});
await resourceLoader.reload();
```

The same loader pattern supports related resource overrides used in full-control setups: `agentsFilesOverride`, `promptsOverride`, and `extensionFactories`.

## Tools

Tool names are an allowlist matched against built-in, extension, and custom tools. Default built-ins when unset: `["read", "bash", "edit", "write"]`.

If you pass a custom `cwd`, `createAgentSession()` applies that cwd when it builds built-in tools. Custom tools are not registered through the `tools` array alone; register them through the extensions system with `pi.registerTool()` (see `06-extensions.ts`).

```typescript
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

// Read-only (no edit/write)
const { session: readOnlySession } = await createAgentSession({
  tools: ["read", "grep", "find", "ls"],
  sessionManager: SessionManager.inMemory(),
});
readOnlySession.dispose();

// Subset of tools
const { session: customToolsSession } = await createAgentSession({
  tools: ["read", "bash", "grep"],
  sessionManager: SessionManager.inMemory(),
});
customToolsSession.dispose();

// Custom cwd + full built-in write surface
const customCwd = "/path/to/project";
const { session: customCwdSession } = await createAgentSession({
  cwd: customCwd,
  tools: ["read", "bash", "edit", "write"],
  sessionManager: SessionManager.inMemory(customCwd),
});
customCwdSession.dispose();
```

Quick-reference allowlist with a named custom tool:

```typescript
const { session } = await createAgentSession({
  tools: ["read", "bash", "my_tool"],
  customTools: [myTool],
  modelRuntime,
});
```

`customTools` defaults to `[]`.

## Extensions and resource factories

Wire extension factories and resource overrides through `DefaultResourceLoader`:

```typescript
const resourceLoader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are helpful.",
  extensionFactories: [myExtension],
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
  agentsFilesOverride: () => ({ agentsFiles: [] }),
  promptsOverride: () => ({ prompts: [], diagnostics: [] }),
});
await resourceLoader.reload();
```

`06-extensions.ts` covers logging, blocking, and result modification patterns. Always `reload()` after constructing a loader that uses overrides or factories.

## Authentication and ModelRuntime

Configure provider auth through `ModelRuntime`. Default runtime paths resolve under `agentDir` (`auth.json`, `models.json`).

```typescript
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";

// Default auth locations
const modelRuntime = await ModelRuntime.create();
const { session: defaultAuthSession } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime,
});
defaultAuthSession.dispose();

// Custom auth and models paths
const customRuntime = await ModelRuntime.create({
  authPath: "/tmp/my-app/auth.json",
  modelsPath: "/tmp/my-app/models.json",
});
const { session: customAuthSession } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime: customRuntime,
});
customAuthSession.dispose();

// Runtime API key override (not persisted as the sole source of truth for disk auth)
await modelRuntime.setRuntimeApiKey("anthropic", "sk-my-temp-key");
const { session: runtimeKeySession } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime,
});
runtimeKeySession.dispose();
```

Full-control setups often gate keys on env vars:

```typescript
if (process.env.MY_ANTHROPIC_KEY) {
  await modelRuntime.setRuntimeApiKey("anthropic", process.env.MY_ANTHROPIC_KEY);
}
```

## Sessions

`SessionManager` controls persistence. `createAgentSession` returns `{ session }` and may also return `modelFallbackMessage` when continuing a session with a changed model.

```typescript
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

// In-memory (no persistence)
const { session: inMemory } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
});
console.log("In-memory session:", inMemory.sessionFile ?? "(none)");
inMemory.dispose();

// New persistent session under cwd
const { session: newSession } = await createAgentSession({
  sessionManager: SessionManager.create(process.cwd()),
});
console.log("New session file:", newSession.sessionFile);
newSession.dispose();

// Continue most recent (or create new if none)
const { session: continued, modelFallbackMessage } = await createAgentSession({
  sessionManager: SessionManager.continueRecent(process.cwd()),
});
if (modelFallbackMessage) console.log("Note:", modelFallbackMessage);
console.log("Continued session:", continued.sessionFile);
continued.dispose();

// List and open a specific session
const sessions = await SessionManager.list(process.cwd());
if (sessions.length > 0) {
  const { session: opened } = await createAgentSession({
    sessionManager: SessionManager.open(sessions[0].path),
  });
  console.log("Opened:", opened.sessionId);
  opened.dispose();
}
```

Optional custom session directory (no cwd encoding), shown commented in the example:

```typescript
// const customDir = "/path/to/my-sessions";
// SessionManager.create(process.cwd(), customDir);
// SessionManager.list(process.cwd(), customDir);
// SessionManager.continueRecent(process.cwd(), customDir);
```

Default when `sessionManager` is omitted: `SessionManager.create(cwd)`.

## Settings

Use `SettingsManager.inMemory()` for process-local overrides without disk settings:

```typescript
import { SettingsManager } from "@earendil-works/pi-coding-agent";

const settingsManager = SettingsManager.inMemory({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 2 },
});
```

Default when omitted: `SettingsManager.create(cwd, agentDir)`. Example `10-settings.ts` targets compaction, retry, and terminal settings overrides.

## Full control

Replace discovery with an explicit `ResourceLoader`, fixed `agentDir`, selected tools, in-memory session and settings, and a dedicated `ModelRuntime`.

```typescript
import { getModel } from "@earendil-works/pi-ai/compat";
import {
  createAgentSession,
  createExtensionRuntime,
  ModelRuntime,
  type ResourceLoader,
  SessionManager,
  SettingsManager,
} from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create({
  authPath: "/tmp/my-agent/auth.json",
  modelsPath: "/tmp/my-agent/models.json",
});
if (process.env.MY_ANTHROPIC_KEY) {
  await modelRuntime.setRuntimeApiKey("anthropic", process.env.MY_ANTHROPIC_KEY);
}

const model = getModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Model not found");

const settingsManager = SettingsManager.inMemory({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 2 },
});

const cwd = process.cwd();

const resourceLoader: ResourceLoader = {
  getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
  getSkills: () => ({ skills: [], diagnostics: [] }),
  getPrompts: () => ({ prompts: [], diagnostics: [] }),
  getThemes: () => ({ themes: [], diagnostics: [] }),
  getAgentsFiles: () => ({ agentsFiles: [] }),
  getSystemPrompt: () => `You are a minimal assistant.
Available: read, bash. Be concise.`,
  getSystemPromptSource: () => undefined,
  getAppendSystemPrompt: () => [],
  getAppendSystemPromptSources: () => [],
  extendResources: () => {},
  reload: async () => {},
};

const { session } = await createAgentSession({
  cwd,
  agentDir: "/tmp/my-agent",
  model,
  thinkingLevel: "off",
  modelRuntime,
  resourceLoader,
  tools: ["read", "bash"],
  sessionManager: SessionManager.inMemory(cwd),
  settingsManager,
});

try {
  session.subscribe((event) => {
    if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
      process.stdout.write(event.assistantMessageEvent.delta);
    }
  });
  await session.prompt("List files in the current directory.");
} finally {
  session.dispose();
}
```

Equivalent composition with `DefaultResourceLoader` + overrides (from the examples README quick reference):

```typescript
const customRuntime = await ModelRuntime.create({
  authPath: "/my/app/auth.json",
  modelsPath: "/my/app/models.json",
});
await customRuntime.setRuntimeApiKey("anthropic", process.env.MY_KEY!);

const resourceLoader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are helpful.",
  extensionFactories: [myExtension],
  skillsOverride: () => ({ skills: [], diagnostics: [] }),
  agentsFilesOverride: () => ({ agentsFiles: [] }),
  promptsOverride: () => ({ prompts: [], diagnostics: [] }),
});
await resourceLoader.reload();

const { session } = await createAgentSession({
  model,
  modelRuntime: customRuntime,
  resourceLoader,
  tools: ["read", "bash", "my_tool"],
  customTools: [myTool],
  sessionManager: SessionManager.inMemory(),
  settingsManager: SettingsManager.inMemory(),
});
```

## Session runtime recreation

`13-session-runtime.ts` shows `createAgentSessionRuntime()` with a recreate function that closes over process-global fixed inputs and rebuilds cwd-bound services and sessions when the active session cwd changes. Use that path when the host process must swap working directories without restarting the agent process.

## Events

Subscribe before `prompt()`. Common event types used in the examples:

| `event.type` | Use |
|--------------|-----|
| `message_update` | Stream text when `assistantMessageEvent.type === "text_delta"`; write `assistantMessageEvent.delta` |
| `tool_execution_start` | Log `event.toolName` |
| `tool_execution_end` | Log `event.result` |
| `agent_end` | Turn complete |

```typescript
session.subscribe((event) => {
  switch (event.type) {
    case "message_update":
      if (event.assistantMessageEvent.type === "text_delta") {
        process.stdout.write(event.assistantMessageEvent.delta);
      }
      break;
    case "tool_execution_start":
      console.log(`Tool: ${event.toolName}`);
      break;
    case "tool_execution_end":
      console.log(`Result: ${event.result}`);
      break;
    case "agent_end":
      console.log("Done");
      break;
  }
});
await session.prompt("Hello");
```

After a turn, inspect history with `session.state.messages`. Always call `session.dispose()` in a `finally` block.

## `createAgentSession` options

| Option | Default | Description |
|--------|---------|-------------|
| `modelRuntime` | Runtime using `agentDir/auth.json` and `models.json` | Canonical model and authentication runtime |
| `cwd` | `process.cwd()` | Working directory |
| `agentDir` | `~/.pi/agent` | Config directory |
| `model` | From settings / first available | Model to use |
| `thinkingLevel` | From settings / `"off"` | `off`, `low`, `medium`, `high` |
| `tools` | `["read", "bash", "edit", "write"]` built-ins | Allowlist across built-in, extension, and custom tools |
| `customTools` | `[]` | Additional tool definitions |
| `resourceLoader` | `DefaultResourceLoader` | Extensions, skills, prompts, themes, context files |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
| `settingsManager` | `SettingsManager.create(cwd, agentDir)` | Settings overrides |

## Lifecycle checklist

```text
ModelRuntime.create(...)     optional auth / models paths, setRuntimeApiKey
DefaultResourceLoader / ResourceLoader
  └─ reload()                required after override/factory construction
SettingsManager.inMemory|create
SessionManager.inMemory|create|continueRecent|open
createAgentSession({ ... })
  ├─ session.subscribe(...)
  ├─ await session.prompt(...)
  ├─ session.state.messages
  └─ session.dispose()
```

## Related pages

<CardGroup>
  <Card title="SDK" href="/sdk">
    Package main export: minimal construction, hooks, custom models, tools, and settings.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    AgentSessionRuntime services, lifecycle events, and embedding without the TUI.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default tools, extension tools, allowlists, and blocked-tool behavior.
  </Card>
  <Card title="Skills" href="/skills">
    SKILL.md rules, naming, collisions, and invocation controls.
  </Card>
  <Card title="Extensions" href="/extensions">
    TypeScript extension registration, active tools, and shutdown contracts.
  </Card>
  <Card title="Agent sessions" href="/agent-sessions">
    Session lifecycle, prompt queue, and runtime ownership of a turn.
  </Card>
  <Card title="Settings" href="/settings">
    Settings load, reload, and merge rules.
  </Card>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth, credential storage, and auth failure modes.
  </Card>
  <Card title="Package exports" href="/package-exports">
    Public npm surface for @earendil-works/pi-coding-agent.
  </Card>
  <Card title="Extension examples" href="/extension-examples">
    Reference extension packages: subagents, plan-mode, and related samples.
  </Card>
</CardGroup>
