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 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

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

FieldTypePurpose
versionnumberFormat version checked by readPiSnapshot().
messagesAgentMessage[]Transcript in Pi's format.
providerstring | undefinedProvider used by this conversation.
modelstring | undefinedModel restored after its catalog loads.
thinkingLevelThinkingLevel | undefinedReasoning effort used by the model.
titlestring | undefinedGenerated 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.