# Minimal SDK agent

> Copy-paste minimal SDK bootstrap, custom prompt and model wiring, and expected first-run output from examples.

- Repository: PrimeIntellect-ai/prime-agent
- GitHub: https://github.com/PrimeIntellect-ai/prime-agent
- Human docs: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1
- Complete Markdown: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1/llms-full.txt

## Source Files

- `packages/coding-agent/examples/sdk/01-minimal.ts`
- `packages/coding-agent/examples/sdk/README.md`
- `packages/coding-agent/examples/sdk/02-custom-model.ts`
- `packages/coding-agent/examples/sdk/03-custom-prompt.ts`
- `packages/coding-agent/examples/README.md`

---

---
title: "Minimal SDK agent"
description: "Copy-paste minimal SDK bootstrap, custom prompt and model wiring, and expected first-run output from examples."
---

`createAgentSession()` in `@earendil-works/pi-coding-agent` is the programmatic factory for a single `AgentSession`. With no options it discovers resources from `process.cwd()` and `~/.prime/agent`, resolves a model from settings or the first available credentialed model, enables the default built-in tool `ipython`, and returns `{ session, extensionsResult, modelFallbackMessage? }`.

The published TypeScript packages still use `@earendil-works/pi-*` package names. Those are API identifiers, not a hard dependency on an external monorepo layout. Provider credentials are BYOK: keys come from `AuthStorage` (runtime overrides, `auth.json`, env vars, or custom resolvers), not from a fixed hosted endpoint.

## Prerequisites

| Requirement | Detail |
|-------------|--------|
| Package | `npm install @earendil-works/pi-coding-agent` (SDK ships in the main package) |
| Runtime | Node with ESM; examples run via `npx tsx` |
| Auth | At least one provider credential so a model can be resolved |
| Working directory | Defaults to `process.cwd()` for project discovery and sessions |
| Agent config dir | Defaults to `~/.prime/agent` via `getAgentDir()`; override with `PRIME_AGENT_CODING_AGENT_DIR` |

Without a resolvable model, `createAgentSession()` still returns a session, but `modelFallbackMessage` is set to a “No models available” guidance string and `thinkingLevel` is forced to `"off"`. Prompting then fails at stream time when no API key/model is usable.

## Minimal bootstrap

Matches `packages/coding-agent/examples/sdk/01-minimal.ts`: all defaults, stream assistant text, then inspect message history.

```typescript
/**
 * Minimal SDK Usage
 *
 * Uses all defaults: discovers skills, extensions, tools, context files
 * from cwd and ~/.pi/agent. Model chosen from settings or first available.
 */

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

const { session } = await createAgentSession();

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);
});
console.log();
```

<Note>
Comment text in the example still mentions `~/.pi/agent`. Runtime config uses package `piConfig.configDir` (`.prime/agent`), so the effective default directory is `~/.prime/agent` unless `PRIME_AGENT_CODING_AGENT_DIR` overrides it.
</Note>

### What defaults do

When options are omitted, `createAgentSession()` wires:

| Surface | Default |
|---------|---------|
| `cwd` | `process.cwd()` (or session manager cwd if provided) |
| `agentDir` | `getAgentDir()` → `~/.prime/agent` or `PRIME_AGENT_CODING_AGENT_DIR` |
| `authStorage` | `AuthStorage.create()` under `agentDir/auth.json` when `agentDir` is set |
| `modelRegistry` | `ModelRegistry.create(authStorage, agentDir/models.json)` |
| `resourceLoader` | `DefaultResourceLoader` + `reload()` (skills, extensions, prompts, themes, context files) |
| `sessionManager` | `SessionManager.create(cwd, …)` (persistent under the session dir) |
| `settingsManager` | `SettingsManager.create(cwd, agentDir)` |
| `model` | Restored session model → settings default → first available |
| `thinkingLevel` | Restored session / settings / `DEFAULT_THINKING_LEVEL` (`"medium"`), then clamped to model capabilities |
| Default tools | Active tool name `ipython` when `tools` and `noTools` are unset |
| MCP | `McpManager` registered so user MCP providers and skill gates apply on the bare SDK path |

### Run the example

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

From an app that depends on the published package, import the same API and run your script with `tsx` or a built ESM entry.

## Expected first-run output

Output is provider- and model-dependent. Structure is stable:

1. **Streaming phase** — `message_update` events with `assistantMessageEvent.type === "text_delta"` write partial assistant text to stdout (no automatic newline after the stream).
2. **Completion** — `session.prompt()` resolves only after the accepted turn finishes (including tool rounds and retries).
3. **History dump** — `session.state.messages` (same history as `session.agent.state.messages` / `session.messages`) prints full message objects: user prompt, assistant message(s), and any tool-result messages if the model called `ipython`.

