
# Conversations

A Pi session can keep conversations in two ways:

- **Built-in history:** set `history: true`. The chat gains a history page and the session
  stores, lists, opens, and forgets conversations.
- **Host-owned storage:** call `session.save()` and keep each `PiSnapshot` in
  `localStorage`, `chrome.storage`, a database, or your backend.

Both approaches restore the transcript with the provider, model, thinking level, and title
that belong to it.

## Built-in history

```ts
import { browserStorage, createPiSession } from "agentak/pi";

const session = createPiSession({
  history: true,
  storage: browserStorage(),
});
```

The clock button in the header opens a full-height history page. It lists newest first,
marks the live conversation, and offers a forget button. Selecting a row replaces the
session state in place, so the widget is not unmounted and the host swaps nothing.

A session with history still opens on a **new conversation**. Stored conversations are one
click away in the history page. Pass `snapshot` when the host explicitly wants to open an
existing conversation at startup.

An empty chat also lists the three newest conversations below the greeting, with a row that
opens the history page for the rest. A row opens its conversation in place, exactly as the
history page does. The list disappears with the greeting when the first message is sent.

History behavior:

- The session writes after each settled turn and again after a generated title lands.
- **New conversation** stores the current one before clearing it.
- `dispose()` stores the last complete state.
- Empty conversations are never stored.
- Built-in history keeps 20 conversations and removes the oldest past the limit.
- If a store is full, it gives up older conversations until the new one fits.
- Forgetting the live conversation leaves a new empty conversation.
- The fork button under a user message stores the current conversation and opens a new one
  on the turns before that message, with the same provider, model, and thinking level. The
  chat puts the message itself back in the composer.
- The retry button beside it stays in the current conversation: the message runs again, and
  the answer it got before is replaced. The stored conversation follows, so a retry
  shortens what history holds.

The default store is page memory, so history disappears on reload. Use `browserStorage()`
for `localStorage`, or pass a `PiHistory` implementation:

```ts
const session = createPiSession({ history: myHistory });
```

A `PiHistory` lists lightweight entries separately from transcripts:

```ts
interface PiHistory {
  ready?: Promise<void>;
  list(): { id: string; title: string; updated: number }[];
  read(id: string): Promise<PiSnapshot | undefined>;
  keep(id: string, snapshot: PiSnapshot, title: string): void;
  forget(id: string): void;
}
```

`list()` answers from memory so the chat can redraw with it, while `read()` fetches one
transcript on demand. `ready` says when the list is in hand; `session.ready` waits on it,
so a host that mounts there gets a history page with its rows already in place.

## Save one snapshot yourself

```ts
import { createPiSession, readPiSnapshot, type PiSnapshot } from "agentak/pi";

function loadSnapshot(): PiSnapshot | undefined {
  const stored = localStorage.getItem("chat");
  if (!stored) return undefined;

  try {
    return readPiSnapshot(JSON.parse(stored));
  } catch {
    return undefined;
  }
}

const session = createPiSession({ snapshot: loadSnapshot() });

let saveTimer: ReturnType<typeof setTimeout> | undefined;
const unsubscribe = session.subscribe(() => {
  clearTimeout(saveTimer);
  saveTimer = setTimeout(() => {
    localStorage.setItem("chat", JSON.stringify(session.save()));
  }, 500);
});

// On cleanup:
// clearTimeout(saveTimer);
// unsubscribe();
// session.dispose();
```

A streaming answer emits many updates. Debouncing host-owned writes avoids serializing the
full conversation for every token. `save()` is safe during a turn and leaves out the
unfinished assistant message.

## Snapshot fields

| Field           | Type                         | Purpose                                       |
| --------------- | ---------------------------- | --------------------------------------------- |
| `version`       | `number`                     | Format version checked by `readPiSnapshot()`. |
| `messages`      | `AgentMessage[]`             | Transcript in Pi's format.                    |
| `provider`      | `string \| undefined`        | Provider used by this conversation.           |
| `model`         | `string \| undefined`        | Model restored after its catalog loads.       |
| `thinkingLevel` | `ThinkingLevel \| undefined` | Reasoning effort used by the model.           |
| `title`         | `string \| undefined`        | Generated title, when enabled.                |

The first-message title is derived from `messages` and does not need a separate field.
Conversation choices override the browser's defaults when that snapshot is opened.

## Read stored data safely

Pass stored JSON through `readPiSnapshot()`. It checks the version and confirms that
`messages` is an array. It drops fields this build does not know and returns `undefined`
for an unsupported shape, which lets the host start a new conversation instead of
throwing.

It is not a deep validator for untrusted data. Validate each message and optional field
when snapshots can come from another user or system.

Before Pi runs a restored transcript, Agentak repairs two incomplete endings:

- It cuts at the first tool call with no result because providers reject that sequence.
- It drops a failed assistant turn at the end.

Pending tool approvals are not restored. The user must approve a new call again.

## Switch conversations in place

```ts
session.restore(loadSnapshot(id));
```

`restore()` stops a running turn, then replaces messages, provider, model, thinking level,
and title inside the current session. Call it without an argument to start a new
conversation. If built-in history is enabled, the conversation being left is stored
first.

A host can instead create one session per conversation and replace the `session` prop.
Dispose the session it no longer uses. In-place restore is usually simpler because the
chat, composer draft, and framework island stay mounted.

## Titles

The first user message is the default title. Set `generateTitle` on `createPiSession()` or
`ChatPanel` to use one extra request after the first answer. A failed title request leaves
the derived title in place.

Read [Errors, usage, and titles](/agents/pi/runtime-behavior) for the complete title flow,
or [Custom agents](/agents/custom) to expose history from another runtime.
