@trigger.dev/sdk 0.0.0-prerelease-20260908122921 → 0.0.0-prerelease-20260909071038

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/commonjs/v3/ai-shared.d.ts +15 -0
  2. package/dist/commonjs/v3/ai-shared.js +35 -1
  3. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  4. package/dist/commonjs/v3/ai.d.ts +76 -9
  5. package/dist/commonjs/v3/ai.js +316 -222
  6. package/dist/commonjs/v3/ai.js.map +1 -1
  7. package/dist/commonjs/v3/chat-client.d.ts +8 -0
  8. package/dist/commonjs/v3/chat-client.js +199 -109
  9. package/dist/commonjs/v3/chat-client.js.map +1 -1
  10. package/dist/commonjs/v3/chat-react.d.ts +54 -0
  11. package/dist/commonjs/v3/chat-react.js +83 -0
  12. package/dist/commonjs/v3/chat-react.js.map +1 -1
  13. package/dist/commonjs/v3/chat.d.ts +11 -0
  14. package/dist/commonjs/v3/chat.js +36 -27
  15. package/dist/commonjs/v3/chat.js.map +1 -1
  16. package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
  17. package/dist/commonjs/v3/chatSnapshotIo.js +174 -0
  18. package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
  19. package/dist/commonjs/v3/test/index.d.ts +1 -0
  20. package/dist/commonjs/v3/test/index.js +3 -1
  21. package/dist/commonjs/v3/test/index.js.map +1 -1
  22. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -4
  23. package/dist/commonjs/v3/test/mock-chat-agent.js +25 -4
  24. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  25. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
  26. package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
  27. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
  28. package/dist/commonjs/v3/transcriptStorage.d.ts +229 -0
  29. package/dist/commonjs/v3/transcriptStorage.js +321 -0
  30. package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
  31. package/dist/commonjs/version.js +1 -1
  32. package/dist/esm/v3/ai-shared.d.ts +15 -0
  33. package/dist/esm/v3/ai-shared.js +33 -0
  34. package/dist/esm/v3/ai-shared.js.map +1 -1
  35. package/dist/esm/v3/ai.d.ts +76 -9
  36. package/dist/esm/v3/ai.js +306 -217
  37. package/dist/esm/v3/ai.js.map +1 -1
  38. package/dist/esm/v3/chat-client.d.ts +8 -0
  39. package/dist/esm/v3/chat-client.js +200 -110
  40. package/dist/esm/v3/chat-client.js.map +1 -1
  41. package/dist/esm/v3/chat-react.d.ts +54 -0
  42. package/dist/esm/v3/chat-react.js +81 -0
  43. package/dist/esm/v3/chat-react.js.map +1 -1
  44. package/dist/esm/v3/chat.d.ts +11 -0
  45. package/dist/esm/v3/chat.js +36 -27
  46. package/dist/esm/v3/chat.js.map +1 -1
  47. package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
  48. package/dist/esm/v3/chatSnapshotIo.js +166 -0
  49. package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
  50. package/dist/esm/v3/test/index.d.ts +1 -0
  51. package/dist/esm/v3/test/index.js +1 -0
  52. package/dist/esm/v3/test/index.js.map +1 -1
  53. package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -4
  54. package/dist/esm/v3/test/mock-chat-agent.js +25 -4
  55. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  56. package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
  57. package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
  58. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
  59. package/dist/esm/v3/transcriptStorage.d.ts +229 -0
  60. package/dist/esm/v3/transcriptStorage.js +309 -0
  61. package/dist/esm/v3/transcriptStorage.js.map +1 -0
  62. package/dist/esm/version.js +1 -1
  63. package/docs/ai-chat/actions.mdx +2 -2
  64. package/docs/ai-chat/background-injection.mdx +2 -0
  65. package/docs/ai-chat/compaction.mdx +2 -0
  66. package/docs/ai-chat/frontend.mdx +2 -0
  67. package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
  68. package/docs/ai-chat/patterns/database-persistence.mdx +6 -2
  69. package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
  70. package/docs/ai-chat/reference.mdx +56 -5
  71. package/docs/ai-chat/transcript-storage.mdx +233 -0
  72. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  73. package/docs/deploy-environment-variables.mdx +23 -2
  74. package/package.json +2 -2
  75. package/skills/trigger-authoring-chat-agent/SKILL.md +3 -2
  76. package/skills/trigger-chat-agent-advanced/SKILL.md +5 -5
package/dist/esm/v3/ai.js CHANGED
@@ -3,6 +3,16 @@ import { accessoryAttributes, apiClientManager, controlSubtype, generateJWT, get
3
3
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
4
4
  import { trace } from "@opentelemetry/api";
5
5
  import { tool as aiTool, convertToModelMessages, dynamicTool, generateId as generateMessageId, getToolName, isToolUIPart, jsonSchema, readUIMessageStream, streamText as aiStreamText, zodSchema, } from "../imports/ai-runtime.js";
6
+ import { createTranscriptShadow, defaultStorage, diffTranscript, parseTranscriptRuntimeState, prefixFingerprint, restoreModelLane, } from "./transcriptStorage.js";
7
+ let transcriptStorageOverride;
8
+ /**
9
+ * Test-only override for the storage `chat.agent` persists through, so a
10
+ * test can capture the exact changesets the runtime produces.
11
+ * @internal
12
+ */
13
+ export function __setTranscriptStorageForTests(storage) {
14
+ transcriptStorageOverride = storage;
15
+ }
6
16
  import { PENDING_MESSAGE_INJECTED_TYPE, upsertIncomingMessage, chatRunTags, } from "./ai-shared.js";
7
17
  import { auth } from "./auth.js";
8
18
  import { locals } from "./locals.js";
@@ -105,182 +115,8 @@ async function findLatestSessionInCursor(chatId) {
105
115
  }
106
116
  return latestCursor;
107
117
  }
