# Agent sessions

> Session lifecycle, prompt queue, concurrent behavior, stats, and runtime services that own a conversation turn.

- 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/src/core/agent-session.ts`
- `packages/coding-agent/src/core/agent-session-services.ts`
- `packages/coding-agent/src/core/agent-session-runtime.ts`
- `packages/coding-agent/test/suite/agent-session-queue.test.ts`
- `packages/coding-agent/test/agent-session-concurrent.test.ts`
- `packages/coding-agent/examples/sdk/11-sessions.ts`

---

---
title: "Agent sessions"
description: "Session lifecycle, prompt queue, concurrent behavior, stats, and runtime services that own a conversation turn."
---

`AgentSession` is the shared turn owner for interactive, print/JSON, RPC, and SDK modes. It wraps an `@earendil-works/pi-agent-core` `Agent`, persists conversation entries through `SessionManager`, and exposes prompting, queueing, compaction hooks, model/tool control, and session stats. Modes add I/O on top; they do not own the agent loop.

## Ownership model

Three construction layers keep cwd-bound infrastructure separate from the conversation object and from session replacement:

| Layer | Type / factory | Owns |
| --- | --- | --- |
| Services | `AgentSessionServices` via `createAgentSessionServices()` | `cwd`, `agentDir`, `ModelRuntime`, `SettingsManager`, `ResourceLoader`, diagnostics |
| Session | `AgentSession` via `createAgentSession()` or `createAgentSessionFromServices()` | Agent state, event fan-out, prompt queue, tools, compaction, stats |
| Runtime | `AgentSessionRuntime` via `createAgentSessionRuntime()` | Current session + services; `/new`, resume, fork, import replacement |

`AgentSessionServices` is infrastructure only. Callers create services first, resolve model/tools against that cwd, then build the session. `AgentSessionRuntime` tears down the current session (abort → `session_shutdown` → dispose), creates the next runtime with the same factory, then rebinds host UI/subscriptions.

```mermaid
flowchart TB
  subgraph host["Host (CLI / RPC / SDK)"]
    UI["I/O and bindings"]
  end
  subgraph runtime["AgentSessionRuntime"]
    S["AgentSession"]
    SVC["AgentSessionServices"]
  end
  subgraph persist["Persistence"]
    SM["SessionManager JSONL"]
  end
  subgraph agentcore["pi-agent-core"]
    A["Agent"]
  end
  UI --> runtime
  SVC --> S
  S --> A
  S --> SM
  S -->|"steer / followUp queues"| A
```

## Creating a session

### SDK entry

`createAgentSession(options?)` builds or reuses loaders, constructs the `Agent` with settings-derived stream options and queue modes, restores messages when the manager already has history, and returns:

```ts
interface CreateAgentSessionResult {
  session: AgentSession;
  extensionsResult: LoadExtensionsResult;
  modelFallbackMessage?: string;
}
```

Important `CreateAgentSessionOptions` fields:

| Option | Default | Role |
| --- | --- | --- |
| `cwd` | `process.cwd()` or session manager cwd | Project-local discovery |
| `agentDir` | `~/.pi/agent` | Auth, models, global resources |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence target |
| `model` / `thinkingLevel` | Settings / first available | Initial model; restored from session when continuing |
| `tools` / `excludeTools` / `noTools` | Default active: `read`, `bash`, `edit`, `write` | Tool allow/deny surface |
| `resourceLoader` | `DefaultResourceLoader` | Skills, extensions, prompts, context files |
| `modelRuntime` | Created from `agentDir` auth/models paths | Provider auth and streaming |

### SessionManager factories

| Factory | Behavior |
| --- | --- |
| `SessionManager.create(cwd, sessionDir?)` | New persisted JSONL under the session directory |
| `SessionManager.continueRecent(cwd, sessionDir?)` | Most recent session for the project, or create new |
| `SessionManager.open(path, sessionDir?, cwdOverride?)` | Open an existing JSONL file |
| `SessionManager.inMemory(cwd?)` | No file persistence |
| `SessionManager.list(cwd, sessionDir?)` | List project sessions |
| `SessionManager.forkFrom(sourcePath, targetCwd, …)` | Copy history into a new session in another project cwd |

Default storage layout (when `sessionDir` is omitted):

```text
~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<uuid>.jsonl
```

Override with `SessionManager.create(cwd, customDir)`, CLI `--session-dir`, or the session-directory env documented in the CLI help.

### CLI session controls

| Flag | Effect |
| --- | --- |
| `-c` / `--continue` | Continue most recent session |
| `-r` / `--resume` | Interactive session picker |
| `--session <path\|id>` | Open file or partial UUID |
| `--session-id <id>` | Exact project session id (create if missing) |
| `--fork <path\|id>` | Fork into a new session file |
| `--session-dir <dir>` | Storage/lookup directory |
| `--no-session` | Ephemeral; do not save |
| `-n` / `--name <name>` | Display name at startup |

Interactive slash commands that act on the same surface: `/session`, `/name`, `/new`, `/resume`, `/fork`, `/clone`, `/tree`, `/compact`, `/export`, `/share`.

### Minimal SDK patterns

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

// Ephemeral
const { session: mem } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
});
mem.dispose();

// New file-backed session
const { session } = await createAgentSession({
  sessionManager: SessionManager.create(process.cwd()),
});
console.log(session.sessionId, session.sessionFile);

// Continue recent (may return modelFallbackMessage if saved model is unavailable)
const { session: continued, modelFallbackMessage } = await createAgentSession({
  sessionManager: SessionManager.continueRecent(process.cwd()),
});
```

