@@ -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
@@ -6154,6 +6174,33 @@ export type ChatAgentOptions<
61546174 event: HydrateMessagesEvent<inferSchemaOut<TClientDataSchema>, TUIMessage>
61556175 ) => TUIMessage[] | Promise<TUIMessage[]>;
61566176
6177+ /**
6178+ * Where the conversation is persisted. The runtime calls `save` after
6179+ * every turn, failed turn and history-changing action with the changes
6180+ * since the last save, and `load` once when a new run boots to continue
6181+ * the conversation.
6182+ *
6183+ * Defaults to `defaultStorage`, the platform's snapshot in object storage
6184+ * that the Sessions dashboard renders. Bring your own to write each change
6185+ * to your database; `memoryTranscriptStorage()` is the reference
6186+ * implementation and `runTranscriptStorageTests` from
6187+ * `@trigger.dev/sdk/ai/test` checks yours against the contract.
6188+ *
6189+ * A storage with `loadContext` also owns the model's context on every
6190+ * turn, which is what `hydrateMessages` did. The two cannot be combined.
6191+ *
6192+ * @example
6193+ * ```ts
6194+ * chat.agent({
6195+ * id: "my-chat",
6196+ * storage: myPostgresTranscriptStorage,
6197+ * run: async ({ messages, signal, streamText }) =>
6198+ * streamText({ model, messages, abortSignal: signal }),
6199+ * });
6200+ * ```
6201+ */
6202+ storage?: TranscriptStorage<inferSchemaOut<TClientDataSchema>>;
6203+
61576204 /**
61586205 * Called at the start of every turn, after message accumulation and `onChatStart` (turn 0),
61596206 * but before the `run` function executes.
@@ -6740,6 +6787,7 @@ function chatAgent<
67406787 onChatStart,
67416788 onValidateMessages,
67426789 hydrateMessages,
6790+ storage,
67436791 actionSchema,
67446792 onAction,
67456793 onTurnStart,
@@ -6769,12 +6817,17 @@ function chatAgent<
67696817 } = options;
67706818
67716819 if (hydrateMessages) {
6772- const storageAtDefinition = transcriptStorageOverride ?? defaultStorage;
6773- if (typeof storageAtDefinition.loadContext === "function") {
6820+ if (storage) {
6821+ throw new Error(
6822+ `chat.agent: "${options.id}" sets both \`hydrateMessages\` and \`storage\`. ` +
6823+ "`hydrateMessages` is deprecated and replaced by the storage: `save` receives every " +
6824+ "change and `loadContext` on the storage owns the model's context. Remove `hydrateMessages`."
6825+ );
6826+ }
6827+ if (typeof (transcriptStorageOverride ?? defaultStorage).loadContext === "function") {
67746828 throw new Error(
67756829 `chat.agent: "${options.id}" sets \`hydrateMessages\` and uses a transcript storage with ` +
6776- "`loadContext`. Both would own the model's context; keep one. `hydrateMessages` is " +
6777- "deprecated, so prefer `loadContext` on the storage."
6830+ "`loadContext`. Both would own the model's context; keep one."
67786831 );
67796832 }
67806833 warnHydrateMessagesDeprecatedOnce(options.id);
@@ -6946,7 +6999,10 @@ function chatAgent<
69466999 // collectively cost ~600ms on every first-message TTFC. Both reads
69477000 // swallow errors internally; the agent stays available either way.
69487001 const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
6949- const transcriptStorage = transcriptStorageOverride ?? defaultStorage;
7002+ const transcriptStorage: TranscriptStorage<unknown> =
7003+ (storage as TranscriptStorage<unknown> | undefined) ??
7004+ transcriptStorageOverride ??
7005+ defaultStorage;
69507006 const storageLoadContext = transcriptStorage.loadContext?.bind(transcriptStorage);
69517007 /**
69527008 * Who supplies the model's context each turn: the deprecated
@@ -12659,6 +12715,68 @@ async function mintPublicTokenWithOverride(args: {
1265912715 });
1266012716}
1266112717
12718+ export type CreateChatLoadTranscriptActionOptions = {
12719+ /**
12720+ * Scope the action to a specific API client configuration (secret key,
12721+ * base URL) instead of the process-wide one. The default storage reads
12722+ * through this client.
12723+ */
12724+ apiClient?: ApiClientConfiguration;
12725+ /** Page size when the caller passes none. */
12726+ limit?: number;
12727+ };
12728+
12729+ export type ChatLoadTranscriptParams<TClientData = unknown> = {
12730+ chatId: string;
12731+ clientData?: TClientData;
12732+ limit?: number;
12733+ before?: string;
12734+ };
12735+
12736+ /**
12737+ * Creates a server-side helper that reads a conversation from a transcript
12738+ * storage, for rendering history before the chat connects. Works the same
12739+ * for every storage, the platform default included, so the browser never
12740+ * reads a store directly and the secret key stays on the server.
12741+ *
12742+ * Wrap it in a Next.js server action (or any server-side handler), scope it
12743+ * to the authenticated user through `clientData`, and pass the result to
12744+ * `useLoadTranscript` in the browser.
12745+ *
12746+ * @example
12747+ * ```ts
12748+ * // actions.ts
12749+ * "use server";
12750+ * import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
12751+ *
12752+ * export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
12753+ * ```
12754+ */
12755+ function createChatLoadTranscriptAction<TClientData = unknown>(
12756+ storage: TranscriptStorage<TClientData>,
12757+ options?: CreateChatLoadTranscriptActionOptions
12758+ ): (params: ChatLoadTranscriptParams<TClientData>) => Promise<TranscriptLoadResult> {
12759+ return async (params) => {
12760+ if (!params.chatId) {
12761+ throw new Error("chat.createLoadTranscriptAction: params.chatId is required.");
12762+ }
12763+ if (options?.apiClient) {
12764+ const { apiClient, ...rest } = options;
12765+ return apiClientManager.runWithConfig(apiClient, () =>
12766+ createChatLoadTranscriptAction(storage, rest)(params)
12767+ );
12768+ }
12769+ const limit = params.limit ?? options?.limit;
12770+ return storage.load(
12771+ { chatId: params.chatId, clientData: params.clientData as TClientData },
12772+ {
12773+ ...(limit !== undefined ? { limit } : {}),
12774+ ...(params.before !== undefined ? { before: params.before } : {}),
12775+ }
12776+ );
12777+ };
12778+ }
12779+
1266212780export const chat = {
1266312781 /** Create a chat agent. See {@link chatAgent}. */
1266412782 agent: chatAgent,
@@ -12670,6 +12788,8 @@ export const chat = {
1267012788 withClientData,
1267112789 /** Create a server-side helper for starting (or resuming) a Session for a chatId. See {@link createChatStartSessionAction}. */
1267212790 createStartSessionAction: createChatStartSessionAction,
12791+ /** Returns a server-side helper that reads a conversation from a transcript storage. */
12792+ createLoadTranscriptAction: createChatLoadTranscriptAction,
1267312793 /** Pipe a stream to the chat transport. See {@link pipeChat}. */
1267412794 pipe: pipeChat,
1267512795 /** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */
0 commit comments