108
- let readChatSnapshotImpl;
109
- export function __setReadChatSnapshotImplForTests(impl) {
110
- readChatSnapshotImpl = impl;
111
- }
112
- let writeChatSnapshotImpl;
113
- export function __setWriteChatSnapshotImplForTests(impl) {
114
- writeChatSnapshotImpl = impl;
115
- }
116
- /**
117
- * Read the persisted snapshot for a session. Returns `undefined` on:
118
- * - missing object (404 from the presigned GET — fresh session, never
119
- * persisted)
120
- * - presign failure (network/auth issue)
121
- * - malformed JSON
122
- * - version mismatch (forward-compat — older runtimes ignore newer blobs)
123
- *
124
- * Always swallows errors via `logger.warn`. The agent boot loop must stay
125
- * available even if S3 hiccups; the worst case is replaying more of
126
- * `session.out` than strictly necessary.
127
- * @internal
128
- */
129
- async function readChatSnapshot(sessionId) {
130
- if (readChatSnapshotImpl) {
131
- return (await readChatSnapshotImpl(sessionId)) ?? undefined;
132
- }
133
- const apiClient = apiClientManager.clientOrThrow();
134
- let presignedUrl;
135
- try {
136
- const resp = await apiClient.getChatSnapshotUrl(sessionId);
137
- presignedUrl = resp.presignedUrl;
138
- }
139
- catch (error) {
140
- logger.warn("chat.agent: snapshot presign (read) failed; continuing without snapshot", {
141
- error: error instanceof Error ? error.message : String(error),
142
- sessionId,
143
- });
144
- return undefined;
145
- }
146
- let response;
147
- try {
148
- response = await fetch(presignedUrl, { method: "GET" });
149
- }
150
- catch (error) {
151
- logger.warn("chat.agent: snapshot fetch failed; continuing without snapshot", {
152
- error: error instanceof Error ? error.message : String(error),
153
- sessionId,
154
- });
155
- return undefined;
156
- }
157
- if (response.status === 404) {
158
- // First-ever boot for this session — no snapshot yet. Caller falls
159
- // through to replay-only.
160
- return undefined;
161
- }
162
- if (!response.ok) {
163
- logger.warn("chat.agent: snapshot fetch returned non-OK; continuing without snapshot", {
164
- status: response.status,
165
- sessionId,
166
- });
167
- return undefined;
168
- }
169
- let parsed;
170
- try {
171
- parsed = await response.json();
172
- }
173
- catch (error) {
174
- logger.warn("chat.agent: snapshot JSON parse failed; continuing without snapshot", {
175
- error: error instanceof Error ? error.message : String(error),
176
- sessionId,
177
- });
178
- return undefined;
179
- }
180
- if (!parsed || typeof parsed !== "object")
181
- return undefined;
182
- const candidate = parsed;
183
- if (candidate.version !== 1 || !Array.isArray(candidate.messages)) {
184
- logger.warn("chat.agent: snapshot version/shape mismatch; ignoring", {
185
- version: candidate.version,
186
- sessionId,
187
- });
188
- return undefined;
189
- }
190
- return candidate;
191
- }
192
- /**
193
- * Persist the snapshot for a session. Awaited by callers immediately after
194
- * `onTurnComplete` — the agent may suspend right after this point, and
195
- * fire-and-forget promises don't reliably complete on suspend.
196
- *
197
- * Errors are swallowed via `logger.warn`. A failed write means the next
198
- * boot replays slightly more of `session.out` (back to the previous
199
- * snapshot's cursor) instead of failing — the conversation stays
200
- * coherent, only the boot path does marginally more work.
201
- * @internal
202
- */
203
- async function writeChatSnapshot(sessionId, snapshot) {
204
- if (writeChatSnapshotImpl) {
205
- await writeChatSnapshotImpl(sessionId, snapshot);
206
- return;
207
- }
208
- const apiClient = apiClientManager.clientOrThrow();
209
- let presignedUrl;
210
- try {
211
- const resp = await apiClient.createChatSnapshotUploadUrl(sessionId);
212
- presignedUrl = resp.presignedUrl;
213
- }
214
- catch (error) {
215
- logger.warn("chat.agent: snapshot presign (write) failed; next run will replay further", {
216
- error: error instanceof Error ? error.message : String(error),
217
- sessionId,
218
- });
219
- return;
220
- }
221
- let response;
222
- try {
223
- response = await fetch(presignedUrl, {
224
- method: "PUT",
225
- headers: { "content-type": "application/json" },
226
- body: JSON.stringify(snapshot),
227
- });
228
- }
229
- catch (error) {
230
- logger.warn("chat.agent: snapshot upload failed; next run will replay further", {
231
- error: error instanceof Error ? error.message : String(error),
232
- sessionId,
233
- });
234
- return;
235
- }
236
- if (!response.ok) {
237
- logger.warn("chat.agent: snapshot upload returned non-OK; next run will replay further", {
238
- status: response.status,
239
- sessionId,
240
- });
241
- }
242
- }
243
- /**
244
- * Test-only entry point that bypasses `__setReadChatSnapshotImplForTests`
245
- * and reaches the real `apiClient.getPayloadUrl` + `fetch` + JSON-parse path.
246
- * Used by `chat-snapshot.test.ts` to verify 404 / 500 / malformed JSON /
247
- * version-mismatch / network-error behavior end-to-end. Tests mock global
248
- * `fetch` and the api-client config; this wrapper lets them drive the
249
- * production code without the override hook short-circuiting.
250
- *
251
- * Not part of the public API. The `__` prefix and `ForTests` suffix mirror
252
- * the override-hook setters above.
253
- * @internal
254
- */
255
- export async function __readChatSnapshotProductionPathForTests(sessionId) {
256
- const saved = readChatSnapshotImpl;
257
- readChatSnapshotImpl = undefined;
258
- try {
259
- return await readChatSnapshot(sessionId);
260
- }
261
- finally {
262
- readChatSnapshotImpl = saved;
263
- }
264
- }
265
- /**
266
- * Test-only entry point that bypasses `__setWriteChatSnapshotImplForTests`
267
- * and reaches the real `apiClient.createUploadPayloadUrl` + `fetch` PUT
268
- * path. Pairs with `__readChatSnapshotProductionPathForTests` — see that
269
- * function's note for the rationale.
270
- *
271
- * Not part of the public API.
272
- * @internal
273
- */
274
- export async function __writeChatSnapshotProductionPathForTests(sessionId, snapshot) {
275
- const saved = writeChatSnapshotImpl;
276
- writeChatSnapshotImpl = undefined;
277
- try {
278
- await writeChatSnapshot(sessionId, snapshot);
279
- }
280
- finally {
281
- writeChatSnapshotImpl = saved;
282
- }
283
- }
118
+ export { __readChatSnapshotProductionPathForTests, __setReadChatSnapshotImplForTests, __setWriteChatSnapshotImplForTests, __writeChatSnapshotProductionPathForTests, } from "./chatSnapshotIo.js";
119
+ export { defaultStorage, memoryTranscriptStorage, reduceTranscriptChanges, snapshotTranscriptStorage, } from "./transcriptStorage.js";
284
120
  /**
285
121
  * Merge two `UIMessage[]` lists by `id`, with the second list winning on
286
122
  * collision. Used at run boot to combine the snapshot's persisted history
@@ -1946,6 +1782,12 @@ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
1946
1782
  * @internal
1947
1783
  */
