Conversations
Keep Pi conversations in built-in history or store snapshots yourself.
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 eachPiSnapshotinlocalStorage,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
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:
const session = createPiSession({ history: myHistory });A PiHistory lists lightweight entries separately from transcripts:
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
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
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 for the complete title flow, or Custom agents to expose history from another runtime.