@@ -81,6 +81,7 @@ import {
8181 restoreModelLane,
8282 type TranscriptChange,
8383 type TranscriptChangeReason,
84+ type TranscriptLoadResult,
8485 type TranscriptRuntimeState,
8586 type TranscriptShadow,
8687 type TranscriptStorage,
@@ -277,6 +278,25 @@ export {
277278 __writeChatSnapshotProductionPathForTests,
278279} from "./chatSnapshotIo.js";
279280
281+ export {
282+ defaultStorage,
283+ memoryTranscriptStorage,
284+ reduceTranscriptChanges,
285+ snapshotTranscriptStorage,
286+ type LoadContextEvent,
287+ type MemoryTranscriptStorage,
288+ type TranscriptChange,
289+ type TranscriptChangeReason,
290+ type TranscriptChangeset,
291+ type TranscriptCursors,
292+ type TranscriptLoadOptions,
293+ type TranscriptLoadResult,
294+ type TranscriptScope,
295+ type TranscriptState,
296+ type TranscriptStorage,
297+ type TranscriptStorageContext,
298+ } from "./transcriptStorage.js";
299+
280300/**
281301 * Merge two `UIMessage[]` lists by `id`, with the second list winning on
282302 * collision. Used at run boot to combine the snapshot's persisted history
@@ -6140,6 +6160,33 @@ export type ChatAgentOptions<
61406160 event: HydrateMessagesEvent<inferSchemaOut<TClientDataSchema>, TUIMessage>
61416161 ) => TUIMessage[] | Promise<TUIMessage[]>;
61426162
6163+ /**
6164+ * Where the conversation is persisted. The runtime calls `save` after
6165+ * every turn, failed turn and history-changing action with the changes
6166+ * since the last save, and `load` once when a new run boots to continue
6167+ * the conversation.
6168+ *
6169+ * Defaults to `defaultStorage`, the platform's snapshot in object storage
6170+ * that the Sessions dashboard renders. Bring your own to write each change
6171+ * to your database; `memoryTranscriptStorage()` is the reference
6172+ * implementation and `runTranscriptStorageTests` from
6173+ * `@trigger.dev/sdk/ai/test` checks yours against the contract.
6174+ *
6175+ * A storage with `loadContext` also owns the model's context on every
6176+ * turn, which is what `hydrateMessages` did. The two cannot be combined.
6177+ *
6178+ * @example
6179+ * ```ts
6180+ * chat.agent({
6181+ * id: "my-chat",
6182+ * storage: myPostgresTranscriptStorage,
6183+ * run: async ({ messages, signal, streamText }) =>
6184+ * streamText({ model, messages, abortSignal: signal }),
6185+ * });
6186+ * ```
6187+ */
6188+ storage?: TranscriptStorage<inferSchemaOut<TClientDataSchema>>;
6189+
61436190 /**
61446191 * Called at the start of every turn, after message accumulation and `onChatStart` (turn 0),
61456192 * but before the `run` function executes.
@@ -6726,6 +6773,7 @@ function chatAgent<
67266773 onChatStart,
67276774 onValidateMessages,
67286775 hydrateMessages,
6776+ storage,
67296777 actionSchema,
67306778 onAction,
67316779 onTurnStart,
@@ -6755,12 +6803,17 @@ function chatAgent<
67556803 } = options;
67566804
67576805 if (hydrateMessages) {
6758- const storageAtDefinition = transcriptStorageOverride ?? defaultStorage;
6759- if (typeof storageAtDefinition.loadContext === "function") {
6806+ if (storage) {
6807+ throw new Error(
6808+ `chat.agent: "${options.id}" sets both \`hydrateMessages\` and \`storage\`. ` +
6809+ "`hydrateMessages` is deprecated and replaced by the storage: `save` receives every " +
6810+ "change and `loadContext` on the storage owns the model's context. Remove `hydrateMessages`."
6811+ );
6812+ }
6813+ if (typeof (transcriptStorageOverride ?? defaultStorage).loadContext === "function") {
67606814 throw new Error(
67616815 `chat.agent: "${options.id}" sets \`hydrateMessages\` and uses a transcript storage with ` +
6762- "`loadContext`. Both would own the model's context; keep one. `hydrateMessages` is " +
6763- "deprecated, so prefer `loadContext` on the storage."
6816+ "`loadContext`. Both would own the model's context; keep one."
67646817 );
67656818 }
67666819 warnHydrateMessagesDeprecatedOnce(options.id);
@@ -6915,7 +6968,10 @@ function chatAgent<
69156968 // collectively cost ~600ms on every first-message TTFC. Both reads
69166969 // swallow errors internally; the agent stays available either way.
69176970 const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
6918- const transcriptStorage = transcriptStorageOverride ?? defaultStorage;
6971+ const transcriptStorage: TranscriptStorage<unknown> =
6972+ (storage as TranscriptStorage<unknown> | undefined) ??
6973+ transcriptStorageOverride ??
6974+ defaultStorage;
69196975 const storageLoadContext = transcriptStorage.loadContext?.bind(transcriptStorage);
69206976 /**
69216977 * Who supplies the model's context each turn: the deprecated
@@ -12604,6 +12660,68 @@ async function mintPublicTokenWithOverride(args: {
1260412660 });
1260512661}
1260612662
12663+ export type CreateChatLoadTranscriptActionOptions = {
12664+ /**
12665+ * Scope the action to a specific API client configuration (secret key,
12666+ * base URL) instead of the process-wide one. The default storage reads
12667+ * through this client.
12668+ */
12669+ apiClient?: ApiClientConfiguration;
12670+ /** Page size when the caller passes none. */
12671+ limit?: number;
12672+ };
12673+
12674+ export type ChatLoadTranscriptParams<TClientData = unknown> = {
12675+ chatId: string;
12676+ clientData?: TClientData;
12677+ limit?: number;
12678+ before?: string;
12679+ };
12680+
12681+ /**
12682+ * Creates a server-side helper that reads a conversation from a transcript
12683+ * storage, for rendering history before the chat connects. Works the same
12684+ * for every storage, the platform default included, so the browser never
12685+ * reads a store directly and the secret key stays on the server.
12686+ *
12687+ * Wrap it in a Next.js server action (or any server-side handler), scope it
12688+ * to the authenticated user through `clientData`, and pass the result to
12689+ * `useLoadTranscript` in the browser.
12690+ *
12691+ * @example
12692+ * ```ts
12693+ * // actions.ts
12694+ * "use server";
12695+ * import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
12696+ *
12697+ * export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
12698+ * ```
12699+ */
12700+ function createChatLoadTranscriptAction<TClientData = unknown>(
12701+ storage: TranscriptStorage<TClientData>,
12702+ options?: CreateChatLoadTranscriptActionOptions
12703+ ): (params: ChatLoadTranscriptParams<TClientData>) => Promise<TranscriptLoadResult> {
12704+ return async (params) => {
12705+ if (!params.chatId) {
12706+ throw new Error("chat.createLoadTranscriptAction: params.chatId is required.");
12707+ }
12708+ if (options?.apiClient) {
12709+ const { apiClient, ...rest } = options;
12710+ return apiClientManager.runWithConfig(apiClient, () =>
12711+ createChatLoadTranscriptAction(storage, rest)(params)
12712+ );
12713+ }
12714+ const limit = params.limit ?? options?.limit;
12715+ return storage.load(
12716+ { chatId: params.chatId, clientData: params.clientData as TClientData },
12717+ {
12718+ ...(limit !== undefined ? { limit } : {}),
12719+ ...(params.before !== undefined ? { before: params.before } : {}),
12720+ }
12721+ );
12722+ };
12723+ }
12724+
1260712725export const chat = {
1260812726 /** Create a chat agent. See {@link chatAgent}. */
1260912727 agent: chatAgent,
@@ -12615,6 +12733,8 @@ export const chat = {
1261512733 withClientData,
1261612734 /** Create a server-side helper for starting (or resuming) a Session for a chatId. See {@link createChatStartSessionAction}. */
1261712735 createStartSessionAction: createChatStartSessionAction,
12736+ /** Returns a server-side helper that reads a conversation from a transcript storage. */
12737+ createLoadTranscriptAction: createChatLoadTranscriptAction,
1261812738 /** Pipe a stream to the chat transport. See {@link pipeChat}. */
1261912739 pipe: pipeChat,
1262012740 /** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */
0 commit comments