1948
1784
  const chatBackgroundQueueKey = locals.create("chat.backgroundQueue");
1785
+ /**
1786
+ * Background injections a step-boundary drain handed to the model this turn,
1787
+ * with the transcript message they followed. Reconciled into the model lane
1788
+ * and the persisted injections once the turn's response is in.
1789
+ */
1790
+ const chatPendingBackgroundKey = locals.create("chat.pendingBackground");
1949
1791
  /**
1950
1792
  * System-role context injected mid-conversation, held for the instructions lane.
1951
1793
  *
@@ -3617,6 +3459,12 @@ function toStreamTextOptions(options) {
3617
3459
  if (bgQueue && bgQueue.length > 0) {
3618
3460
  const injected = bgQueue.splice(0); // drain
3619
3461
  resultMessages = [...(resultMessages ?? messages), ...injected];
3462
+ const pendingBackground = locals.get(chatPendingBackgroundKey) ?? [];
3463
+ pendingBackground.push({
3464
+ afterId: locals.get(chatCurrentUIMessagesKey)?.at(-1)?.id ?? "",
3465
+ messages: injected,
3466
+ });
3467
+ locals.set(chatPendingBackgroundKey, pendingBackground);
3620
3468
  }
3621
3469
  return resultMessages ? { messages: resultMessages } : undefined;
3622
3470
  };
@@ -3684,6 +3532,16 @@ function isUIMessageStreamable(value) {
3684
3532
  "toUIMessageStream" in value &&
3685
3533
  typeof value.toUIMessageStream === "function");
3686
3534
  }
3535
+ const warnedHydrateMessagesDeprecated = new Set();
3536
+ function warnHydrateMessagesDeprecatedOnce(agentId) {
3537
+ if (warnedHydrateMessagesDeprecated.has(agentId))
3538
+ return;
3539
+ warnedHydrateMessagesDeprecated.add(agentId);
3540
+ console.warn(`[chat.agent] \`hydrateMessages\` on "${agentId}" is deprecated. Give the agent a transcript ` +
3541
+ "storage instead: `save` receives every change to the conversation and `loadContext` " +
3542
+ "lets the application own the model's context, with crash recovery and durable " +
3543
+ "compaction that `hydrateMessages` never had.");
3544
+ }
3687
3545
  let warnedMissingOnAction = false;
3688
3546
  function warnMissingOnActionOnce() {
3689
3547
  if (warnedMissingOnAction)
@@ -3896,7 +3754,19 @@ function chatCustomAgent(options) {
3896
3754
  return task;
3897
3755
  }
3898
3756
  function chatAgent(options) {
3899
- const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages, actionSchema, onAction, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, pendingMessages: pendingMessagesConfig, prepareMessages, tools: toolsOption, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, onChatSuspend, onChatResume, exitAfterPreloadIdle = false, oomMachine, registry: promptRegistry, system: agentSystem, cacheControl: agentCacheControl, systemProviderOptions: agentSystemProviderOptions, versionSkew, ...restOptions } = options;
3757
+ const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages, storage, actionSchema, onAction, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, pendingMessages: pendingMessagesConfig, prepareMessages, tools: toolsOption, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, onChatSuspend, onChatResume, exitAfterPreloadIdle = false, oomMachine, registry: promptRegistry, system: agentSystem, cacheControl: agentCacheControl, systemProviderOptions: agentSystemProviderOptions, versionSkew, ...restOptions } = options;
3758
+ if (hydrateMessages) {
3759
+ if (storage) {
3760
+ throw new Error(`chat.agent: "${options.id}" sets both \`hydrateMessages\` and \`storage\`. ` +
3761
+ "`hydrateMessages` is deprecated and replaced by the storage: `save` receives every " +
3762
+ "change and `loadContext` on the storage owns the model's context. Remove `hydrateMessages`.");
3763
+ }
3764
+ if (typeof (transcriptStorageOverride ?? defaultStorage).loadContext === "function") {
3765
+ throw new Error(`chat.agent: "${options.id}" sets \`hydrateMessages\` and uses a transcript storage with ` +
3766
+ "`loadContext`. Both would own the model's context; keep one.");
3767
+ }
3768
+ warnHydrateMessagesDeprecatedOnce(options.id);
3769
+ }
3900
3770
  const parseClientData = clientDataSchema ? getSchemaParseFn(clientDataSchema) : undefined;
3901
3771
  const parseAction = actionSchema ? getSchemaParseFn(actionSchema) : undefined;
3902
3772
  // chat.agent does not expose generic retry options (see docstring on
@@ -3979,6 +3849,24 @@ function chatAgent(options) {
3979
3849
  // durable snapshot + `session.out` replay (or `hydrateMessages` if
3980
3850
  // registered) — the wire is delta-only now, no longer a seed.
3981
3851
  let accumulatedMessages = [];
3852
+ /**
3853
+ * Give the model accumulator the background injections a step-boundary
3854
+ * drain handed to the model this turn, and record them for persistence.
3855
+ * Returns how many model messages were appended.
3856
+ */
3857
+ const reconcilePendingBackground = () => {
3858
+ const pending = locals.get(chatPendingBackgroundKey);
3859
+ if (!pending || pending.length === 0)
3860
+ return 0;
3861
+ locals.set(chatPendingBackgroundKey, []);
3862
+ let appended = 0;
3863
+ for (const entry of pending) {
3864
+ accumulatedMessages.push(...entry.messages);
3865
+ laneInjections.push(entry);
3866
+ appended += entry.messages.length;
3867
+ }
3868
+ return appended;
3869
+ };
3982
3870
  /**
3983
3871
  * Give the model accumulator the steering messages a drain consumed,
3984
3872
  * in the form the model actually received. Appended, never reconverted
@@ -4014,7 +3902,41 @@ function chatAgent(options) {
4014
3902
  // collectively cost ~600ms on every first-message TTFC. Both reads
4015
3903
  // swallow errors internally; the agent stays available either way.
4016
3904
  const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
3905
+ const transcriptStorage = storage ??
3906
+ transcriptStorageOverride ??
3907
+ defaultStorage;
3908
+ const storageLoadContext = transcriptStorage.loadContext?.bind(transcriptStorage);
3909
+ /**
3910
+ * Who supplies the model's context each turn: the deprecated
3911
+ * `hydrateMessages` hook, the storage's `loadContext`, or (undefined)
3912
+ * the runtime's own transcript.
3913
+ */
3914
+ const loadContextHook = hydrateMessages
3915
+ ? (event) => hydrateMessages(event)
3916
+ : storageLoadContext
3917
+ ? (event) => storageLoadContext({ chatId: event.chatId, clientData: event.clientData }, event)
3918
+ : undefined;
3919
+ let transcriptShadow = createTranscriptShadow([]);
3920
+ let bootTranscriptState = null;
3921
+ /**
3922
+ * True while the model lane holds a compaction summary, so it cannot be
3923
+ * rebuilt from the transcript and has to be persisted as state. Reset
3924
+ * wherever the lane is reconverted from the UI lane.
3925
+ */
3926
+ let laneCompacted = false;
3927
+ /** Conversational `chat.inject` messages in the lane, anchored to the transcript. */
3928
+ let laneInjections = [];
3929
+ let persistedStateSet = false;
4017
3930
  let bootSnapshot;
