# Sessions and full control

> SDK session management, settings injection, session runtime hooks, and the full-control composition example.

- 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/11-sessions.ts`
- `packages/coding-agent/examples/sdk/12-full-control.ts`
- `packages/coding-agent/examples/sdk/13-session-runtime.ts`
- `packages/coding-agent/examples/sdk/10-settings.ts`
- `packages/coding-agent/src/core/agent-session-runtime.ts`

---

---
title: "Sessions and full control"
description: "SDK session management, settings injection, session runtime hooks, and the full-control composition example."
---

The SDK surface for programmatic agents is `createAgentSession()` for a single session, and `createAgentSessionRuntime()` when the host must replace the active session (`newSession`, `switchSession`, `fork`, `importFromJsonl`). Session persistence is owned by `SessionManager`; settings by `SettingsManager`; credentials and models by `AuthStorage` / `ModelRegistry`; discovery by `ResourceLoader`. Package imports use `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` (API identifiers for the published packages).

## Two entry points

| Entry | Use when | Returns |
|-------|----------|---------|
| `createAgentSession(options?)` | One session for the process lifetime | `{ session, extensionsResult, modelFallbackMessage? }` |
| `createAgentSessionRuntime(factory, options)` | Host needs session replacement without restarting | `AgentSessionRuntime` with `.session`, `.services`, `.diagnostics` |

`createAgentSession` defaults:

| Option | Default |
|--------|---------|
| `cwd` | `process.cwd()` (or `sessionManager.getCwd()`) |
| `agentDir` | `getAgentDir()` → `~/.prime/agent` (overridable via env) |
| `authStorage` | `AuthStorage.create(agentDir/auth.json)` |
| `modelRegistry` | `ModelRegistry.create(authStorage, agentDir/models.json)` |
| `sessionManager` | `SessionManager.create(cwd)` |
| `settingsManager` | `SettingsManager.create(cwd, agentDir)` |
| `resourceLoader` | `DefaultResourceLoader` + `reload()` |
| `tools` | built-in `ipython` unless `tools` / `noTools` override |

For replaceable sessions, split **cwd-bound services** from the session:

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

`createAgentSessionServices` rebuilds auth, settings, model registry, resource loader, and MCP manager for the effective cwd. `createAgentSessionFromServices` builds the `AgentSession` against those services.

## SessionManager factories

`SessionManager` controls whether history is persisted and which file is active.

| Factory | Persistence | Behavior |
|---------|-------------|----------|
| `SessionManager.inMemory(cwd?)` | None | Ephemeral; `session.sessionFile` is unset |
| `SessionManager.create(cwd, sessionDir?)` | New file under session dir | Fresh session; optional custom `sessionDir` (no cwd encoding required) |
| `SessionManager.continueRecent(cwd, sessionDir?)` | Most recent for cwd, else new | Resume path for “continue last” |
| `SessionManager.open(path, sessionDir?, cwdOverride?)` | Existing path | Load header cwd (or override) |
| `SessionManager.openAsync(path, ...)` | Same as `open` | Non-blocking parse for daemon/large files |
| `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` | New file in target | Copy history into another project cwd |

List sessions for a cwd:

```typescript
const sessions = await SessionManager.list(process.cwd());
// SessionInfo: path, id, cwd, name?, state?, parentSessionPath?,
// rlmDepth, created, modified, messageCount, firstMessage, ...
```

`SessionManager.listAll()` scans the global sessions root. Cross-project fork uses `forkFrom`.

### Example: persistence modes

From `packages/coding-agent/examples/sdk/11-sessions.ts`:

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

// In-memory
const { session: inMemory } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
});

// New persistent file
const { session: newSession } = await createAgentSession({
  sessionManager: SessionManager.create(process.cwd()),
});

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

// Open listed session
const sessions = await SessionManager.list(process.cwd());
if (sessions.length > 0) {
  const { session: opened } = await createAgentSession({
    sessionManager: SessionManager.open(sessions[0].path),
  });
}
```