Always call `session.dispose()` (or `runtime.dispose()`) when finished.

## Prompt turn lifecycle

`prompt(text, options?)` is the primary entry for a user turn.

### Preflight and expansion order

1. **Extension slash commands** (`/command …` registered via `pi.registerCommand`) run immediately, including while streaming, and do not start an agent prompt.
2. Reject if **manual compaction** is in progress (`_compactionAbortController` set).
3. Emit extension **`input`** handlers (`source` defaults to `"interactive"`; pass streaming behavior when already streaming). Handlers may `handled` or `transform` text/images.
4. Expand `/skill:name` and file-based prompt templates when `expandPromptTemplates` is true (default).
5. If `isStreaming`, queue with `streamingBehavior: "steer" | "followUp"` (required); otherwise start a new run.
6. When idle: validate model and provider auth, optionally auto-compact from the last assistant message, inject `nextTurn` asides, emit `before_agent_start`, then run the agent.

### Run and settle

```ts
// Conceptual flow inside AgentSession
_isAgentRunActive = true
await agent.prompt(messages)
while (await _handlePostAgentRun()) {
  await agent.continue()  // retry, compaction recovery, or extension-queued messages
}
// finally:
_systemPromptOverride = undefined
flush pending bash messages
await _emitAgentSettled()  // agent_settled + resolve waitForIdle
```

`_handlePostAgentRun()` may continue the loop for:

- retryable provider errors (`auto_retry_*` events),
- auto-compaction after threshold/overflow,
- messages queued by `agent_end` extension handlers (`agent.hasQueuedMessages()`).

### Idle / streaming flags

| Accessor | Meaning |
| --- | --- |
| `isStreaming` | An agent run or post-run continuation is active (`_isAgentRunActive`) |
| `isIdle` | Negation of `isStreaming` |
| `isCompacting` | Manual compaction, auto-compaction, or branch summary in progress |
| `waitForIdle()` | Resolves after `agent_settled` clears the active run |
| `abort()` | Aborts retry + agent, then `waitForIdle()` |

## Prompt queue and concurrent behavior

Only one agent run owns the loop. A second bare `prompt()` while streaming throws:

```text
Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.
```

### Queue channels

| Channel | API | When delivered |
| --- | --- | --- |
| **Steer** | `steer(text, images?)` or `prompt(..., { streamingBehavior: "steer" })` | After the current assistant tool calls finish, before the next LLM call |
| **Follow-up** | `followUp(text, images?)` or `prompt(..., { streamingBehavior: "followUp" })` | After the run has no more tool calls or steering messages |
| **Next turn** | `sendCustomMessage(..., { deliverAs: "nextTurn" })` | Injected as context with the *next* idle user prompt |

Extension helpers:

- `sendUserMessage(content, { deliverAs })` → `prompt` with `expandPromptTemplates: false`, `source: "extension"`.
- `sendCustomMessage` with `deliverAs: "steer" | "followUp"` while streaming, or `triggerTurn` when idle.

### Queue modes

Settings keys (defaults `"one-at-a-time"`):

