@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.
- package/dist/commonjs/v3/ai-shared.d.ts +15 -0
- package/dist/commonjs/v3/ai-shared.js +35 -1
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +76 -9
- package/dist/commonjs/v3/ai.js +316 -222
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +8 -0
- package/dist/commonjs/v3/chat-client.js +199 -109
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +54 -0
- package/dist/commonjs/v3/chat-react.js +83 -0
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +11 -0
- package/dist/commonjs/v3/chat.js +36 -27
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/commonjs/v3/chatSnapshotIo.js +174 -0
- package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
- package/dist/commonjs/v3/test/index.d.ts +1 -0
- package/dist/commonjs/v3/test/index.js +3 -1
- package/dist/commonjs/v3/test/index.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -4
- package/dist/commonjs/v3/test/mock-chat-agent.js +25 -4
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/commonjs/v3/transcriptStorage.d.ts +229 -0
- package/dist/commonjs/v3/transcriptStorage.js +321 -0
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai-shared.d.ts +15 -0
- package/dist/esm/v3/ai-shared.js +33 -0
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +76 -9
- package/dist/esm/v3/ai.js +306 -217
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +8 -0
- package/dist/esm/v3/chat-client.js +200 -110
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +54 -0
- package/dist/esm/v3/chat-react.js +81 -0
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +11 -0
- package/dist/esm/v3/chat.js +36 -27
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/esm/v3/chatSnapshotIo.js +166 -0
- package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
- package/dist/esm/v3/test/index.d.ts +1 -0
- package/dist/esm/v3/test/index.js +1 -0
- package/dist/esm/v3/test/index.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -4
- package/dist/esm/v3/test/mock-chat-agent.js +25 -4
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
- package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
- package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
- package/dist/esm/v3/transcriptStorage.d.ts +229 -0
- package/dist/esm/v3/transcriptStorage.js +309 -0
- package/dist/esm/v3/transcriptStorage.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +2 -2
- package/docs/ai-chat/background-injection.mdx +2 -0
- package/docs/ai-chat/compaction.mdx +2 -0
- package/docs/ai-chat/frontend.mdx +2 -0
- package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
- package/docs/ai-chat/patterns/database-persistence.mdx +6 -2
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
- package/docs/ai-chat/reference.mdx +56 -5
- package/docs/ai-chat/transcript-storage.mdx +233 -0
- package/docs/config/extensions/syncEnvVars.mdx +6 -0
- package/docs/deploy-environment-variables.mdx +23 -2
- package/package.json +2 -2
- package/skills/trigger-authoring-chat-agent/SKILL.md +3 -2
- 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
|
-
|
|
109
|
-
export
|
|
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
|
|
4032
|
-
*
|
|
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
|
-
|
|
4046
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
4959
|
+
if (loadContextHook) {
|
|
4945
4960
|
const hydrated = await tracer.startActiveSpan("hydrateMessages()", async () => {
|
|
4946
|
-
return
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
6016
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6333
|
-
|
|
6334
|
-
version: 1,
|
|
6335
|
-
savedAt: Date.now(),
|
|
6383
|
+
await saveTranscript({
|
|
6384
|
+
reason: "turn-error",
|
|
6336
6385
|
messages: erroredUIMessagesWithPartial,
|
|
6337
|
-
|
|
6338
|
-
|
|
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}. */
|