@pai/storage
ThreadStore contract. Apps pick Mongo or in-memory.
ThreadStore is the durable thread provider. You do not implement this unless you are writing a storage adapter.
| Callable | When |
|---|---|
createMongoThreadStore() | Production |
createInMemoryThreadStore() | Tests, first app (createPai default) |
Pass the store as createPai({ storage }). Call initialize() on Mongo at boot.
Listing a thread's attachments
listThreadAttachments() returns the files users sent in one thread without paging the transcript. Each item is the stored attachment payload (fileId, mediaType, optional filename, byteSize, metadata) plus the messageId that carried it.
// Every file sent in the thread.
const { items } = await storage.listThreadAttachments({ scopeKey, threadId });
// Or a page at a time.
const page = await storage.listThreadAttachments({
scopeKey,
threadId,
limit: 20, // 1–100 attachments
pageToken, // from the previous page's nextPageToken
});- Without
limit, every match (afterpageToken, if you pass one) comes back in one result with nonextPageToken. - Only attachments on user messages that are visible in the transcript. Assistant files and hidden messages are excluded.
- Newest message first; attachments within one message keep their order. A file sent twice appears once per message.
- A page can end partway through a message;
nextPageTokenresumes at its next attachment. Tokens are bound to the scope and thread, and reusing one elsewhere throwsThreadStorePageTokenError. - A missing thread returns an empty page.
Listing a thread's tool calls
listThreadToolCalls() returns one tool's calls in one thread without paging the transcript. Each item is the stored native tool part (the AI SDK ToolUIPart shape: type: "tool-<name>", toolCallId, state, input, output or errorText) plus the messageId that carried it.
// Every call of one tool in the thread.
const { items } = await storage.listThreadToolCalls({
scopeKey,
threadId,
toolName: "publish_file",
});
// A preliminary output is still streaming, so it is not a finished call.
const done = items.filter(
({ part }) => part.state === "output-available" && part.preliminary !== true,
);
// Or a page at a time: add `limit` (1–100 tool calls) and pass back `pageToken`.- Without
limit, every match (afterpageToken, if you pass one) comes back in one result with nonextPageToken. Each item carries the tool's whole output, so pass alimitfor a tool with large outputs on a long thread. toolNameis the key the agent'stoolsregisters the tool under, which names its stored parts; it is not the tool'sidwhere the two differ.- Only
tool-<toolName>parts on assistant messages that are visible in the transcript.dynamic-toolparts are not matched; PAI stores a registered tool's calls as static parts. - Every state is returned; filter on
part.stateyourself. - Newest message first; calls within one message keep their order.
- A page can end partway through a message;
nextPageTokenresumes at its next call. Tokens are bound to the scope, thread, and tool name, and reusing one elsewhere throwsThreadStorePageTokenError. - A missing thread returns an empty page.
Both listings read live data, not a snapshot: a message added after one page is never returned in part by the next, while parts appended to the boundary message are. A store may reject a token whose boundary message has since been deleted (for example by a regeneration) with ThreadStorePageTokenError; list again from the start.
Custom ThreadStore adapters must implement both listings; createThreadStoreConformanceSuite from @pai/storage/test covers their behaviour.
Provider utilities
@pai/storage/thread-store exports canonical record parsers, commit-input validation, helpers for applying write sets and validating record relationships, and transcriptPartsOf with the threadAttachmentListing and threadToolCallListing() rules that decide which stored parts the two listings return. Storage providers own their read queries, filtering, ordering, and pagination-token formats.