<Note>
Restored sessions may surface `modelFallbackMessage` when the saved provider/model is unavailable or unauthenticated. Treat it as a host warning, not a hard failure.
</Note>

## Settings injection

`SettingsManager` merges **global** (`agentDir`) and **project** (cwd) settings, then optional runtime overrides.

| Factory | I/O | Use |
|---------|-----|-----|
| `SettingsManager.create(cwd, agentDir?)` | File-backed | Production / CLI-aligned |
| `SettingsManager.inMemory(partial?)` | None | Tests and hermetic hosts |
| `SettingsManager.fromStorage(storage)` | Custom backend | Alternate storage |

Common methods:

| Method | Effect |
|--------|--------|
| `applyOverrides(partial)` | Deep-merge into in-memory effective settings (not automatically a durable write of every key) |
| `setDefaultThinkingLevel(level)` | Updates memory and queues persistence |
| `flush()` | Awaits the write queue (durability boundary) |
| `drainErrors(scope?)` | Returns `{ scope, error }[]` then clears |
| `getGlobalSettings()` / `getProjectSettings()` | Cloned snapshots |
| `reload()` | Re-read storage; clears modification tracking |

Settings keys used in SDK examples include:

| Key | Nested fields (examples) | Defaults (when unset) |
|-----|--------------------------|------------------------|
| `compaction` | `enabled`, `reserveTokens`, `keepRecentTokens`, `agentCallable` | compaction enabled by default |
| `retry` | `enabled`, `maxRetries`, `baseDelayMs`, `provider.*` | retry enabled; `maxRetries` 3; `baseDelayMs` 2000 |
| `defaultThinkingLevel` | — | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "max"` |

### Example: override + flush

From `packages/coding-agent/examples/sdk/10-settings.ts`:

```typescript
const settingsManager = SettingsManager.create(cwd);
settingsManager.applyOverrides({
  compaction: { enabled: false },
  retry: { enabled: true, maxRetries: 5, baseDelayMs: 1000 },
});

await createAgentSession({
  settingsManager,
  sessionManager: SessionManager.inMemory(),
});

settingsManager.setDefaultThinkingLevel("low");
await settingsManager.flush();

const settingsErrors = settingsManager.drainErrors();
for (const { scope, error } of settingsErrors) {
  console.warn(`Warning (${scope} settings): ${error.message}`);
}
```

<Warning>
Setters update memory immediately and queue writes. Call `flush()` before process exit when durability matters. Surface `drainErrors()` at the app layer; load failures do not throw from constructors.
</Warning>

## Full-control composition

Full control means **no filesystem discovery**: supply auth, models, settings, tools, system prompt, and a custom `ResourceLoader` so extensions, skills, prompts, themes, and Agents files are empty or explicit.

From `packages/coding-agent/examples/sdk/12-full-control.ts`:

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

const authStorage = AuthStorage.create("/tmp/my-agent/auth.json");
if (process.env.MY_ANTHROPIC_KEY) {
  authStorage.setRuntimeApiKey("anthropic", process.env.MY_ANTHROPIC_KEY); // not persisted
}

const modelRegistry = ModelRegistry.inMemory(authStorage);
const model = getModel("anthropic", "claude-sonnet-5");
if (!model) throw new Error("Model not found");

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

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.\nAvailable: ipython. Be concise.`,
  getAppendSystemPrompt: () => [],
  extendResources: () => {},
  reload: async () => {},
};

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

`ResourceLoader` contract:

| Method | Role |
|--------|------|
| `getExtensions()` | Extensions + `createExtensionRuntime()` |
| `getSkills()` / `getPrompts()` / `getThemes()` | Discovered resources + diagnostics |
| `getAgentsFiles()` | Context files (e.g. AGENTS.md) |
| `getSystemPrompt()` / `getAppendSystemPrompt()` | System prompt text |
| `extendResources(paths)` | Runtime path injection |
| `reload()` | Refresh after disk changes |

Auth notes for BYOK hosts:

- `AuthStorage.create(path?)` — file-backed credentials (default `~/.prime/agent/auth.json`).
- `AuthStorage.inMemory(data?)` — no disk.
- `setRuntimeApiKey(provider, key)` — process-local override; **not** written to disk.
- Prefer explicit provider keys from env or your secret store; do not hardcode a hosted gateway.

## Session runtime replacement

`AgentSessionRuntime` owns the current `AgentSession` plus cwd-bound `AgentSessionServices`. Replacement methods tear down the current runtime, create the next one via the stored factory, then rebind. If creation fails after teardown starts, the error propagates; hosts own user-facing recovery.

### Runtime methods

| Method | Shutdown reason | Start reason | Notes |
|--------|-----------------|--------------|-------|
| `newSession({ parentSession?, setup?, withSession? })` | `new` | `new` | Optional parent linkage; `setup` mutates the new manager before finish |
| `switchSession(path, { cwdOverride?, withSession? })` | `resume` | `resume` | Opens path; asserts session cwd exists |
| `fork(entryId, { position?, withSession? })` | `fork` | `fork` | `position`: `"before"` (default) or `"at"`; may return `selectedText` |
| `importFromJsonl(inputPath, cwdOverride?)` | `resume` | `resume` | Copies into session dir if needed; throws `SessionImportFileNotFoundError` if missing |
| `dispose()` | `quit` | — | Idempotent; flushes traces, disposes subagent runtimes, releases lease |

Return shape for switch/new/import: `{ cancelled: boolean }`. Fork also returns `{ selectedText?: string }`.

Cancellation: extensions can cancel via `session_before_switch` or `session_before_fork` (`result.cancel === true`).

### Lifecycle events

| Event | When | Cancellable |
|-------|------|-------------|
| `session_before_switch` | Before new/resume | Yes |
| `session_before_fork` | Before fork | Yes |
| `session_shutdown` | Before invalidating current session | No |
| `session_start` | After new session is applied | No |

`session_start.reason`: `"startup" | "reload" | "new" | "resume" | "fork"`.  
`session_shutdown.reason`: `"quit" | "reload" | "new" | "resume" | "fork"`.

Replacement also:

1. Emits `session_shutdown` to the extension runner  
2. Flushes agent trace upload (best-effort on teardown)  
3. Runs optional `setBeforeSessionInvalidate` (sync UI teardown)  
4. Awaits `session.disposeAsync()` (kernel snapshot flush)  
5. Disposes hosted RLM subagent runtimes  
6. Applies the new session; commits session lease  
7. Calls `setRebindSession` / `onSessionReplaced` listeners / `withSession`

Metadata on the runtime (`AgentSessionRuntimeMetadata`) includes `kind: "top-level" | "subagent"`, parent ids, RLM child id, spawn prompt, and `rehydratedCompleted`.

### Required rebind pattern

After any replacement, **do not keep stale subscriptions** against the old `AgentSession`. Rebind to `runtime.session`:

```typescript
// packages/coding-agent/examples/sdk/13-session-runtime.ts
let unsubscribe: (() => void) | undefined;

async function bindSession() {
  unsubscribe?.();
  const session = runtime.session;
  await session.bindExtensions({});
  unsubscribe = session.subscribe((event) => {
    if (event.type === "session_action_update") {
      console.log(
        "Queued:",
        event.actions.steering.length + event.actions.followUps.length,
      );
    }
  });
  return session;
}

let session = await bindSession();
await runtime.newSession();
session = await bindSession();

if (originalSessionFile) {
  await runtime.switchSession(originalSessionFile);
  session = await bindSession();
}

unsubscribe?.();
await runtime.dispose();
```

<Tip>
Hosts can also use `runtime.setRebindSession(fn)` or `runtime.onSessionReplaced(listener)` so every replacement path rebinds automatically.
</Tip>

### Architecture

```mermaid
flowchart TB
  subgraph Host["Host process"]
    Factory["CreateAgentSessionRuntimeFactory"]
    RT["AgentSessionRuntime"]
  end

  subgraph Services["AgentSessionServices cwd-bound"]
    Auth["AuthStorage"]
    SM["SettingsManager"]
    MR["ModelRegistry"]
    RL["ResourceLoader"]
    MCP["McpManager"]
  end

  subgraph Session["Active AgentSession"]
    SessMgr["SessionManager"]
    Ext["ExtensionRunner"]
    Sub["RLM subagent runtimes"]
  end

  Factory --> RT
  RT --> Services
  RT --> Session
  Factory -->|"new / switch / fork / import"| Services
  RT -->|"teardown + apply"| Session
```

## Session events for hosts

Subscribe on `AgentSession` (not only runtime):

```typescript
session.subscribe((event) => {
  switch (event.type) {
    case "message_update":
      if (event.assistantMessageEvent.type === "text_delta") {
        process.stdout.write(event.assistantMessageEvent.delta);
      }
      break;
    case "session_action_update":
      // steering + followUps queues
      break;
    case "tool_execution_start":
    case "tool_execution_end":
    case "agent_end":
      break;
  }
});

await session.prompt("List files in the current directory.");
```

## Run the examples

From the monorepo package:

```bash
cd packages/coding-agent
npx tsx examples/sdk/10-settings.ts
npx tsx examples/sdk/11-sessions.ts
npx tsx examples/sdk/12-full-control.ts
npx tsx examples/sdk/13-session-runtime.ts
```

| File | Focus |
|------|--------|
| `10-settings.ts` | Disk + in-memory settings, `applyOverrides`, `flush`, `drainErrors` |
| `11-sessions.ts` | `inMemory` / `create` / `continueRecent` / `list` / `open` |
| `12-full-control.ts` | Explicit auth, model, empty discovery loader, tool allowlist |
| `13-session-runtime.ts` | Factory, `newSession`, `switchSession`, rebind, `dispose` |

## Constraints and failure modes

| Case | Behavior |
|------|----------|
| Missing import path | `importFromJsonl` throws `SessionImportFileNotFoundError` |
| Unresolvable session cwd | `assertSessionCwdExists` fails on open/switch/import |
| Invalid fork entry | `Error("Invalid entry ID for forking")` |
| Extension cancels switch/fork | Method returns `{ cancelled: true }`; session unchanged |
| Session creation fails mid-replace | Error propagates; uncommitted lease released |
| No models configured | `modelFallbackMessage` may describe missing models; clears once `session.model` is set |
| Settings parse errors | Collected via `drainErrors()`; empty settings used for that scope |
| Concurrent writers | Session leases (`acquireSessionLease`) protect replacement paths |

## Verification checklist

<Check>
After `SessionManager.create`, `session.sessionFile` is a real path; after `inMemory`, it is unset.
</Check>
<Check>
After `runtime.newSession()`, `runtime.session` is a new object; re-subscribe and `bindExtensions` again.
</Check>
<Check>
Full-control loaders return empty skills/extensions; only listed `tools` are available.
</Check>
<Check>
`settingsManager.flush()` completes without errors, or `drainErrors()` reports scoped failures.
</Check>
<Check>
`await runtime.dispose()` ends cleanly and is safe to call twice.
</Check>

## Related pages

<CardGroup cols={2}>
  <Card title="Minimal SDK agent" href="/sdk-minimal">
    Bootstrap `createAgentSession` with defaults and first prompt.
  </Card>
  <Card title="Skills, tools, and extensions" href="/sdk-skills-tools-extensions">
    Resource loaders, custom tools, and extension factories.
  </Card>
  <Card title="Session configuration reference" href="/session-configuration">
    Full settings keys, defaults, and reload behavior.
  </Card>
  <Card title="Sessions and runtime" href="/sessions-runtime">
    Lifecycle, queueing, tree navigation, session-scoped vs durable state.
  </Card>
  <Card title="Settings and provider keys" href="/settings-providers">
    Provider registration, OAuth, and 401 recovery.
  </Card>
  <Card title="Agent connection modes" href="/agent-connection">
    Daemon, in-process, and snapshot connection paths.
  </Card>
</CardGroup>