Typical successful terminal shape:

```text
<streamed assistant text about cwd contents>
{ role: 'user', content: '...', ... }
{ role: 'assistant', content: [...], ... }
# optional tool messages if tools ran
```

<Check>
Success signals: streaming text appears, `prompt()` resolves without throw, and `session.state.messages` contains at least one user and one assistant message. If `modelFallbackMessage` is set after create, fix credentials before treating a blank stream as success.
</Check>

### Failure modes on first run

| Symptom | Cause | Action |
|---------|--------|--------|
| `modelFallbackMessage` about no models | No credentialed model in registry/settings | Configure API key/OAuth under agent dir or env; see authentication docs |
| Auth / 401-style throw during stream | Invalid or missing key for the selected model | Update `auth.json` or `authStorage.setRuntimeApiKey(provider, key)` |
| Empty stdout, then dumped messages | No `text_delta` events (tool-only turn, or error path) | Inspect `session.state.messages` and subscribe to `tool_execution_*` / `agent_end` |
| Hang on tool | Default `ipython` tool needs a working kernel/runtime | Ensure IPython runtime prep for the environment, or restrict `tools` |

## Custom model wiring

Matches `packages/coding-agent/examples/sdk/02-custom-model.ts`.

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

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

// Built-in catalog lookup (does not require a key to exist)
const opus = getModel("anthropic", "claude-opus-4-5");
if (opus) {
  console.log(`Found model: ${opus.provider}/${opus.id}`);
}

// Custom / models.json entry
const customModel = modelRegistry.find("my-provider", "my-model");

// Only models with configured credentials
const available = await modelRegistry.getAvailable();
console.log(
  "Available models:",
  available.map((m) => `${m.provider}/${m.id}`),
);

if (available.length > 0) {
  const { session } = await createAgentSession({
    model: available[0],
    thinkingLevel: "medium", // off | minimal | low | medium | high | xhigh | max (clamped)
    authStorage,
    modelRegistry,
  });

  session.subscribe((event) => {
    if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
      process.stdout.write(event.assistantMessageEvent.delta);
    }
  });

  await session.prompt("Say hello in one sentence.");
  console.log();
}
```

### Model selection paths

| API | Role |
|-----|------|
| `getModel(provider, id)` from `@earendil-works/pi-ai` | Built-in model catalog entry |
| `modelRegistry.find(provider, id)` | Built-in + custom models from `models.json` |
| `await modelRegistry.getAvailable()` | Subset with valid API keys / OAuth |
| `createAgentSession({ model, thinkingLevel })` | Pin model and thinking for the session |

Thinking levels used by the session: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`. Values are clamped to what the selected model supports.

### Credential resolution (BYOK)

`AuthStorage` resolves keys in order:

1. Runtime overrides (`setRuntimeApiKey`) — not persisted  
2. Stored credentials in `auth.json` (API keys or OAuth)  
3. Provider environment variables  
4. Fallback resolvers for custom providers in `models.json`

Pass the same `authStorage` and `modelRegistry` into `createAgentSession` so stream auth matches model discovery.

## Custom system prompt

Matches `packages/coding-agent/examples/sdk/03-custom-prompt.ts`. Prompt text is owned by `DefaultResourceLoader`, not by a top-level `createAgentSession` string option.

### Replace the system prompt

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

const cwd = process.cwd();
const agentDir = getAgentDir();

const loader1 = new DefaultResourceLoader({
  cwd,
  agentDir,
  systemPromptOverride: () =>
    `You are a helpful assistant that speaks like a pirate.
Always end responses with "Arrr!"`,
  // Avoid appending APPEND_SYSTEM.md from agent/project dirs
  appendSystemPromptOverride: () => [],
});
await loader1.reload();

const { session: session1 } = await createAgentSession({
  resourceLoader: loader1,
  sessionManager: SessionManager.inMemory(),
});
```

### Append to the default prompt

```typescript
const loader2 = new DefaultResourceLoader({
  cwd,
  agentDir,
  appendSystemPromptOverride: (base) => [
    ...base,
    "## Additional Instructions\n- Always be concise\n- Use bullet points when listing things",
  ],
});
await loader2.reload();