3931
+ let bootClientData = payload.metadata;
3932
+ if (parseClientData) {
3933
+ try {
3934
+ bootClientData = await parseClientData(payload.metadata);
3935
+ }
3936
+ catch {
3937
+ bootClientData = payload.metadata;
3938
+ }
3939
+ }
4018
3940
  /**
4019
3941
  * The `lastOutEventId` the most recent snapshot carried.
4020
3942
  *
@@ -4024,31 +3946,95 @@ function chatAgent(options) {
4024
3946
  * keeps an action's write cursor-neutral.
4025
3947
  */
4026
3948
  let lastSnapshotOutEventId;
3949
+ const storageTrigger = (trigger) => trigger === "regenerate-message"
3950
+ ? "regenerate-message"
3951
+ : trigger === "action" || trigger === "action-turn"
3952
+ ? "action"
3953
+ : "submit-message";
3954
+ /**
3955
+ * Hand the runtime's view of the transcript to the storage as a
3956
+ * changeset: the diff against what was last saved, plus the cursors the
3957
+ * next boot resumes from. The shadow only advances when the save
3958
+ * succeeds, so a failed save is folded into the next changeset.
3959
+ */
3960
+ /** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
3961
+ let transcriptState = null;
3962
+ const saveTranscript = async (opts) => {
3963
+ const { changes, shadow } = diffTranscript(transcriptShadow, opts.messages, {
3964
+ nonFinalIds: opts.nonFinalIds,
3965
+ });
3966
+ const throughId = opts.messages.at(-1)?.id ?? "";
3967
+ const queued = locals.get(chatBackgroundQueueKey) ?? [];
3968
+ const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
3969
+ ? {
3970
+ v: 1,
3971
+ ...(laneCompacted
3972
+ ? {
3973
+ compaction: {
3974
+ modelMessages: accumulatedMessages,
3975
+ throughId,
3976
+ fingerprint: prefixFingerprint(shadow, throughId),
3977
+ },
3978
+ }
3979
+ : {}),
3980
+ ...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
3981
+ ...(queued.length > 0 ? { queued: [...queued] } : {}),
3982
+ }
3983
+ : null;
3984
+ if (runtimeState !== null || persistedStateSet) {
3985
+ changes.push({ op: "state", value: runtimeState });
3986
+ }
3987
+ transcriptState = runtimeState;
3988
+ const inCursor = chatInputRouter().resumeFloor();
3989
+ await transcriptStorage.save({
3990
+ chatId: payload.chatId,
3991
+ clientData: opts.clientData,
3992
+ turn: opts.turn,
3993
+ trigger: opts.trigger,
3994
+ runId: ctx.run.id,
3995
+ ctx,
3996
+ }, {
3997
+ reason: opts.reason,
3998
+ changes,
3999
+ transcript: {
4000
+ entries: opts.messages.map((message) => ({
4001
+ id: message.id,
4002
+ final: !shadow.nonFinal.has(message.id),
4003
+ message,
4004
+ })),
4005
+ state: transcriptState,
4006
+ },
4007
+ cursors: {
4008
+ lastOutEventId: opts.lastOutEventId,
4009
+ lastInEventId: inCursor !== undefined ? String(inCursor) : undefined,
4010
+ },
4011
+ });
4012
+ transcriptShadow = shadow;
4013
+ persistedStateSet = runtimeState !== null;
4014
+ };
4027
4015
  /**
4028
4016
  * Persist the accumulator outside a turn.
4029
4017
  *
4030
4018
  * An action is not a turn, so it never reaches the turn-complete path where
4031
- * the snapshot is normally written — but it can change the conversation in
4032
- * two ways: a `chat.history` mutation, and a response streamed back from
4033
- * `onAction`. Both have to survive, and one write at the end of the action
4034
- * covers both rather than writing twice for a regenerate that does both.
4019
+ * the transcript is normally saved, but a `chat.history` mutation changes
4020
+ * the conversation and has to survive the run ending.
4035
4021
  *
4036
4022
  * Cursor-neutral: an action has no turn cursor of its own, and writing
4037
4023
  * `undefined` would drop the resume point the last turn established and make
4038
4024
  * the next boot replay from further back.
4039
4025
  */