| Setting | Values | Effect |
| --- | --- | --- |
| `steeringMode` | `"all"` \| `"one-at-a-time"` | Batch all pending steers into one LLM turn, or drain one per turn |
| `followUpMode` | `"all"` \| `"one-at-a-time"` | Same for follow-ups |

`AgentSession.setSteeringMode` / `setFollowUpMode` update the agent and persist via `SettingsManager`. Modes are applied when the underlying `Agent` is constructed from settings.

### Queue inspection

| API | Returns |
| --- | --- |
| `pendingMessageCount` | Steer + follow-up length |
| `getSteeringMessages()` / `getFollowUpMessages()` | Pending text snapshots |
| `clearQueue()` | Clears both queues and agent queues; returns `{ steering, followUp }` |

UI/extensions observe `queue_update` events with current `steering` and `followUp` arrays. When a queued user message starts, it is removed from the matching local list before fan-out so listeners see the updated queue.

### Hard constraints

| Situation | Behavior |
| --- | --- |
| `prompt` while streaming without `streamingBehavior` | Throws concurrent-processing error |
| `prompt` during manual compaction | Throws: wait for compaction to finish |
| Extension command via `steer` / `followUp` | Throws: extension commands cannot be queued; use `prompt` when idle (or they already run immediately via `prompt`) |
| Extension command via idle `prompt` | Executes immediately; no user message written |

## Events

Subscribe with `session.subscribe(listener)`; returns an unsubscribe function. Session persistence runs on internal agent event handling (for example `message_end` → session append) before or alongside listener delivery.

Session-specific event types beyond core agent events include:

| Event | Notes |
| --- | --- |
| `agent_settled` | Run fully finished; idle waiters resolve after this |
| `queue_update` | Pending steer/follow-up text lists |
| `compaction_start` / `compaction_end` | Manual, threshold, or overflow |
| `auto_retry_start` / `auto_retry_end` | Provider retry loop |
| `session_info_changed` | Display name change |
| `thinking_level_changed` | Thinking level updates |
| `entry_appended` | Raw session entry append |
| `bash_execution_update` | Streaming bash output |
| `agent_end` (session form) | Includes `willRetry` |

`agent_settled` is also emitted to extension handlers, so host code and extensions can synchronize on turn completion.

## Session stats

`getSessionStats(): SessionStats` walks **all** session entries (including compacted history), so totals reflect billed work across the whole file, not only the active LLM context.

```ts
interface SessionStats {
  sessionFile: string | undefined;
  sessionId: string;
  userMessages: number;
  assistantMessages: number;
  toolCalls: number;
  toolResults: number;
  totalMessages: number;
  tokens: {
    input: number;
    output: number;
    cacheRead: number;
    cacheWrite: number;
    total: number;
  };
  cost: number;
  contextUsage?: ContextUsage;
}
```

`getContextUsage()` estimates current-branch context against the model `contextWindow`. After compaction, if no successful post-compaction assistant usage exists yet, it returns `{ tokens: null, contextWindow, percent: null }` until the next LLM response.

Interactive `/session` surfaces the same identity, counts, tokens, and cost fields.

## Display name and identity

| Accessor / API | Role |
| --- | --- |
| `sessionId` | Stable session UUID from the manager |
| `sessionFile` | JSONL path, or `undefined` when not persisted |
| `sessionName` | Optional display name |
| `setSessionName(name)` | Appends session-info entry; emits `session_info_changed` |

## Runtime replacement

Use `AgentSessionRuntime` when the host must replace the active session without rebuilding the whole process.

| Method | Shutdown reason | Notes |
| --- | --- | --- |
| `newSession({ parentSession?, setup?, withSession? })` | `"new"` | New file if persisted, else new in-memory session |
| `switchSession(path, { cwdOverride?, withSession?, … })` | `"resume"` | Opens target; recreates services for session cwd |
| `fork(entryId, { position?, withSession? })` | `"fork"` | `"before"` (default, user entry) or `"at"` leaf; new branched file when persisted |
| `importFromJsonl(path, cwdOverride?)` | `"resume"` | Copies into session dir when needed; throws `SessionImportFileNotFoundError` |
| `dispose()` | `"quit"` | Final shutdown |

Replacement sequence (all switch/new/fork/import paths):

1. Optional `session_before_switch` / `session_before_fork` (cancellable).
2. `session.abort()` so the aborted turn (including tool results) is persisted.
3. `session_shutdown` extension event.
4. Synchronous `beforeSessionInvalidate` (host UI detach).
5. `session.dispose()` (stales extension contexts).
6. `createRuntime(...)` and apply.
7. Optional `rebindSession` + `withSession(replacedCtx)`.

After replacement, rebind subscriptions and `bindExtensions` to `runtime.session`. Captured extension `ctx` / `pi` from the previous session is invalid; post-replacement work belongs in `withSession`.

```ts
import {
  createAgentSessionRuntime,
  createAgentSessionServices,
  createAgentSessionFromServices,
  SessionManager,
  getAgentDir,
} from "@earendil-works/pi-coding-agent";

const createRuntime = 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()),
});

let unsub = runtime.session.subscribe((e) => {
  if (e.type === "agent_settled") console.log("turn idle");
});

await runtime.newSession();
unsub();
unsub = runtime.session.subscribe(/* rebind */);
await runtime.dispose();
```

## Diagnostics

`createAgentSessionServices` and runtime creation accumulate non-fatal `AgentSessionRuntimeDiagnostic` values (`info` | `warning` | `error`) instead of exiting. Typical sources: extension provider registration failures, unknown extension CLI flags, flags that require a value. The host decides whether to print or abort startup.

## Dispose

`AgentSession.dispose()`:

- aborts retry, compaction, branch summary, bash, and the agent (errors from abort hooks are swallowed),
- invalidates the extension runner with a stale-context message,
- disconnects agent subscriptions and clears listeners,
- runs `cleanupSessionResources(sessionId)`.

## Failure modes

| Symptom | Likely cause | Mitigation |
| --- | --- | --- |
| Concurrent prompt error | Second `prompt` while streaming without queue mode | Pass `streamingBehavior`, or call `steer` / `followUp` |
| Compaction prompt error | User/RPC prompt during manual compact | Await compaction / `compaction_end` |
| Extension command queue error | `/cmd` passed to `steer`/`followUp` | Use idle `prompt` (commands already run immediately) |
| Auth error on idle prompt | Missing API key or expired OAuth | Configure credentials or `/login <provider>` |
| No model selected | No model resolved at create or restore | Pass `model` or fix settings/auth so a model is available |
| `modelFallbackMessage` set | Continued session model unavailable | Session still opens; using fallback model from resolver |
| Stale extension context after `/new` | Using old `ctx` after replacement | Use `withSession` / rebind to `runtime.session` |
| Fork/clone before first save | Persisted session file not yet written | Wait for first assistant response before forking |
| Import path missing | `importFromJsonl` path does not exist | Catch `SessionImportFileNotFoundError` |

## Verification signals

- After a turn: `agent_settled` fires and `isIdle === true`.
- Queue: `queue_update` lengths match `getSteeringMessages` / `getFollowUpMessages`; `pendingMessageCount` decreases as messages start.
- Stats: `getSessionStats().sessionId` matches `session.sessionId`; token totals include pre-compaction assistant usage when present in the file.
- Replacement: `runtime.session` identity changes; previous `sessionFile` differs after `newSession` when persisted.
- Ephemeral: `SessionManager.inMemory()` → `session.sessionFile === undefined`.

## Related pages

<CardGroup>
  <Card title="Session runtime" href="/session-runtime">
    Embedding with AgentSessionRuntime, lifecycle events, and host rebind patterns without the TUI.
  </Card>
  <Card title="Branching and session trees" href="/branching-and-tree">
    In-file tree navigation, fork/clone, branch summaries, and cancel-during-compact interactions.
  </Card>
  <Card title="Context compaction" href="/compaction">
    Auto/manual compaction triggers, overflow recovery, and interaction with in-flight prompts.
  </Card>
  <Card title="SDK" href="/sdk">
    Package main export, createAgentSession options, and embedder control hooks.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste session and runtime recipes, including sessions and full-control setups.
  </Card>
  <Card title="RPC mode" href="/rpc-mode">
    Process integration constraints for prompts during compaction and JSON stream behavior.
  </Card>
  <Card title="Run modes" href="/run-modes">
    How interactive, print/JSON, RPC, and SDK modes attach I/O to the same AgentSession.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Retry/network failures, SIGTERM cleanup, and session settlement issues.
  </Card>
</CardGroup>