const { session: session2 } = await createAgentSession({
  resourceLoader: loader2,
  sessionManager: SessionManager.inMemory(),
});
```

| Override | Behavior |
|----------|----------|
| `systemPromptOverride(base)` | Replace or transform the resolved base system prompt |
| `appendSystemPromptOverride(base)` | Replace or transform append segments (including discovered `APPEND_SYSTEM` sources) |
| `await loader.reload()` | **Required** before passing the loader into `createAgentSession` |
| `SessionManager.inMemory()` | Avoids writing a persistent session file while experimenting |

<Warning>
If you fully replace the system prompt but leave the default append discovery in place, project/user append files can still be merged. The replace example clears that with `appendSystemPromptOverride: () => []`.
</Warning>

## Core options used by minimal setups

<ParamField body="cwd" type="string">
Working directory for project-local discovery. Default: `process.cwd()`.
</ParamField>

<ParamField body="agentDir" type="string">
Global config directory. Default: `getAgentDir()` (`~/.prime/agent`).
</ParamField>

<ParamField body="authStorage" type="AuthStorage">
Credential store. Default: `AuthStorage.create(...)`.
</ParamField>

<ParamField body="modelRegistry" type="ModelRegistry">
Model catalog + availability. Default: `ModelRegistry.create(authStorage, ...)`.
</ParamField>

<ParamField body="model" type="Model">
Explicit model. Default: settings / first available / restored session model.
</ParamField>

<ParamField body="thinkingLevel" type="ThinkingLevel">
Default from settings or `"medium"`, then clamped to the model.
</ParamField>

<ParamField body="tools" type="string[]">
Allowlist of tool names. When omitted, default built-in is `ipython` unless `noTools` changes that.
</ParamField>

<ParamField body="resourceLoader" type="ResourceLoader">
Skills, extensions, prompts, themes, context. Default: `DefaultResourceLoader` with `reload()`.
</ParamField>

<ParamField body="sessionManager" type="SessionManager">
Persistence. Default: disk-backed under the session directory; use `SessionManager.inMemory()` for ephemeral runs.
</ParamField>

### Return value

<ResponseField name="session" type="AgentSession">
Live session: `prompt()`, `subscribe()`, model controls, `state` / `messages`, `dispose()` / `disposeAsync()`.
</ResponseField>

<ResponseField name="extensionsResult" type="LoadExtensionsResult">
Loaded extension metadata for UI or host wiring.
</ResponseField>

<ResponseField name="modelFallbackMessage" type="string | undefined">
Set when the preferred model could not be restored or no model is available.
</ResponseField>

## Event subscription (streaming)

Minimal scripts usually only handle `text_delta`. Full event surface for debugging first runs:

```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;
  }
});
```

`subscribe` returns an unsubscribe function. Prefer `await session.disposeAsync()` when the process should tear down IPython/kernel state cleanly.

## Prompt entry point

```typescript
await session.prompt(text, options?);
```

Useful `PromptOptions` for embedders:

| Option | Default / notes |
|--------|-----------------|
| `expandPromptTemplates` | `true` — expand file-based prompt templates |
| `images` | Optional image attachments |
| `streamingBehavior` | Required if calling `prompt` while already streaming: `"steer"` or `"followUp"` |
| `source` | Input source for extension handlers; defaults to interactive |

`prompt()` resolves after the accepted run completes. Preflight rejection and stream-time failures surface as thrown errors or message/event stream content depending on stage.

## Verification checklist

<Steps>
  <Step title="Install and import">
    Depend on `@earendil-works/pi-coding-agent` and import `createAgentSession` (plus `AuthStorage` / `ModelRegistry` when wiring credentials explicitly).
  </Step>
  <Step title="Confirm credentials">
    Run with a configured provider, or call `await modelRegistry.getAvailable()` and assert `length > 0` before creating the session.
  </Step>
  <Step title="Run minimal prompt">
    `cd packages/coding-agent && npx tsx examples/sdk/01-minimal.ts` (or your app script).
  </Step>
  <Step title="Observe stream + history">
    Expect stdout text deltas, resolved `prompt()`, and non-empty `session.state.messages`.
  </Step>
  <Step title="Optional: pin model and prompt">
    Port patterns from `02-custom-model.ts` and `03-custom-prompt.ts`; always `await loader.reload()` for resource overrides.
  </Step>
</Steps>

## Scope of this page

This page covers the minimal path: bootstrap, model pin, system-prompt overrides, run command, and first-run signals. Skills, tools, extensions, context files, session persistence strategies, and full-control composition are separate SDK pages.

## Next

<CardGroup>
  <Card title="Skills, tools, and extensions" href="/sdk-skills-tools-extensions">
    Load skills, register tools and extensions, context files, and prompt templates from the SDK.
  </Card>
  <Card title="Sessions and full control" href="/sdk-sessions-control">
    Session managers, settings injection, runtime hooks, and the full-control composition example.
  </Card>
  <Card title="Authentication and providers" href="/authentication-providers">
    Login, API keys, OAuth, multi-provider selection, and BYOK boundaries.
  </Card>
  <Card title="Settings and provider keys" href="/settings-providers">
    Provider registration, model selection, and dynamic provider updates.
  </Card>
  <Card title="Overview" href="/overview">
    CLI, SDK, and mode entry points for Prime Agent.
  </Card>
</CardGroup>