4040
- const writeSnapshotOutsideTurn = async (reason) => {
4026
+ const writeSnapshotOutsideTurn = async (reason, turnContext) => {
4041
4027
  if (hydrateMessages)
4042
4028
  return;
4043
4029
  try {
4044
4030
  await tracer.startActiveSpan("snapshot.write", async () => {
4045
- const snapshotInCursor = chatInputRouter().resumeFloor();
4046
- await writeChatSnapshot(sessionIdForSnapshot, {
4047
- version: 1,
4048
- savedAt: Date.now(),
4031
+ await saveTranscript({
4032
+ reason: "action",
4049
4033
  messages: accumulatedUIMessages,
4034
+ turn: turnContext.turn,
4035
+ trigger: "action",
4036
+ clientData: turnContext.clientData,
4050
4037
  lastOutEventId: lastSnapshotOutEventId,
4051
- lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
4052
4038
  });
4053
4039
  }, {
4054
4040
  attributes: {
@@ -4082,7 +4068,7 @@ function chatAgent(options) {
4082
4068
  // resume-cursor block reuses it instead of re-scanning.
4083
4069
  let bootInCursor;
4084
4070
  let bootInCursorResolved = false;
4085
- if (!hydrateMessages && couldHavePriorState) {
4071
+ if (couldHavePriorState) {
4086
4072
  // Single parent span for the whole boot read phase — snapshot
4087
4073
  // read, session.out replay, session.in replay. Per-phase timing
4088
4074
  // + result counts are attributes on the span.
@@ -4090,18 +4076,33 @@ function chatAgent(options) {
4090
4076
  // snapshot read
4091
4077
  const snapStart = Date.now();
4092
4078
  try {
4093
- bootSnapshot = await readChatSnapshot(sessionIdForSnapshot);
4079
+ const loaded = hydrateMessages
4080
+ ? undefined
4081
+ : await transcriptStorage.load({
4082
+ chatId: payload.chatId,
4083
+ clientData: bootClientData,
4084
+ });
4085
+ if (loaded) {
4086
+ transcriptShadow = createTranscriptShadow(loaded.messages, new Set(loaded.nonFinalIds ?? []));
4087
+ bootTranscriptState = loaded.state;
4088
+ transcriptState = loaded.state ?? null;
4089
+ persistedStateSet = loaded.state !== null && loaded.state !== undefined;
4090
+ bootSnapshot = {
4091
+ messages: loaded.messages,
4092
+ lastOutEventId: loaded.cursors?.lastOutEventId,
4093
+ lastInEventId: loaded.cursors?.lastInEventId,
4094
+ };
4095
+ }
4094
4096
  }
4095
4097
  catch (error) {
4096
- // `readChatSnapshot` already swallows + warns internally; this catch
4097
- // is just belt-and-suspenders against tracer/span errors.
4098
- logger.warn("chat.agent: snapshot read failed; continuing without snapshot", {
4098
+ logger.warn("chat.agent: transcript load failed; continuing from the stream tail", {
4099
4099
  error: error instanceof Error ? error.message : String(error),
4100
4100
  sessionId: sessionIdForSnapshot,
4101
4101
  });
4102
4102
  }
4103
4103
  bootSpan.setAttribute("chat.boot.snapshot.durationMs", Date.now() - snapStart);
4104
- bootSpan.setAttribute("chat.boot.snapshot.present", !!bootSnapshot);
4104
+ bootSpan.setAttribute("chat.boot.snapshot.present", bootSnapshot !== undefined &&
4105
+ (bootSnapshot.messages.length > 0 || bootSnapshot.lastOutEventId !== undefined));
4105
4106
  bootSpan.setAttribute("chat.boot.snapshot.messageCount", bootSnapshot?.messages?.length ?? 0);
4106
4107
  // Seed the trim chain from the snapshot's `lastOutEventId` (the SSE
4107
4108
  // id of the previous turn's `turn-complete` control record). The
@@ -4214,7 +4215,7 @@ function chatAgent(options) {
4214
4215
  resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
4215
4216
  });
4216
4217
  // ── Recovery boot + chain reconstruction ────────────────────────
4217
- if (!hydrateMessages) {
4218
+ {
4218
4219
  const settledMessages = mergeByIdReplaceWins(bootSnapshot?.messages ?? [], replayedSettled);
4219
4220
  const inFlightUsers = replayedInTail.map((r) => r.message);
4220
4221
  const partialAssistant = replayedPartial;
@@ -4367,7 +4368,8 @@ function chatAgent(options) {
4367
4368
  // path where wire-borne UIMessage[] still seeds the accumulator,
4368
4369
  // and it's safe because the route handler isn't subject to the
4369
4370
  // `/in/append` 512 KiB cap.
4370
- if (accumulatedUIMessages.length === 0 &&
4371
+ if (!loadContextHook &&
4372
+ accumulatedUIMessages.length === 0 &&
4371
4373
  payload.trigger === "handover-prepare" &&
4372
4374
  Array.isArray(payload.headStartMessages) &&
4373
4375
  payload.headStartMessages.length > 0) {
@@ -4396,7 +4398,17 @@ function chatAgent(options) {
4396
4398
  }
4397
4399
  }
4398
4400
  try {
4399
- accumulatedMessages = await toModelMessages(accumulatedUIMessages);
4401
+ const bootRuntimeState = parseTranscriptRuntimeState(bootTranscriptState);
4402
+ const restored = await restoreModelLane(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
4403
+ accumulatedMessages = restored.messages;
4404
+ laneCompacted = restored.compacted;
4405
+ laneInjections = restored.injections;
4406
+ if (bootRuntimeState?.queued && bootRuntimeState.queued.length > 0) {
4407
+ locals.set(chatBackgroundQueueKey, [
4408
+ ...(locals.get(chatBackgroundQueueKey) ?? []),
4409
+ ...bootRuntimeState.queued,
4410
+ ]);
4411
+ }
4400
4412
  }
4401
4413
  catch (error) {
4402
4414
  logger.warn("chat.agent: toModelMessages failed at boot; starting empty", {
@@ -4773,6 +4785,7 @@ function chatAgent(options) {
4773
4785
  } // end else (no boot-injected first turn)
4774
4786
  }
4775
4787
  for (let turn = 0; turn < maxTurns; turn++) {
4788
+ let turnClientData = payload.metadata;
4776
4789
  // Declared here so the finally can detach it — a handler leaked past
4777
4790
  // its turn duplicates every mid-stream message into the shared buffer.
4778
4791
  let turnMsgSub;
@@ -4794,6 +4807,7 @@ function chatAgent(options) {
4794
4807
  // `cleanupAbortedParts` no longer has to be re-applied below.
4795
4808
  const cleanedIncomingMessages = incomingMessages.map((msg) => msg.role === "assistant" ? cleanupAbortedParts(msg) : msg);
4796
4809
  const clientData = (parseClientData ? await parseClientData(wireMetadata) : wireMetadata);
4810
+ turnClientData = clientData;
4797
4811
  const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages);
4798
4812
  // Actions are not turns. They use a different span name
4799
4813
  // and don't carry a turn.number. Branched on at `isAction`.
@@ -4830,6 +4844,7 @@ function chatAgent(options) {
4830
4844
  locals.set(chatDeferKey, new Set());
4831
4845
  locals.set(chatCompactionStateKey, undefined);
4832
4846
  locals.set(chatSteeringQueueKey, []);
4847
+ locals.set(chatPendingBackgroundKey, []);
4833
4848
  locals.set(chatResponsePartsKey, []);
4834
4849
  // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
4835
4850
  // by deferred work from the previous turn's onTurnComplete need to
@@ -4941,9 +4956,9 @@ function chatAgent(options) {
4941
4956
  ? await parseAction(currentWirePayload.action)
4942
4957
  : currentWirePayload.action;
4943
4958
  // Hydrate messages from backend if configured
4944
- if (hydrateMessages) {
4959
+ if (loadContextHook) {
4945
4960
  const hydrated = await tracer.startActiveSpan("hydrateMessages()", async () => {
4946
- return hydrateMessages({
4961
+ return loadContextHook({
4947
4962
  chatId: currentWirePayload.chatId,
4948
4963
  turn,
4949
4964
  trigger: "action",
@@ -4963,6 +4978,8 @@ function chatAgent(options) {
4963
4978
  });
4964
4979
  accumulatedUIMessages = [...hydrated];
4965
4980
  accumulatedMessages = await toModelMessages(hydrated);
4981
+ laneCompacted = false;
4982
+ laneInjections = [];
4966
4983
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4967
4984
  }
4968
4985
  // Fire onAction — handler may mutate state via
@@ -4993,6 +5010,8 @@ function chatAgent(options) {
4993
5010
  locals.set(chatOverrideMessagesKey, undefined);
4994
5011
  accumulatedUIMessages = [...actionOverride];
4995
5012
  accumulatedMessages = await toModelMessages(actionOverride);
5013
+ laneCompacted = false;
5014
+ laneInjections = [];
4996
5015
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4997
5016
  actionChangedHistory = true;
4998
5017
  }
@@ -5015,7 +5034,7 @@ function chatAgent(options) {
5015
5034
  // route handler's first-turn history to the hydrate hook as
5016
5035
  // incoming messages instead (gated on the pending handover).
5017
5036
  if (turn === 0 &&
5018
- hydrateMessages &&
5037
+ loadContextHook &&
5019
5038
  cleanedUIMessages.length === 0 &&
5020
5039
  (locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
5021
5040
  Array.isArray(payload.headStartMessages) &&
@@ -5045,7 +5064,7 @@ function chatAgent(options) {
5045
5064
  },
5046
5065
  }));
5047
5066
  }
5048
- if (hydrateMessages) {
5067
+ if (loadContextHook) {
5049
5068
  // Snapshot the ids the accumulator knew BEFORE this
5050
5069
  // turn ran — used below to decide whether an
5051
5070
  // incoming wire message is genuinely new or just a
@@ -5063,7 +5082,7 @@ function chatAgent(options) {
5063
5082
  // was always true for `submit-message` is now true for every
5064
5083
  // trigger.
5065
5084
  const hydrated = await tracer.startActiveSpan("hydrateMessages()", async () => {
5066
- return hydrateMessages({
5085
+ return loadContextHook({
5067
5086
  chatId: currentWirePayload.chatId,
5068
5087
  turn,
5069
5088
  trigger: currentWirePayload.trigger,
@@ -5102,6 +5121,8 @@ function chatAgent(options) {
5102
5121
  }
5103
5122
  accumulatedUIMessages = merged;
5104
5123
  accumulatedMessages = await toModelMessages(merged);
5124
+ laneCompacted = false;
5125
+ laneInjections = [];
5105
5126
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5106
5127
  // Track new messages for onTurnComplete.newUIMessages.
5107
5128
  // Only push for genuinely new ids — HITL continuations
@@ -5147,6 +5168,8 @@ function chatAgent(options) {
5147
5168
  accumulatedUIMessages.pop();
5148
5169
  }
5149
5170
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5171
+ laneCompacted = false;
5172
+ laneInjections = [];
5150
5173
  }
5151
5174
  else if (cleanedUIMessages.length > 0) {
5152
5175
  // Submit-message (and the special-cased
@@ -5197,6 +5220,8 @@ function chatAgent(options) {
5197
5220
  if (!inPlace) {
5198
5221
  logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
5199
5222
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5223
+ laneCompacted = false;
5224
+ laneInjections = [];
5200
5225
  }
5201
5226
  }
5202
5227
  else {
@@ -5282,7 +5307,7 @@ function chatAgent(options) {
5282
5307
  // history rather than from the snapshot the edit replaced.
5283
5308
  // The turn then does its own hooks, completion and snapshot.
5284
5309
  if (actionChangedHistory) {
5285
- await writeSnapshotOutsideTurn("action");
5310
+ await writeSnapshotOutsideTurn("action", { turn, clientData });
5286
5311
  }
5287
5312
  actionTurn = true;
5288
5313
  }
@@ -5294,7 +5319,7 @@ function chatAgent(options) {
5294
5319
  else {
5295
5320
  msgSub?.off();
5296
5321
  if (actionChangedHistory) {
5297
- await writeSnapshotOutsideTurn("action");
5322
+ await writeSnapshotOutsideTurn("action", { turn, clientData });
5298
5323
  }
5299
5324
  await writeTurnCompleteChunk(currentWirePayload.chatId);
5300
5325
  // Don't consume a turn iteration — actions aren't turns.
@@ -5392,6 +5417,8 @@ function chatAgent(options) {
5392
5417
  locals.set(chatOverrideMessagesKey, undefined);
5393
5418
  accumulatedUIMessages = [...turnStartOverride];
5394
5419
  accumulatedMessages = await toModelMessages(turnStartOverride);
5420
+ laneCompacted = false;
5421
+ laneInjections = [];
5395
5422
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5396
5423
  }
5397
5424
  }, {
@@ -5452,7 +5479,12 @@ function chatAgent(options) {
5452
5479
  const lastAccumulated = accumulatedMessages[accumulatedMessages.length - 1];
5453
5480
  const bgQueue = locals.get(chatBackgroundQueueKey);
5454
5481
  if (bgQueue && bgQueue.length > 0 && lastAccumulated?.role !== "tool") {
5455
- accumulatedMessages.push(...bgQueue.splice(0));
5482
+ const injected = bgQueue.splice(0);
5483
+ accumulatedMessages.push(...injected);
5484
+ laneInjections.push({
5485
+ afterId: accumulatedUIMessages.at(-1)?.id ?? "",
5486
+ messages: injected,
5487
+ });
5456
5488
  }
5457
5489
  if (isHeadStartFinalTurn) {
5458
5490
  // The synthesized partial UIMessage IS the response.
@@ -5613,6 +5645,8 @@ function chatAgent(options) {
5613
5645
  locals.set(chatPendingSteerKey, pending);
5614
5646
  const pendingIds = new Set(pending.map((e) => e.ui.id));
5615
5647
  accumulatedMessages = await toModelMessages(runOverride.filter((m) => !pendingIds.has(m.id)));
5648
+ laneCompacted = false;
5649
+ laneInjections = [];
5616
5650
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5617
5651
  }
5618
5652
  // Check if compaction set a model-only override (preserves UI messages).
@@ -5635,6 +5669,8 @@ function chatAgent(options) {
5635
5669
  accumulatedMessages = taskCompactionConfig?.compactModelMessages
5636
5670
  ? await taskCompactionConfig.compactModelMessages(compactEvent)
5637
5671
  : modelOnlyOverride;
5672
+ laneCompacted = true;
5673
+ laneInjections = [];
5638
5674
  // Apply UI messages: callback or default (preserve all)
5639
5675
  if (taskCompactionConfig?.compactUIMessages) {
5640
5676
  accumulatedUIMessages = (await taskCompactionConfig.compactUIMessages(compactEvent));
@@ -5650,7 +5686,7 @@ function chatAgent(options) {
5650
5686
  // branches below, so a turn that captured no response is covered.
5651
5687
  const steerTailThisTurn = reconcilePendingSteer({
5652
5688
  turnNew: turnNewModelMessages,
5653
- }).reduce((n, e) => n + e.model.length, 0);
5689
+ }).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
5654
5690
  // Append the assistant's response (partial or complete) to the accumulator.
5655
5691
  // The onFinish callback fires even on abort/stop, so partial responses
5656
5692
  // from stopped generation are captured correctly.
@@ -5713,6 +5749,8 @@ function chatAgent(options) {
5713
5749
  if (!ok) {
5714
5750
  logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
5715
5751
  accumulatedMessages = await toModelMessages(accumulatedUIMessages);
5752
+ laneCompacted = false;
5753
+ laneInjections = [];
5716
5754
  }
5717
5755
  }
5718
5756
  else {
@@ -5821,6 +5859,8 @@ function chatAgent(options) {
5821
5859
  ],
5822
5860
  },
5823
5861
  ];
5862
+ laneCompacted = true;
5863
+ laneInjections = [];
5824
5864
  // UI messages: callback or default (preserve all)
5825
5865
  if (outerCompaction.compactUIMessages) {
5826
5866
  accumulatedUIMessages = (await outerCompaction.compactUIMessages(outerCompactEvent));
@@ -5910,6 +5950,8 @@ function chatAgent(options) {
5910
5950
  locals.set(chatOverrideMessagesKey, undefined);
5911
5951
  accumulatedUIMessages = [...override];
5912
5952
  accumulatedMessages = await toModelMessages(override);
5953
+ laneCompacted = false;
5954
+ laneInjections = [];
5913
5955
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5914
5956
  // Update event so onTurnComplete sees compacted messages
5915
5957
  turnCompleteEvent.messages = accumulatedMessages;
@@ -5956,6 +5998,8 @@ function chatAgent(options) {
5956
5998
  locals.set(chatOverrideMessagesKey, undefined);
5957
5999
  accumulatedUIMessages = [...turnCompleteOverride];
5958
6000
  accumulatedMessages = await toModelMessages(turnCompleteOverride);
6001
+ laneCompacted = false;
6002
+ laneInjections = [];
5959
6003
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5960
6004
  }
5961
6005
  }, {
@@ -6009,15 +6053,18 @@ function chatAgent(options) {
6009
6053
  if (!hydrateMessages) {
6010
6054
  try {
6011
6055
  await tracer.startActiveSpan("snapshot.write", async () => {
6012
- const snapshotInCursor = chatInputRouter().resumeFloor();
6013
6056
  lastSnapshotOutEventId =
6014
6057
  turnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
6015
- await writeChatSnapshot(sessionIdForSnapshot, {
6016
- version: 1,
6017
- savedAt: Date.now(),
6058
+ await saveTranscript({
6059
+ reason: "turn-complete",
6018
6060
  messages: accumulatedUIMessages,
6061
+ turn,
6062
+ trigger: storageTrigger(currentWirePayload.trigger),
6063
+ clientData,
6019
6064
  lastOutEventId: lastSnapshotOutEventId,
6020
- lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
6065
+ nonFinalIds: wasStopped && capturedResponseMessage?.id
6066
+ ? new Set([capturedResponseMessage.id])
6067
+ : undefined,
6021
6068
  });
6022
6069
  }, {
6023
6070
  attributes: {
@@ -6238,6 +6285,7 @@ function chatAgent(options) {
6238
6285
  let erroredNewUIMessages = buildErroredNew();
6239
6286
  let erroredNewModelMessages = [];
6240
6287
  const reconciledSteer = reconcilePendingSteer();
6288
+ const backgroundTailThisTurn = reconcilePendingBackground();
6241
6289
  if (!responseCommitted) {
6242
6290
  try {
6243
6291
  if (erroredNewUIMessages.length > 0) {
@@ -6262,10 +6310,13 @@ function chatAgent(options) {
6262
6310
  accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
6263
6311
  }
6264
6312
  else {
6265
- const ok = await replaceModelRun(accumulatedMessages, erroredUIMessages[partialIdx], partialResponse, reconciledSteer.reduce((n, e) => n + e.model.length, 0));
6313
+ const ok = await replaceModelRun(accumulatedMessages, erroredUIMessages[partialIdx], partialResponse, reconciledSteer.reduce((n, e) => n + e.model.length, 0) +
6314
+ backgroundTailThisTurn);
6266
6315
  if (!ok) {
6267
6316
  logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
6268
6317
  accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
6318
+ laneCompacted = false;
6319
+ laneInjections = [];
6269
6320
  }
6270
6321
  }
6271
6322
  accumulatedUIMessages = erroredUIMessagesWithPartial;
@@ -6329,13 +6380,14 @@ function chatAgent(options) {
6329
6380
  // neither the snapshot nor the replayable `.in` tail.
6330
6381
  if (!hydrateMessages) {
6331
6382
  try {
6332
- const errorSnapshotInCursor = chatInputRouter().resumeFloor();
6333
- await writeChatSnapshot(sessionIdForSnapshot, {
6334
- version: 1,
6335
- savedAt: Date.now(),
6383
+ await saveTranscript({
6384
+ reason: "turn-error",
6336
6385
  messages: erroredUIMessagesWithPartial,
6337
- lastOutEventId: errorTurnCompleteResult?.lastEventId,
6338
- lastInEventId: errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined,
6386
+ turn,
6387
+ trigger: storageTrigger(currentWirePayload.trigger),
6388
+ clientData: turnClientData,
6389
+ lastOutEventId: lastSnapshotOutEventId,
6390
+ nonFinalIds: includePartial && partialResponse ? new Set([partialResponse.id]) : undefined,
6339
6391
  });
6340
6392
  }
6341
6393
  catch (error) {
@@ -8652,6 +8704,41 @@ async function mintPublicTokenWithOverride(args) {
8652
8704
  expirationTime: args.expirationTime,
8653
8705
  });
8654
8706
  }
8707
+ /**
8708
+ * Creates a server-side helper that reads a conversation from a transcript
8709
+ * storage, for rendering history before the chat connects. Works the same
8710
+ * for every storage, the platform default included, so the browser never
8711
+ * reads a store directly and the secret key stays on the server.
8712
+ *
8713
+ * Wrap it in a Next.js server action (or any server-side handler), scope it
8714
+ * to the authenticated user through `clientData`, and pass the result to
8715
+ * `useLoadTranscript` in the browser.
8716
+ *
8717
+ * @example
8718
+ * ```ts
8719
+ * // actions.ts
8720
+ * "use server";
8721
+ * import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
8722
+ *
8723
+ * export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
8724
+ * ```
8725
+ */
8726
+ function createChatLoadTranscriptAction(storage, options) {
8727
+ return async (params) => {
8728
+ if (!params.chatId) {
8729
+ throw new Error("chat.createLoadTranscriptAction: params.chatId is required.");
8730
+ }
8731
+ if (options?.apiClient) {
8732
+ const { apiClient, ...rest } = options;
8733
+ return apiClientManager.runWithConfig(apiClient, () => createChatLoadTranscriptAction(storage, rest)(params));
8734
+ }
8735
+ const limit = params.limit ?? options?.limit;
8736
+ return storage.load({ chatId: params.chatId, clientData: params.clientData }, {
8737
+ ...(limit !== undefined ? { limit } : {}),
8738
+ ...(params.before !== undefined ? { before: params.before } : {}),
8739
+ });
8740
+ };
8741
+ }
8655
8742
  export const chat = {
8656
8743
  /** Create a chat agent. See {@link chatAgent}. */
8657
8744
  agent: chatAgent,
@@ -8663,6 +8750,8 @@ export const chat = {
8663
8750
  withClientData,
8664
8751
  /** Create a server-side helper for starting (or resuming) a Session for a chatId. See {@link createChatStartSessionAction}. */
8665
8752
  createStartSessionAction: createChatStartSessionAction,
8753
+ /** Returns a server-side helper that reads a conversation from a transcript storage. */
8754
+ createLoadTranscriptAction: createChatLoadTranscriptAction,
8666
8755
  /** Pipe a stream to the chat transport. See {@link pipeChat}. */
8667
8756
  pipe: pipeChat,
8668
8757
  /** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */