@trigger.dev/sdk 0.0.0-prerelease-streamfix-20260909094302 → 0.0.0-prerelease-20260911094758
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/imports/ai-runtime-cjs.cjs.map +1 -1
- package/dist/commonjs/imports/ai-runtime.js +2 -0
- 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 +275 -25
- package/dist/commonjs/v3/ai.js +1373 -354
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +11 -2
- package/dist/commonjs/v3/chat-client.js +227 -111
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +88 -0
- package/dist/commonjs/v3/chat-react.js +130 -1
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +42 -6
- package/dist/commonjs/v3/chat-server.js +52 -7
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +92 -10
- package/dist/commonjs/v3/chat.js +328 -73
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/commonjs/v3/chatSnapshotIo.js +173 -0
- package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
- package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
- package/dist/commonjs/v3/chatVersionSkew.js +30 -0
- package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
- package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
- package/dist/commonjs/v3/externalDeploymentId.js +43 -0
- package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
- package/dist/commonjs/v3/retry.d.ts +4 -0
- package/dist/commonjs/v3/retry.js +22 -13
- package/dist/commonjs/v3/retry.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +15 -2
- package/dist/commonjs/v3/sessions.js +12 -1
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.js +30 -36
- package/dist/commonjs/v3/shared.js.map +1 -1
- 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 +47 -4
- package/dist/commonjs/v3/test/mock-chat-agent.js +116 -10
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +6 -0
- package/dist/commonjs/v3/test/test-session-handle.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 +261 -0
- package/dist/commonjs/v3/transcriptStorage.js +370 -0
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/imports/ai-runtime.d.ts +2 -2
- package/dist/esm/imports/ai-runtime.js +2 -2
- package/dist/esm/imports/ai-runtime.js.map +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 +275 -25
- package/dist/esm/v3/ai.js +1365 -351
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +11 -2
- package/dist/esm/v3/chat-client.js +228 -112
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +88 -0
- package/dist/esm/v3/chat-react.js +127 -1
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +42 -6
- package/dist/esm/v3/chat-server.js +53 -8
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +92 -10
- package/dist/esm/v3/chat.js +329 -74
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
- package/dist/esm/v3/chatSnapshotIo.js +165 -0
- package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
- package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
- package/dist/esm/v3/chatVersionSkew.js +27 -0
- package/dist/esm/v3/chatVersionSkew.js.map +1 -0
- package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
- package/dist/esm/v3/externalDeploymentId.js +38 -0
- package/dist/esm/v3/externalDeploymentId.js.map +1 -0
- package/dist/esm/v3/retry.d.ts +4 -0
- package/dist/esm/v3/retry.js +14 -8
- package/dist/esm/v3/retry.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +15 -2
- package/dist/esm/v3/sessions.js +11 -1
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.js +17 -23
- package/dist/esm/v3/shared.js.map +1 -1
- 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 +47 -4
- package/dist/esm/v3/test/mock-chat-agent.js +118 -12
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +6 -0
- package/dist/esm/v3/test/test-session-handle.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 +261 -0
- package/dist/esm/v3/transcriptStorage.js +357 -0
- package/dist/esm/v3/transcriptStorage.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +55 -23
- package/docs/ai-chat/anatomy.mdx +3 -3
- package/docs/ai-chat/backend.mdx +125 -48
- package/docs/ai-chat/background-injection.mdx +69 -19
- package/docs/ai-chat/client-protocol.mdx +5 -4
- package/docs/ai-chat/compaction.mdx +13 -7
- package/docs/ai-chat/custom-agents.mdx +23 -0
- package/docs/ai-chat/fast-starts.mdx +27 -20
- package/docs/ai-chat/frontend.mdx +19 -14
- package/docs/ai-chat/how-it-works.mdx +1 -1
- package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +16 -14
- package/docs/ai-chat/patterns/database-persistence.mdx +6 -2
- package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
- package/docs/ai-chat/patterns/skills.mdx +7 -10
- package/docs/ai-chat/patterns/version-upgrades.mdx +79 -6
- package/docs/ai-chat/pending-messages.mdx +3 -3
- package/docs/ai-chat/prompt-caching.mdx +23 -25
- package/docs/ai-chat/quick-start.mdx +11 -11
- package/docs/ai-chat/reference.mdx +67 -9
- package/docs/ai-chat/sessions.mdx +6 -1
- package/docs/ai-chat/testing.mdx +2 -1
- package/docs/ai-chat/tools.mdx +18 -13
- package/docs/ai-chat/transcript-storage.mdx +239 -0
- package/docs/ai-chat/upgrade-guide.mdx +2 -2
- package/docs/apikeys.mdx +27 -45
- package/docs/config/extensions/syncEnvVars.mdx +6 -0
- package/docs/deploy-environment-variables.mdx +23 -2
- package/docs/deployment/overview.mdx +4 -8
- package/docs/deployment/preview-branches.mdx +4 -4
- package/docs/deployment/version-skew-protection.mdx +62 -0
- package/docs/manual-setup.mdx +7 -7
- package/docs/mcp-tools.mdx +9 -0
- package/docs/quick-start.mdx +3 -3
- package/docs/realtime/auth.mdx +1 -1
- package/docs/reports.mdx +157 -0
- package/docs/self-hosting/security.mdx +17 -0
- package/docs/tasks/scheduled.mdx +24 -0
- package/docs/triggering.mdx +1 -1
- package/docs/troubleshooting-zod.mdx +158 -0
- package/package.json +4 -4
- package/skills/trigger-authoring-chat-agent/SKILL.md +41 -29
- package/skills/trigger-chat-agent-advanced/SKILL.md +54 -19
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
|
|
4
|
-
exports.
|
|
5
|
-
exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
|
|
6
|
-
exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
|
|
7
|
-
exports.__writeChatSnapshotProductionPathForTests = __writeChatSnapshotProductionPathForTests;
|
|
3
|
+
exports.chat = exports.__buildManagedStreamTextOptionsForTests = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = exports.snapshotTranscriptStorage = exports.reduceTranscriptChanges = exports.memoryTranscriptStorage = exports.defaultStorage = exports.__writeChatSnapshotProductionPathForTests = exports.__setWriteChatSnapshotImplForTests = exports.__setReadChatSnapshotImplForTests = exports.__readChatSnapshotProductionPathForTests = void 0;
|
|
4
|
+
exports.__setTranscriptStorageForTests = __setTranscriptStorageForTests;
|
|
8
5
|
exports.__mergeByIdReplaceWinsForTests = __mergeByIdReplaceWinsForTests;
|
|
9
6
|
exports.__setReplaySessionOutTailImplForTests = __setReplaySessionOutTailImplForTests;
|
|
10
7
|
exports.__replaySessionOutTailProductionPathForTests = __replaySessionOutTailProductionPathForTests;
|
|
@@ -19,6 +16,16 @@ const v3_1 = require("@trigger.dev/core/v3");
|
|
|
19
16
|
// ESM-only `ai@7` (see ../imports/ai-runtime.ts).
|
|
20
17
|
const api_1 = require("@opentelemetry/api");
|
|
21
18
|
const ai_runtime_js_1 = require("../imports/ai-runtime.js");
|
|
19
|
+
const transcriptStorage_js_1 = require("./transcriptStorage.js");
|
|
20
|
+
let transcriptStorageOverride;
|
|
21
|
+
/**
|
|
22
|
+
* Test-only override for the storage `chat.agent` persists through, so a
|
|
23
|
+
* test can capture the exact changesets the runtime produces.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
function __setTranscriptStorageForTests(storage) {
|
|
27
|
+
transcriptStorageOverride = storage;
|
|
28
|
+
}
|
|
22
29
|
const ai_shared_js_1 = require("./ai-shared.js");
|
|
23
30
|
Object.defineProperty(exports, "PENDING_MESSAGE_INJECTED_TYPE", { enumerable: true, get: function () { return ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE; } });
|
|
24
31
|
Object.defineProperty(exports, "upsertIncomingMessage", { enumerable: true, get: function () { return ai_shared_js_1.upsertIncomingMessage; } });
|
|
@@ -35,6 +42,8 @@ const metadata_js_1 = require("./metadata.js");
|
|
|
35
42
|
// pulled in transitively here never reach a client chunk.
|
|
36
43
|
const agentSkillsRuntime_js_1 = require("./agentSkillsRuntime.js");
|
|
37
44
|
const aiAutoTelemetry_js_1 = require("./aiAutoTelemetry.js");
|
|
45
|
+
const externalDeploymentId_js_1 = require("./externalDeploymentId.js");
|
|
46
|
+
const chatVersionSkew_js_1 = require("./chatVersionSkew.js");
|
|
38
47
|
const sessions_js_1 = require("./sessions.js");
|
|
39
48
|
const shared_js_1 = require("./shared.js");
|
|
40
49
|
const streams_js_1 = require("./streams.js");
|
|
@@ -121,182 +130,16 @@ async function findLatestSessionInCursor(chatId) {
|
|
|
121
130
|
}
|
|
122
131
|
return latestCursor;
|
|
123
132
|
}
|
|
124
|
-
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
* - missing object (404 from the presigned GET — fresh session, never
|
|
135
|
-
* persisted)
|
|
136
|
-
* - presign failure (network/auth issue)
|
|
137
|
-
* - malformed JSON
|
|
138
|
-
* - version mismatch (forward-compat — older runtimes ignore newer blobs)
|
|
139
|
-
*
|
|
140
|
-
* Always swallows errors via `logger.warn`. The agent boot loop must stay
|
|
141
|
-
* available even if S3 hiccups; the worst case is replaying more of
|
|
142
|
-
* `session.out` than strictly necessary.
|
|
143
|
-
* @internal
|
|
144
|
-
*/
|
|
145
|
-
async function readChatSnapshot(sessionId) {
|
|
146
|
-
if (readChatSnapshotImpl) {
|
|
147
|
-
return (await readChatSnapshotImpl(sessionId)) ?? undefined;
|
|
148
|
-
}
|
|
149
|
-
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
150
|
-
let presignedUrl;
|
|
151
|
-
try {
|
|
152
|
-
const resp = await apiClient.getChatSnapshotUrl(sessionId);
|
|
153
|
-
presignedUrl = resp.presignedUrl;
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
v3_1.logger.warn("chat.agent: snapshot presign (read) failed; continuing without snapshot", {
|
|
157
|
-
error: error instanceof Error ? error.message : String(error),
|
|
158
|
-
sessionId,
|
|
159
|
-
});
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
let response;
|
|
163
|
-
try {
|
|
164
|
-
response = await fetch(presignedUrl, { method: "GET" });
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
v3_1.logger.warn("chat.agent: snapshot fetch failed; continuing without snapshot", {
|
|
168
|
-
error: error instanceof Error ? error.message : String(error),
|
|
169
|
-
sessionId,
|
|
170
|
-
});
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
if (response.status === 404) {
|
|
174
|
-
// First-ever boot for this session — no snapshot yet. Caller falls
|
|
175
|
-
// through to replay-only.
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
if (!response.ok) {
|
|
179
|
-
v3_1.logger.warn("chat.agent: snapshot fetch returned non-OK; continuing without snapshot", {
|
|
180
|
-
status: response.status,
|
|
181
|
-
sessionId,
|
|
182
|
-
});
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
let parsed;
|
|
186
|
-
try {
|
|
187
|
-
parsed = await response.json();
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
v3_1.logger.warn("chat.agent: snapshot JSON parse failed; continuing without snapshot", {
|
|
191
|
-
error: error instanceof Error ? error.message : String(error),
|
|
192
|
-
sessionId,
|
|
193
|
-
});
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
if (!parsed || typeof parsed !== "object")
|
|
197
|
-
return undefined;
|
|
198
|
-
const candidate = parsed;
|
|
199
|
-
if (candidate.version !== 1 || !Array.isArray(candidate.messages)) {
|
|
200
|
-
v3_1.logger.warn("chat.agent: snapshot version/shape mismatch; ignoring", {
|
|
201
|
-
version: candidate.version,
|
|
202
|
-
sessionId,
|
|
203
|
-
});
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
return candidate;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Persist the snapshot for a session. Awaited by callers immediately after
|
|
210
|
-
* `onTurnComplete` — the agent may suspend right after this point, and
|
|
211
|
-
* fire-and-forget promises don't reliably complete on suspend.
|
|
212
|
-
*
|
|
213
|
-
* Errors are swallowed via `logger.warn`. A failed write means the next
|
|
214
|
-
* boot replays slightly more of `session.out` (back to the previous
|
|
215
|
-
* snapshot's cursor) instead of failing — the conversation stays
|
|
216
|
-
* coherent, only the boot path does marginally more work.
|
|
217
|
-
* @internal
|
|
218
|
-
*/
|
|
219
|
-
async function writeChatSnapshot(sessionId, snapshot) {
|
|
220
|
-
if (writeChatSnapshotImpl) {
|
|
221
|
-
await writeChatSnapshotImpl(sessionId, snapshot);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
225
|
-
let presignedUrl;
|
|
226
|
-
try {
|
|
227
|
-
const resp = await apiClient.createChatSnapshotUploadUrl(sessionId);
|
|
228
|
-
presignedUrl = resp.presignedUrl;
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
v3_1.logger.warn("chat.agent: snapshot presign (write) failed; next run will replay further", {
|
|
232
|
-
error: error instanceof Error ? error.message : String(error),
|
|
233
|
-
sessionId,
|
|
234
|
-
});
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
let response;
|
|
238
|
-
try {
|
|
239
|
-
response = await fetch(presignedUrl, {
|
|
240
|
-
method: "PUT",
|
|
241
|
-
headers: { "content-type": "application/json" },
|
|
242
|
-
body: JSON.stringify(snapshot),
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
v3_1.logger.warn("chat.agent: snapshot upload failed; next run will replay further", {
|
|
247
|
-
error: error instanceof Error ? error.message : String(error),
|
|
248
|
-
sessionId,
|
|
249
|
-
});
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
if (!response.ok) {
|
|
253
|
-
v3_1.logger.warn("chat.agent: snapshot upload returned non-OK; next run will replay further", {
|
|
254
|
-
status: response.status,
|
|
255
|
-
sessionId,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Test-only entry point that bypasses `__setReadChatSnapshotImplForTests`
|
|
261
|
-
* and reaches the real `apiClient.getPayloadUrl` + `fetch` + JSON-parse path.
|
|
262
|
-
* Used by `chat-snapshot.test.ts` to verify 404 / 500 / malformed JSON /
|
|
263
|
-
* version-mismatch / network-error behavior end-to-end. Tests mock global
|
|
264
|
-
* `fetch` and the api-client config; this wrapper lets them drive the
|
|
265
|
-
* production code without the override hook short-circuiting.
|
|
266
|
-
*
|
|
267
|
-
* Not part of the public API. The `__` prefix and `ForTests` suffix mirror
|
|
268
|
-
* the override-hook setters above.
|
|
269
|
-
* @internal
|
|
270
|
-
*/
|
|
271
|
-
async function __readChatSnapshotProductionPathForTests(sessionId) {
|
|
272
|
-
const saved = readChatSnapshotImpl;
|
|
273
|
-
readChatSnapshotImpl = undefined;
|
|
274
|
-
try {
|
|
275
|
-
return await readChatSnapshot(sessionId);
|
|
276
|
-
}
|
|
277
|
-
finally {
|
|
278
|
-
readChatSnapshotImpl = saved;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Test-only entry point that bypasses `__setWriteChatSnapshotImplForTests`
|
|
283
|
-
* and reaches the real `apiClient.createUploadPayloadUrl` + `fetch` PUT
|
|
284
|
-
* path. Pairs with `__readChatSnapshotProductionPathForTests` — see that
|
|
285
|
-
* function's note for the rationale.
|
|
286
|
-
*
|
|
287
|
-
* Not part of the public API.
|
|
288
|
-
* @internal
|
|
289
|
-
*/
|
|
290
|
-
async function __writeChatSnapshotProductionPathForTests(sessionId, snapshot) {
|
|
291
|
-
const saved = writeChatSnapshotImpl;
|
|
292
|
-
writeChatSnapshotImpl = undefined;
|
|
293
|
-
try {
|
|
294
|
-
await writeChatSnapshot(sessionId, snapshot);
|
|
295
|
-
}
|
|
296
|
-
finally {
|
|
297
|
-
writeChatSnapshotImpl = saved;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
133
|
+
var chatSnapshotIo_js_1 = require("./chatSnapshotIo.js");
|
|
134
|
+
Object.defineProperty(exports, "__readChatSnapshotProductionPathForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__readChatSnapshotProductionPathForTests; } });
|
|
135
|
+
Object.defineProperty(exports, "__setReadChatSnapshotImplForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__setReadChatSnapshotImplForTests; } });
|
|
136
|
+
Object.defineProperty(exports, "__setWriteChatSnapshotImplForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__setWriteChatSnapshotImplForTests; } });
|
|
137
|
+
Object.defineProperty(exports, "__writeChatSnapshotProductionPathForTests", { enumerable: true, get: function () { return chatSnapshotIo_js_1.__writeChatSnapshotProductionPathForTests; } });
|
|
138
|
+
var transcriptStorage_js_2 = require("./transcriptStorage.js");
|
|
139
|
+
Object.defineProperty(exports, "defaultStorage", { enumerable: true, get: function () { return transcriptStorage_js_2.defaultStorage; } });
|
|
140
|
+
Object.defineProperty(exports, "memoryTranscriptStorage", { enumerable: true, get: function () { return transcriptStorage_js_2.memoryTranscriptStorage; } });
|
|
141
|
+
Object.defineProperty(exports, "reduceTranscriptChanges", { enumerable: true, get: function () { return transcriptStorage_js_2.reduceTranscriptChanges; } });
|
|
142
|
+
Object.defineProperty(exports, "snapshotTranscriptStorage", { enumerable: true, get: function () { return transcriptStorage_js_2.snapshotTranscriptStorage; } });
|
|
300
143
|
/**
|
|
301
144
|
* Merge two `UIMessage[]` lists by `id`, with the second list winning on
|
|
302
145
|
* collision. Used at run boot to combine the snapshot's persisted history
|
|
@@ -1045,7 +888,7 @@ async function writeChatCustomAgentClientDataErrorToStream(payload) {
|
|
|
1045
888
|
}
|
|
1046
889
|
}
|
|
1047
890
|
async function reportChatCustomAgentClientDataError(payload, error, options) {
|
|
1048
|
-
const errorText = error instanceof Error ? error.message : "An unexpected error occurred";
|
|
891
|
+
const errorText = error instanceof Error && error.message ? error.message : "An unexpected error occurred";
|
|
1049
892
|
v3_1.logger.warn("chat.customAgent: clientData validation failed", {
|
|
1050
893
|
chatId: payload.chatId,
|
|
1051
894
|
trigger: payload.trigger,
|
|
@@ -1774,12 +1617,19 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1774
1617
|
}
|
|
1775
1618
|
const router = entry.router;
|
|
1776
1619
|
router.restore(checkpoint);
|
|
1620
|
+
if (options?.recoveredSeqNums && options.recoveredSeqNums.length > 0) {
|
|
1621
|
+
router.markRecovered(options.recoveredSeqNums);
|
|
1622
|
+
}
|
|
1777
1623
|
const floor = router.resumeFrom();
|
|
1778
1624
|
if (floor !== undefined) {
|
|
1779
1625
|
v3_1.sessionStreams.setLastSeqNum(chatId, "in", floor);
|
|
1780
1626
|
v3_1.sessionStreams.setLastDispatchedSeqNum(chatId, "in", floor);
|
|
1781
1627
|
}
|
|
1782
1628
|
v3_1.sessionStreams.onRecord(chatId, "in", (record) => {
|
|
1629
|
+
// The floor is the tail's `Last-Event-ID`, but a reconnect can still
|
|
1630
|
+
// re-deliver below it and a replayable route would re-queue it.
|
|
1631
|
+
if (floor !== undefined && record.seqNum <= floor)
|
|
1632
|
+
return true;
|
|
1783
1633
|
router.ingest(record);
|
|
1784
1634
|
return true;
|
|
1785
1635
|
});
|
|
@@ -1951,6 +1801,35 @@ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
|
|
|
1951
1801
|
* @internal
|
|
1952
1802
|
*/
|
|
1953
1803
|
const chatBackgroundQueueKey = locals_js_1.locals.create("chat.backgroundQueue");
|
|
1804
|
+
/**
|
|
1805
|
+
* Background injections a step-boundary drain handed to the model this turn,
|
|
1806
|
+
* with the transcript message they followed. Reconciled into the model lane
|
|
1807
|
+
* and the persisted injections once the turn's response is in.
|
|
1808
|
+
*/
|
|
1809
|
+
const chatPendingBackgroundKey = locals_js_1.locals.create("chat.pendingBackground");
|
|
1810
|
+
/**
|
|
1811
|
+
* System-role context injected mid-conversation, held for the instructions lane.
|
|
1812
|
+
*
|
|
1813
|
+
* Kept apart from the message queue because ai@7 rejects a system message inside
|
|
1814
|
+
* `messages` for every provider — `standardizePrompt` throws upstream of any
|
|
1815
|
+
* provider call, and its own advice is to use the instructions option. Instructions
|
|
1816
|
+
* accept `Array<SystemModelMessage>`, so a system-role injection has a correct
|
|
1817
|
+
* home: appended as another system block rather than smuggled into the transcript.
|
|
1818
|
+
*
|
|
1819
|
+
* This is also the only way to inject *trusted* context. A message injected as
|
|
1820
|
+
* `user` is untrusted by construction, and a well-aligned model treats it that
|
|
1821
|
+
* way — it will say so, and re-derive the answer from tools instead.
|
|
1822
|
+
*/
|
|
1823
|
+
const chatInjectedInstructionsKey = locals_js_1.locals.create("chat.injectedInstructions");
|
|
1824
|
+
/**
|
|
1825
|
+
* What a turn already consumed from the instructions lane, so a second
|
|
1826
|
+
* `toStreamTextOptions()` call in the same turn sees the same blocks.
|
|
1827
|
+
*
|
|
1828
|
+
* Consumed blocks are moved here rather than left in the pending lane: leaving
|
|
1829
|
+
* them there means an injection made during the consumed turn sits behind them,
|
|
1830
|
+
* and clearing the lane on the next turn destroys both.
|
|
1831
|
+
*/
|
|
1832
|
+
const chatInstructionsConsumedKey = locals_js_1.locals.create("chat.injectedInstructionsConsumed");
|
|
1954
1833
|
/**
|
|
1955
1834
|
* Run-scoped pipe counter. Stored in locals so concurrent runs in the
|
|
1956
1835
|
* same worker don't share state.
|
|
@@ -2461,18 +2340,63 @@ const chatToolsOptionKey = locals_js_1.locals.create("chat.toolsOption");
|
|
|
2461
2340
|
const chatResolvedToolsKey = locals_js_1.locals.create("chat.resolvedTools");
|
|
2462
2341
|
/** @internal Flag set by `chat.requestUpgrade()` to exit the loop after the current turn. */
|
|
2463
2342
|
const chatUpgradeRequestedKey = locals_js_1.locals.create("chat.upgradeRequested");
|
|
2343
|
+
/** @internal Target for the upgrade handoff, set by `chat.requestUpgrade({ externalDeploymentId })`. */
|
|
2344
|
+
const chatUpgradeExternalDeploymentIdKey = locals_js_1.locals.create("chat.upgradeExternalDeploymentId");
|
|
2464
2345
|
/**
|
|
2465
2346
|
* @internal Flag set by `chat.endRun()` to exit the loop after the current
|
|
2466
2347
|
* turn completes, without any upgrade semantics. Checked at the same
|
|
2467
2348
|
* post-turn / pre-wait sites as `chatUpgradeRequestedKey`.
|
|
2468
2349
|
*/
|
|
2469
2350
|
const chatEndRunRequestedKey = locals_js_1.locals.create("chat.endRunRequested");
|
|
2351
|
+
/**
|
|
2352
|
+
* @internal Set by `chat.close()`. Holds the close request (and its reason)
|
|
2353
|
+
* for the rest of the run: the loop writes the terminal `session-closed`
|
|
2354
|
+
* record, closes the session row, and exits at the same post-turn /
|
|
2355
|
+
* pre-wait sites as `chatEndRunRequestedKey`.
|
|
2356
|
+
*/
|
|
2357
|
+
const chatCloseRequestedKey = locals_js_1.locals.create("chat.closeRequested");
|
|
2358
|
+
/** @internal Matches the server's `CloseSessionRequestBody.reason` cap. */
|
|
2359
|
+
const CHAT_CLOSE_REASON_MAX_LENGTH = 256;
|
|
2360
|
+
/** @internal Set once the session row is closed, so the close happens once. */
|
|
2361
|
+
const chatClosePerformedKey = locals_js_1.locals.create("chat.closePerformed");
|
|
2362
|
+
/**
|
|
2363
|
+
* @internal Set once the terminal `.out` record is written. Tracked apart from
|
|
2364
|
+
* {@link chatClosePerformedKey} so a retried close does not emit a second
|
|
2365
|
+
* client-visible event.
|
|
2366
|
+
*/
|
|
2367
|
+
const chatCloseRecordWrittenKey = locals_js_1.locals.create("chat.closeRecordWritten");
|
|
2470
2368
|
/** @internal */
|
|
2471
2369
|
const chatAgentCompactionKey = locals_js_1.locals.create("chat.agentCompaction");
|
|
2472
2370
|
/** @internal */
|
|
2473
2371
|
const chatPendingMessagesKey = locals_js_1.locals.create("chat.pendingMessages");
|
|
2474
2372
|
/** @internal */
|
|
2475
2373
|
const chatSteeringQueueKey = locals_js_1.locals.create("chat.steeringQueue");
|
|
2374
|
+
/**
|
|
2375
|
+
* This turn's new messages, as `onTurnComplete.newUIMessages` will see them.
|
|
2376
|
+
*
|
|
2377
|
+
* Held in locals because `drainSteeringQueue` runs outside the turn closure and
|
|
2378
|
+
* has to append the messages it injects. Without that, an injected message
|
|
2379
|
+
* reaches the model and the browser but no hook, so an app persisting from
|
|
2380
|
+
* `onTurnComplete` never learns it existed.
|
|
2381
|
+
*/
|
|
2382
|
+
const chatTurnNewUIMessagesKey = locals_js_1.locals.create("chat.turnNewUIMessages");
|
|
2383
|
+
/**
|
|
2384
|
+
* Steering messages a drain consumed that the model accumulator has not been
|
|
2385
|
+
* given yet.
|
|
2386
|
+
*
|
|
2387
|
+
* The two accumulators are maintained separately, and the model one is
|
|
2388
|
+
* normally advanced by appending each turn's delta. A drained message is
|
|
2389
|
+
* appended to the UI one but reaches the model only through the `prepareStep`
|
|
2390
|
+
* return value, which is per-step: without this the model lane never learns
|
|
2391
|
+
* the message exists and every later turn of the run answers without it,
|
|
2392
|
+
* while the browser, the snapshot and `chat.history.*` all still show it.
|
|
2393
|
+
*
|
|
2394
|
+
* Held as the messages rather than a "rebuild me" flag because the model lane
|
|
2395
|
+
* can only be appended to, never reconstructed. Compaction replaces it with a
|
|
2396
|
+
* summary and deliberately leaves the UI lane whole, so reconverting the UI
|
|
2397
|
+
* lane restores every message the summary replaced.
|
|
2398
|
+
*/
|
|
2399
|
+
const chatPendingSteerKey = locals_js_1.locals.create("chat.pendingSteer");
|
|
2476
2400
|
/** @internal — IDs of messages that were successfully injected via prepareStep */
|
|
2477
2401
|
const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
|
|
2478
2402
|
/** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
|
|
@@ -2825,20 +2749,32 @@ function chatCompactionStep(options) {
|
|
|
2825
2749
|
return result.type === "skipped" ? undefined : result;
|
|
2826
2750
|
};
|
|
2827
2751
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2752
|
+
const EMPTY_DRAIN = { injected: [], claimed: [] };
|
|
2753
|
+
/**
|
|
2754
|
+
* The model messages to record for one claimed message. Without `prepare`
|
|
2755
|
+
* each entry's own conversion is used. With it, `prepare` returned one list
|
|
2756
|
+
* for the whole batch, so the first claimed message carries all of it and the
|
|
2757
|
+
* rest carry none, which keeps the total exactly what the model received.
|
|
2758
|
+
*/
|
|
2759
|
+
function modelFormOf(m, batch, injected) {
|
|
2760
|
+
return batch[0] === m ? injected : [];
|
|
2761
|
+
}
|
|
2831
2762
|
/**
|
|
2832
2763
|
* Drain the steering queue as a batch. Calls `shouldInject` once with all
|
|
2833
2764
|
* pending messages. If it returns true, calls `prepareMessages` once to
|
|
2834
2765
|
* transform the batch, then clears the queue.
|
|
2835
|
-
* Returns the model messages to inject
|
|
2766
|
+
* Returns the model messages to inject and the UI messages actually claimed.
|
|
2767
|
+
*
|
|
2768
|
+
* `claimed` is returned rather than only published to locals because each
|
|
2769
|
+
* surface files it somewhere different: `chat.agent` has an accumulator in
|
|
2770
|
+
* locals, while `chat.createSession` keeps its own. Publishing to locals alone
|
|
2771
|
+
* is silently a no-op for any surface that never set the key.
|
|
2836
2772
|
* @internal
|
|
2837
2773
|
*/
|
|
2838
2774
|
async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
2839
2775
|
const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
|
|
2840
2776
|
if (!queue || queue.length === 0)
|
|
2841
|
-
return
|
|
2777
|
+
return EMPTY_DRAIN;
|
|
2842
2778
|
const ctx = locals_js_1.locals.get(chatTurnContextKey);
|
|
2843
2779
|
const stepNumber = steps.length - 1;
|
|
2844
2780
|
/**
|
|
@@ -2861,7 +2797,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2861
2797
|
// Call shouldInject once for the whole batch
|
|
2862
2798
|
const shouldInject = config.shouldInject ? await config.shouldInject(batchEvent) : false;
|
|
2863
2799
|
if (!shouldInject)
|
|
2864
|
-
return
|
|
2800
|
+
return EMPTY_DRAIN;
|
|
2865
2801
|
const textOfUIMessage = (m) => (m.parts ?? [])
|
|
2866
2802
|
.filter((p) => p.type === "text")
|
|
2867
2803
|
.map((p) => p.text)
|
|
@@ -2907,7 +2843,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2907
2843
|
queue.splice(at, 1);
|
|
2908
2844
|
}
|
|
2909
2845
|
if (claimed.length === 0)
|
|
2910
|
-
return
|
|
2846
|
+
return EMPTY_DRAIN;
|
|
2911
2847
|
/**
|
|
2912
2848
|
* Give the claim back if the transform fails. `prepare` is caller code and
|
|
2913
2849
|
* can throw; the records have already left the router by this point, so
|
|
@@ -2937,6 +2873,37 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2937
2873
|
for (const m of claimedUIMessages)
|
|
2938
2874
|
injectedIds.add(m.id);
|
|
2939
2875
|
}
|
|
2876
|
+
// Record them as part of the conversation.
|
|
2877
|
+
//
|
|
2878
|
+
// The model has them and the browser has them; without this the
|
|
2879
|
+
// accumulator does not, so they reach neither `uiMessages` nor
|
|
2880
|
+
// `newUIMessages` on `onTurnComplete` and an app that persists from there
|
|
2881
|
+
// silently loses the instruction the answer was shaped by. Appending here
|
|
2882
|
+
// rather than at turn end keeps them in the order they happened: after the
|
|
2883
|
+
// message that started the turn, before the response that answers it.
|
|
2884
|
+
//
|
|
2885
|
+
// De-duplicated by id because a step boundary can drain more than once per
|
|
2886
|
+
// turn, and because a message that failed to inject falls back to becoming
|
|
2887
|
+
// its own turn, where it is accumulated the normal way.
|
|
2888
|
+
const currentUIMessages = locals_js_1.locals.get(chatCurrentUIMessagesKey);
|
|
2889
|
+
const turnNew = locals_js_1.locals.get(chatTurnNewUIMessagesKey);
|
|
2890
|
+
for (const m of claimedUIMessages) {
|
|
2891
|
+
if (currentUIMessages && !currentUIMessages.some((existing) => existing.id === m.id)) {
|
|
2892
|
+
currentUIMessages.push(m);
|
|
2893
|
+
}
|
|
2894
|
+
if (turnNew && !turnNew.some((existing) => existing.id === m.id)) {
|
|
2895
|
+
turnNew.push(m);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (claimedUIMessages.length > 0 && currentUIMessages) {
|
|
2899
|
+
const pendingSteer = locals_js_1.locals.get(chatPendingSteerKey) ?? [];
|
|
2900
|
+
for (const m of claimedUIMessages) {
|
|
2901
|
+
if (!pendingSteer.some((existing) => existing.ui.id === m.id)) {
|
|
2902
|
+
pendingSteer.push({ ui: m, model: modelFormOf(m, claimedUIMessages, injected) });
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
locals_js_1.locals.set(chatPendingSteerKey, pendingSteer);
|
|
2906
|
+
}
|
|
2940
2907
|
// Write injection confirmation chunk to the stream so the frontend
|
|
2941
2908
|
// knows which messages were injected and where in the response.
|
|
2942
2909
|
if (injected.length > 0) {
|
|
@@ -2978,7 +2945,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2978
2945
|
/* non-fatal */
|
|
2979
2946
|
}
|
|
2980
2947
|
}
|
|
2981
|
-
return injected;
|
|
2948
|
+
return { injected, claimed: claimedUIMessages };
|
|
2982
2949
|
}, {
|
|
2983
2950
|
attributes: {
|
|
2984
2951
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-message-forward",
|
|
@@ -3204,32 +3171,149 @@ function buildSkillTools(skills) {
|
|
|
3204
3171
|
return { loadSkill, readFile, bash };
|
|
3205
3172
|
}
|
|
3206
3173
|
/**
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3210
|
-
* (
|
|
3211
|
-
*
|
|
3212
|
-
*
|
|
3213
|
-
*
|
|
3174
|
+
* A `streamText` with the agent's managed options already applied.
|
|
3175
|
+
*
|
|
3176
|
+
* Handed to `run()` so the managed state cannot be missed by omission. Spreading
|
|
3177
|
+
* `chat.toStreamTextOptions()` is still supported and equivalent; this exists
|
|
3178
|
+
* because forgetting the spread silently drops the managed prompt, the skill
|
|
3179
|
+
* tools, telemetry, and the `prepareStep` that delivers steering, compaction and
|
|
3180
|
+
* conversational injection.
|
|
3181
|
+
*
|
|
3182
|
+
* Caller options win for everything the caller owns (model, messages, signal,
|
|
3183
|
+
* stopWhen). The three that would otherwise clobber managed behaviour are
|
|
3184
|
+
* merged instead of replaced:
|
|
3185
|
+
*
|
|
3186
|
+
* - `tools` are passed into the helper, so skill tools survive.
|
|
3187
|
+
* - `prepareStep` is composed after the managed one, so a caller's per-step
|
|
3188
|
+
* overrides apply on top of steering and compaction instead of disabling them.
|
|
3189
|
+
*
|
|
3190
|
+
* `system` may be set at the call site, on `chat.agent({ system })`, or
|
|
3191
|
+
* through `chat.prompt.set()`, but only in one of them. Setting it in two
|
|
3192
|
+
* places throws: no shape merges two system values on every supported
|
|
3193
|
+
* version, and dropping one silently is the failure this seam exists to
|
|
3194
|
+
* prevent. Injected instructions append to whichever one is in play.
|
|
3195
|
+
*/
|
|
3196
|
+
/**
|
|
3197
|
+
* The agent-level managed options (`registry`, `system`, `cacheControl`,
|
|
3198
|
+
* `systemProviderOptions`), published for the run so that
|
|
3199
|
+
* `chat.toStreamTextOptions()` applies them too. Without this only the bound
|
|
3200
|
+
* `streamText` saw them, and the documented spread form silently ran without
|
|
3201
|
+
* the agent's system prompt or model.
|
|
3202
|
+
*/
|
|
3203
|
+
const chatAgentManagedConfigKey = locals_js_1.locals.create("chat.agentManagedConfig");
|
|
3204
|
+
/**
|
|
3205
|
+
* The caller's `streamText` options merged with the agent's managed ones.
|
|
3214
3206
|
*
|
|
3215
|
-
*
|
|
3207
|
+
* Pure, and separate from the call so it can be asserted directly: everything
|
|
3208
|
+
* the caller did not name has to survive the merge, and the way to be sure of
|
|
3209
|
+
* that is to look at the merged object rather than at what the model received.
|
|
3216
3210
|
*/
|
|
3211
|
+
function buildManagedStreamTextOptions(options, config) {
|
|
3212
|
+
const { registry, system: agentSystem, cacheControl, systemProviderOptions, tools: agentTools, } = config;
|
|
3213
|
+
/**
|
|
3214
|
+
* Only the three keys that collide are intercepted. Everything else, telemetry
|
|
3215
|
+
* included, stays in `rest` and reaches `streamText` untouched, with the
|
|
3216
|
+
* caller's value winning because `rest` is spread after `managed`. Pulling a
|
|
3217
|
+
* key out to "handle" it is how a caller's option gets silently dropped.
|
|
3218
|
+
*/
|
|
3219
|
+
const { tools, system: callerSystem, prepareStep: callerPrepareStep, ...rest } = options;
|
|
3220
|
+
const managed = toStreamTextOptions({
|
|
3221
|
+
registry,
|
|
3222
|
+
system: callerSystem ?? agentSystem,
|
|
3223
|
+
cacheControl,
|
|
3224
|
+
systemProviderOptions,
|
|
3225
|
+
/**
|
|
3226
|
+
* A call site that names `tools` replaces the agent's set rather than
|
|
3227
|
+
* adding to it, so narrowing the tools for one call still works. Omitting
|
|
3228
|
+
* `tools` falls back to the agent's, which is what an `onAction`
|
|
3229
|
+
* regenerate needs: without it a regenerated answer can call nothing.
|
|
3230
|
+
*/
|
|
3231
|
+
tools: (tools ?? agentTools),
|
|
3232
|
+
});
|
|
3233
|
+
const promptSystem = locals_js_1.locals.get(chatPromptKey)?.text;
|
|
3234
|
+
/**
|
|
3235
|
+
* Two managed sources conflict too, not only a caller against a managed one.
|
|
3236
|
+
* `toStreamTextOptions` resolves `prompt?.text || agentSystem`, so the prompt
|
|
3237
|
+
* would win and `chat.agent({ system })` would go nowhere.
|
|
3238
|
+
*/
|
|
3239
|
+
if (promptSystem && agentSystem) {
|
|
3240
|
+
throw new Error("chat.agent: `system` is set both on chat.agent({ system }) and by chat.prompt.set(), and only one " +
|
|
3241
|
+
"of them can apply. The prompt would win and the agent's `system` would go nowhere. Keep it in one " +
|
|
3242
|
+
"place, and add per-turn context with chat.inject({ role: 'system' }).");
|
|
3243
|
+
}
|
|
3244
|
+
const managedSystem = promptSystem || agentSystem;
|
|
3245
|
+
if (callerSystem !== undefined && managedSystem) {
|
|
3246
|
+
throw new Error("chat.agent: `system` is already set " +
|
|
3247
|
+
(promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
|
|
3248
|
+
", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
|
|
3249
|
+
"per-turn context with chat.inject({ role: 'system' }) rather than a second system value.");
|
|
3250
|
+
}
|
|
3251
|
+
const managedPrepareStep = managed.prepareStep;
|
|
3252
|
+
if (typeof callerPrepareStep === "function") {
|
|
3253
|
+
managed.prepareStep = async (arg) => {
|
|
3254
|
+
const first = managedPrepareStep ? await managedPrepareStep(arg) : undefined;
|
|
3255
|
+
const second = await callerPrepareStep({ ...arg, ...(first ?? {}) });
|
|
3256
|
+
return { ...(first ?? {}), ...(second ?? {}) };
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
return { ...managed, ...rest };
|
|
3260
|
+
}
|
|
3261
|
+
/** @internal Test hook for {@link buildManagedStreamTextOptions}. */
|
|
3262
|
+
exports.__buildManagedStreamTextOptionsForTests = buildManagedStreamTextOptions;
|
|
3263
|
+
function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSystemProviderOptions) {
|
|
3264
|
+
const bound = (options = {}) => (0, ai_runtime_js_1.streamText)(buildManagedStreamTextOptions(options, {
|
|
3265
|
+
registry,
|
|
3266
|
+
system: agentSystem,
|
|
3267
|
+
cacheControl: agentCacheControl,
|
|
3268
|
+
systemProviderOptions: agentSystemProviderOptions,
|
|
3269
|
+
/** Read per call, so per-turn tools resolved after binding are included. */
|
|
3270
|
+
tools: locals_js_1.locals.get(chatResolvedToolsKey),
|
|
3271
|
+
}));
|
|
3272
|
+
return bound;
|
|
3273
|
+
}
|
|
3217
3274
|
function toStreamTextOptions(options) {
|
|
3275
|
+
const agentDefaults = locals_js_1.locals.get(chatAgentManagedConfigKey);
|
|
3276
|
+
if (agentDefaults) {
|
|
3277
|
+
options = {
|
|
3278
|
+
registry: agentDefaults.registry,
|
|
3279
|
+
system: agentDefaults.system,
|
|
3280
|
+
cacheControl: agentDefaults.cacheControl,
|
|
3281
|
+
systemProviderOptions: agentDefaults.systemProviderOptions,
|
|
3282
|
+
...options,
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3218
3285
|
const prompt = locals_js_1.locals.get(chatPromptKey);
|
|
3219
3286
|
const skills = locals_js_1.locals.get(chatSkillsKey);
|
|
3220
3287
|
const result = {};
|
|
3221
3288
|
// Build the combined system prompt: stored prompt + skills preamble.
|
|
3222
|
-
const
|
|
3289
|
+
const baseSystem = options?.system;
|
|
3290
|
+
const baseSystemText = typeof baseSystem === "string"
|
|
3291
|
+
? baseSystem
|
|
3292
|
+
: typeof baseSystem?.content === "string"
|
|
3293
|
+
? baseSystem.content
|
|
3294
|
+
: "";
|
|
3295
|
+
const promptText = prompt?.text || baseSystemText;
|
|
3223
3296
|
const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
|
|
3224
3297
|
if (promptText || skillsText) {
|
|
3225
3298
|
const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3299
|
+
/**
|
|
3300
|
+
* Resolve system-prompt provider options for caching. Precedence, most
|
|
3301
|
+
* specific first and no deep merge: explicit `systemProviderOptions`, the
|
|
3302
|
+
* `cacheControl` sugar, the ones carried on a structured `system` message,
|
|
3303
|
+
* then whatever `chat.prompt.set()` stored.
|
|
3304
|
+
*
|
|
3305
|
+
* A structured `system` counts only when its own text is the one being
|
|
3306
|
+
* sent. When `chat.prompt.set()` supplied the text, its provider options
|
|
3307
|
+
* are the ones that describe it.
|
|
3308
|
+
*/
|
|
3309
|
+
const baseSystemProviderOptions = !prompt?.text && baseSystem && typeof baseSystem !== "string"
|
|
3310
|
+
? baseSystem.providerOptions
|
|
3311
|
+
: undefined;
|
|
3229
3312
|
const systemProviderOptions = options?.systemProviderOptions ??
|
|
3230
3313
|
(options?.cacheControl
|
|
3231
3314
|
? { anthropic: { cacheControl: options.cacheControl } }
|
|
3232
3315
|
: undefined) ??
|
|
3316
|
+
baseSystemProviderOptions ??
|
|
3233
3317
|
locals_js_1.locals.get(chatPromptProviderOptionsKey);
|
|
3234
3318
|
// A bare string stays a bare string (the unchanged default). With provider
|
|
3235
3319
|
// options, emit a structured `SystemModelMessage` so the provider can cache
|
|
@@ -3238,6 +3322,88 @@ function toStreamTextOptions(options) {
|
|
|
3238
3322
|
? { role: "system", content: systemText, providerOptions: systemProviderOptions }
|
|
3239
3323
|
: systemText;
|
|
3240
3324
|
}
|
|
3325
|
+
/**
|
|
3326
|
+
* Append anything injected as system context, in whichever shape the installed
|
|
3327
|
+
* AI SDK accepts.
|
|
3328
|
+
*
|
|
3329
|
+
* `system` widened over time: on ai@5 it is `string` only, and from ai@6 it is
|
|
3330
|
+
* `string | SystemModelMessage | Array<SystemModelMessage>`. This package's peer
|
|
3331
|
+
* range still spans all three, so emitting an array unconditionally would break
|
|
3332
|
+
* v5 consumers — for whom a system-role injection used to work, since v5 accepted
|
|
3333
|
+
* a system message inside `messages` that v7 rejects.
|
|
3334
|
+
*
|
|
3335
|
+
* So: concatenate into one string when the base is a plain string, which every
|
|
3336
|
+
* version accepts and which loses nothing (separate blocks only matter for
|
|
3337
|
+
* per-block `providerOptions`). Use the array form only when the base is already
|
|
3338
|
+
* a structured message — that path requires v6+ regardless, because it is how
|
|
3339
|
+
* prompt caching marks the system block, and flattening it would silently throw
|
|
3340
|
+
* the cache away.
|
|
3341
|
+
*
|
|
3342
|
+
* Either way the injected text goes last: the base prompt keeps its position for
|
|
3343
|
+
* caching, and the addition reads as a later amendment. A changed prefix does
|
|
3344
|
+
* cost the first call its cache hit, on turns that actually injected.
|
|
3345
|
+
*/
|
|
3346
|
+
/**
|
|
3347
|
+
* Consumed once per turn, not once per read, and moved out of the lane rather
|
|
3348
|
+
* than marked read in place.
|
|
3349
|
+
*
|
|
3350
|
+
* Per turn, because a `run()` that builds options twice (a cheap classifier
|
|
3351
|
+
* pass and then the answer) has to see the injection in both, and draining on
|
|
3352
|
+
* read hands it to whichever call ran first. Moved out, because blocks left in
|
|
3353
|
+
* the lane sit in front of anything injected during the same turn, and
|
|
3354
|
+
* clearing the lane on the next turn then destroys both. Outside a turn there
|
|
3355
|
+
* is no turn to scope the stash to, so the lane drains on read there.
|
|
3356
|
+
*/
|
|
3357
|
+
const injectedInstructions = locals_js_1.locals.get(chatInjectedInstructionsKey);
|
|
3358
|
+
const currentTurn = locals_js_1.locals.get(chatTurnContextKey)?.turn;
|
|
3359
|
+
const consumedThisTurn = currentTurn === undefined ? undefined : locals_js_1.locals.get(chatInstructionsConsumedKey);
|
|
3360
|
+
let injectedBlocks = [];
|
|
3361
|
+
if (consumedThisTurn && consumedThisTurn.turn === currentTurn) {
|
|
3362
|
+
injectedBlocks = consumedThisTurn.blocks;
|
|
3363
|
+
// Anything injected since the stash was taken joins it, so an instruction
|
|
3364
|
+
// added after an action read the lane still reaches the real turn that
|
|
3365
|
+
// shares the action's turn number, rather than the one after.
|
|
3366
|
+
if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3367
|
+
injectedBlocks.push(...injectedInstructions.splice(0));
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
else if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3371
|
+
injectedBlocks = injectedInstructions.splice(0);
|
|
3372
|
+
if (currentTurn !== undefined) {
|
|
3373
|
+
locals_js_1.locals.set(chatInstructionsConsumedKey, { turn: currentTurn, blocks: injectedBlocks });
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
if (injectedBlocks.length > 0) {
|
|
3377
|
+
const blocks = injectedBlocks;
|
|
3378
|
+
const injectedText = blocks
|
|
3379
|
+
.map((block) => (typeof block.content === "string" ? block.content : ""))
|
|
3380
|
+
.filter(Boolean)
|
|
3381
|
+
.join("\n\n");
|
|
3382
|
+
const base = result.system;
|
|
3383
|
+
if (base === undefined) {
|
|
3384
|
+
result.system = injectedText;
|
|
3385
|
+
}
|
|
3386
|
+
else if (typeof base === "string") {
|
|
3387
|
+
result.system = [base, injectedText].filter(Boolean).join("\n\n");
|
|
3388
|
+
}
|
|
3389
|
+
else {
|
|
3390
|
+
// Merged into the existing block rather than added as a second one. An array
|
|
3391
|
+
// of system blocks would keep the base block's cache entry, but ai@5 rejects
|
|
3392
|
+
// it outright ("Invalid prompt: system must be a string") while accepting a
|
|
3393
|
+
// single structured block, and this package's peer range still spans v5.
|
|
3394
|
+
// Choosing per version would mean resolving the installed version at runtime,
|
|
3395
|
+
// which is not something to build on: `import.meta.url` is illegal in this
|
|
3396
|
+
// package's CommonJS output, and a bundled task may have no resolvable `ai`
|
|
3397
|
+
// to read. One shape that works everywhere beats a cache hit.
|
|
3398
|
+
const baseBlock = base;
|
|
3399
|
+
result.system = {
|
|
3400
|
+
...baseBlock,
|
|
3401
|
+
content: [typeof baseBlock.content === "string" ? baseBlock.content : "", injectedText]
|
|
3402
|
+
.filter(Boolean)
|
|
3403
|
+
.join("\n\n"),
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3241
3407
|
// Prompt-related options (only if chat.prompt.set() was called)
|
|
3242
3408
|
if (prompt) {
|
|
3243
3409
|
// Resolve model via registry if both are present
|
|
@@ -3292,7 +3458,7 @@ function toStreamTextOptions(options) {
|
|
|
3292
3458
|
}
|
|
3293
3459
|
// 2. Pending message injection (steering)
|
|
3294
3460
|
if (taskPendingMessages) {
|
|
3295
|
-
const injected = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
|
|
3461
|
+
const { injected } = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
|
|
3296
3462
|
if (injected.length > 0) {
|
|
3297
3463
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
3298
3464
|
}
|
|
@@ -3302,18 +3468,89 @@ function toStreamTextOptions(options) {
|
|
|
3302
3468
|
if (bgQueue && bgQueue.length > 0) {
|
|
3303
3469
|
const injected = bgQueue.splice(0); // drain
|
|
3304
3470
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
3471
|
+
const pendingBackground = locals_js_1.locals.get(chatPendingBackgroundKey) ?? [];
|
|
3472
|
+
pendingBackground.push({
|
|
3473
|
+
afterId: locals_js_1.locals.get(chatCurrentUIMessagesKey)?.at(-1)?.id ?? "",
|
|
3474
|
+
messages: injected,
|
|
3475
|
+
});
|
|
3476
|
+
locals_js_1.locals.set(chatPendingBackgroundKey, pendingBackground);
|
|
3305
3477
|
}
|
|
3306
3478
|
return resultMessages ? { messages: resultMessages } : undefined;
|
|
3307
3479
|
};
|
|
3308
3480
|
}
|
|
3309
3481
|
return result;
|
|
3310
3482
|
}
|
|
3483
|
+
const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
|
|
3484
|
+
/**
|
|
3485
|
+
* Turn the current action into a turn.
|
|
3486
|
+
*
|
|
3487
|
+
* Return it from `onAction` after editing history. The action's own work is
|
|
3488
|
+
* finished first (the edit is applied and snapshotted), then a turn runs on the
|
|
3489
|
+
* result exactly as a message turn does: `onTurnStart`, `run()` with the edited
|
|
3490
|
+
* history, `onBeforeTurnComplete`, `onTurnComplete`, and the turn counter
|
|
3491
|
+
* advances. That gives the answer everything a turn has, the system prompt,
|
|
3492
|
+
* tools, steering, compaction, injected instructions and persistence, with no
|
|
3493
|
+
* action-specific handling.
|
|
3494
|
+
*
|
|
3495
|
+
* @example
|
|
3496
|
+
* ```ts
|
|
3497
|
+
* onAction: async ({ action }) => {
|
|
3498
|
+
* if (action.type === "regenerate") {
|
|
3499
|
+
* chat.history.slice(0, -1);
|
|
3500
|
+
* return chat.turn();
|
|
3501
|
+
* }
|
|
3502
|
+
* if (action.type === "undo") chat.history.slice(0, -2); // no turn
|
|
3503
|
+
* },
|
|
3504
|
+
* ```
|
|
3505
|
+
*/
|
|
3506
|
+
function chatTurn() {
|
|
3507
|
+
return { [actionTurnBrand]: true };
|
|
3508
|
+
}
|
|
3509
|
+
function isActionTurn(value) {
|
|
3510
|
+
return typeof value === "object" && value !== null && value[actionTurnBrand] === true;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Replace, in a model lane, the run of messages one UI message contributed.
|
|
3514
|
+
*
|
|
3515
|
+
* Used when a UI message is replaced in place (a tool-approval continuation
|
|
3516
|
+
* merging onto the trailing assistant, a captured response reusing an existing
|
|
3517
|
+
* id, a partial replacing an existing message). Reconverting the whole lane
|
|
3518
|
+
* from the UI lane would also replace a compaction summary with the full
|
|
3519
|
+
* transcript and drop the model forms `pendingMessages.prepare` produced.
|
|
3520
|
+
*
|
|
3521
|
+
* The replaced message is the trailing one, so its run is the lane's tail,
|
|
3522
|
+
* before any steer forms appended after it this turn (`tailAfter`). If the
|
|
3523
|
+
* tail does not match the old message's conversion, nothing is changed and
|
|
3524
|
+
* `false` is returned so the caller can fall back to a full reconversion.
|
|
3525
|
+
*/
|
|
3526
|
+
async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
|
|
3527
|
+
const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
|
|
3528
|
+
const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
|
|
3529
|
+
const end = lane.length - tailAfter;
|
|
3530
|
+
const start = end - oldRun.length;
|
|
3531
|
+
if (start < 0 || end > lane.length)
|
|
3532
|
+
return false;
|
|
3533
|
+
if (JSON.stringify(lane.slice(start, end)) !== JSON.stringify(oldRun))
|
|
3534
|
+
return false;
|
|
3535
|
+
lane.splice(start, oldRun.length, ...newRun);
|
|
3536
|
+
return true;
|
|
3537
|
+
}
|
|
3311
3538
|
function isUIMessageStreamable(value) {
|
|
3312
3539
|
return (typeof value === "object" &&
|
|
3313
3540
|
value !== null &&
|
|
3314
3541
|
"toUIMessageStream" in value &&
|
|
3315
3542
|
typeof value.toUIMessageStream === "function");
|
|
3316
3543
|
}
|
|
3544
|
+
const warnedHydrateMessagesDeprecated = new Set();
|
|
3545
|
+
function warnHydrateMessagesDeprecatedOnce(agentId) {
|
|
3546
|
+
if (warnedHydrateMessagesDeprecated.has(agentId))
|
|
3547
|
+
return;
|
|
3548
|
+
warnedHydrateMessagesDeprecated.add(agentId);
|
|
3549
|
+
console.warn(`[chat.agent] \`hydrateMessages\` on "${agentId}" is deprecated. Give the agent a transcript ` +
|
|
3550
|
+
"storage instead: `save` receives every change to the conversation and `loadContext` " +
|
|
3551
|
+
"lets the application own the model's context, with crash recovery and durable " +
|
|
3552
|
+
"compaction that `hydrateMessages` never had.");
|
|
3553
|
+
}
|
|
3317
3554
|
let warnedMissingOnAction = false;
|
|
3318
3555
|
function warnMissingOnActionOnce() {
|
|
3319
3556
|
if (warnedMissingOnAction)
|
|
@@ -3446,10 +3683,22 @@ function chatCustomAgent(options) {
|
|
|
3446
3683
|
await installChatInputRouter(payload.chatId, {
|
|
3447
3684
|
resuming: Boolean(payload.continuation),
|
|
3448
3685
|
});
|
|
3686
|
+
// A custom agent's loop is the customer's, so there is no exit site the
|
|
3687
|
+
// SDK controls. Perform a requested close when `run()` returns, whatever
|
|
3688
|
+
// shape the loop had. Idempotent, so the createSession iterator having
|
|
3689
|
+
// already closed on its own exit costs nothing.
|
|
3690
|
+
const withClose = async (result) => {
|
|
3691
|
+
try {
|
|
3692
|
+
return await result;
|
|
3693
|
+
}
|
|
3694
|
+
finally {
|
|
3695
|
+
await performChatClose();
|
|
3696
|
+
}
|
|
3697
|
+
};
|
|
3449
3698
|
// Keep the schema-free path identical to the original custom-agent
|
|
3450
3699
|
// wrapper, including when userRun starts executing.
|
|
3451
3700
|
if (!parseClientData) {
|
|
3452
|
-
return userRun(payload, runOptions);
|
|
3701
|
+
return withClose(userRun(payload, runOptions));
|
|
3453
3702
|
}
|
|
3454
3703
|
const isHandoverBoot = payload.trigger === "handover-prepare";
|
|
3455
3704
|
const isMessagelessBoot = payload.trigger === "preload" ||
|
|
@@ -3465,7 +3714,7 @@ function chatCustomAgent(options) {
|
|
|
3465
3714
|
writeErrorToStream: !isMessagelessBoot && !isHandoverBoot,
|
|
3466
3715
|
});
|
|
3467
3716
|
if (validated.ok) {
|
|
3468
|
-
return userRun(validated.payload, runOptions);
|
|
3717
|
+
return withClose(userRun(validated.payload, runOptions));
|
|
3469
3718
|
}
|
|
3470
3719
|
if (isHandoverBoot) {
|
|
3471
3720
|
const signal = await waitForHandover({
|
|
@@ -3501,7 +3750,7 @@ function chatCustomAgent(options) {
|
|
|
3501
3750
|
sessionId: next.output.sessionId ?? payload.sessionId,
|
|
3502
3751
|
idleTimeoutInSeconds: next.output.idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds,
|
|
3503
3752
|
};
|
|
3504
|
-
return userRun(recoveredPayload, runOptions);
|
|
3753
|
+
return withClose(userRun(recoveredPayload, runOptions));
|
|
3505
3754
|
},
|
|
3506
3755
|
});
|
|
3507
3756
|
// Register clientDataSchema so the CLI converts it to JSONSchema
|
|
@@ -3514,7 +3763,19 @@ function chatCustomAgent(options) {
|
|
|
3514
3763
|
return task;
|
|
3515
3764
|
}
|
|
3516
3765
|
function chatAgent(options) {
|
|
3517
|
-
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, ...restOptions } = options;
|
|
3766
|
+
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;
|
|
3767
|
+
if (hydrateMessages) {
|
|
3768
|
+
if (storage) {
|
|
3769
|
+
throw new Error(`chat.agent: "${options.id}" sets both \`hydrateMessages\` and \`storage\`. ` +
|
|
3770
|
+
"`hydrateMessages` is deprecated and replaced by the storage: `save` receives every " +
|
|
3771
|
+
"change and `loadContext` on the storage owns the model's context. Remove `hydrateMessages`.");
|
|
3772
|
+
}
|
|
3773
|
+
if (typeof (transcriptStorageOverride ?? transcriptStorage_js_1.defaultStorage).loadContext === "function") {
|
|
3774
|
+
throw new Error(`chat.agent: "${options.id}" sets \`hydrateMessages\` and uses a transcript storage with ` +
|
|
3775
|
+
"`loadContext`. Both would own the model's context; keep one.");
|
|
3776
|
+
}
|
|
3777
|
+
warnHydrateMessagesDeprecatedOnce(options.id);
|
|
3778
|
+
}
|
|
3518
3779
|
const parseClientData = clientDataSchema ? (0, v3_1.getSchemaParseFn)(clientDataSchema) : undefined;
|
|
3519
3780
|
const parseAction = actionSchema ? (0, v3_1.getSchemaParseFn)(actionSchema) : undefined;
|
|
3520
3781
|
// chat.agent does not expose generic retry options (see docstring on
|
|
@@ -3597,6 +3858,42 @@ function chatAgent(options) {
|
|
|
3597
3858
|
// durable snapshot + `session.out` replay (or `hydrateMessages` if
|
|
3598
3859
|
// registered) — the wire is delta-only now, no longer a seed.
|
|
3599
3860
|
let accumulatedMessages = [];
|
|
3861
|
+
/**
|
|
3862
|
+
* Give the model accumulator the background injections a step-boundary
|
|
3863
|
+
* drain handed to the model this turn, and record them for persistence.
|
|
3864
|
+
* Returns how many model messages were appended.
|
|
3865
|
+
*/
|
|
3866
|
+
const reconcilePendingBackground = () => {
|
|
3867
|
+
const pending = locals_js_1.locals.get(chatPendingBackgroundKey);
|
|
3868
|
+
if (!pending || pending.length === 0)
|
|
3869
|
+
return 0;
|
|
3870
|
+
locals_js_1.locals.set(chatPendingBackgroundKey, []);
|
|
3871
|
+
let appended = 0;
|
|
3872
|
+
for (const entry of pending) {
|
|
3873
|
+
accumulatedMessages.push(...entry.messages);
|
|
3874
|
+
laneInjections.push(entry);
|
|
3875
|
+
appended += entry.messages.length;
|
|
3876
|
+
}
|
|
3877
|
+
return appended;
|
|
3878
|
+
};
|
|
3879
|
+
/**
|
|
3880
|
+
* Give the model accumulator the steering messages a drain consumed,
|
|
3881
|
+
* in the form the model actually received. Appended, never reconverted
|
|
3882
|
+
* from the UI lane, so a model-only compaction summary survives. Called
|
|
3883
|
+
* on both the success and the error path, before the response or the
|
|
3884
|
+
* partial joins the lane, so the order stays steer-then-answer.
|
|
3885
|
+
*/
|
|
3886
|
+
const reconcilePendingSteer = (options) => {
|
|
3887
|
+
const pending = locals_js_1.locals.get(chatPendingSteerKey);
|
|
3888
|
+
if (!pending || pending.length === 0)
|
|
3889
|
+
return [];
|
|
3890
|
+
locals_js_1.locals.set(chatPendingSteerKey, []);
|
|
3891
|
+
for (const entry of pending) {
|
|
3892
|
+
accumulatedMessages.push(...entry.model);
|
|
3893
|
+
options?.turnNew?.push(...entry.model);
|
|
3894
|
+
}
|
|
3895
|
+
return pending;
|
|
3896
|
+
};
|
|
3600
3897
|
// Accumulated UI messages for persistence. Mirrors the model accumulator
|
|
3601
3898
|
// but in frontend-friendly UIMessage format (with parts, id, etc.).
|
|
3602
3899
|
let accumulatedUIMessages = [];
|
|
@@ -3614,7 +3911,156 @@ function chatAgent(options) {
|
|
|
3614
3911
|
// collectively cost ~600ms on every first-message TTFC. Both reads
|
|
3615
3912
|
// swallow errors internally; the agent stays available either way.
|
|
3616
3913
|
const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
|
|
3914
|
+
const transcriptStorage = storage ??
|
|
3915
|
+
transcriptStorageOverride ??
|
|
3916
|
+
transcriptStorage_js_1.defaultStorage;
|
|
3917
|
+
const storageLoadContext = transcriptStorage.loadContext?.bind(transcriptStorage);
|
|
3918
|
+
/**
|
|
3919
|
+
* Who supplies the model's context each turn: the deprecated
|
|
3920
|
+
* `hydrateMessages` hook, the storage's `loadContext`, or (undefined)
|
|
3921
|
+
* the runtime's own transcript.
|
|
3922
|
+
*/
|
|
3923
|
+
const loadContextHook = hydrateMessages
|
|
3924
|
+
? (event) => hydrateMessages(event)
|
|
3925
|
+
: storageLoadContext
|
|
3926
|
+
? (event) => storageLoadContext({ chatId: event.chatId, clientData: event.clientData }, event)
|
|
3927
|
+
: undefined;
|
|
3928
|
+
let transcriptShadow = (0, transcriptStorage_js_1.createTranscriptShadow)([]);
|
|
3929
|
+
let bootTranscriptState = null;
|
|
3930
|
+
/**
|
|
3931
|
+
* True while the model lane holds a compaction summary, so it cannot be
|
|
3932
|
+
* rebuilt from the transcript and has to be persisted as state. Reset
|
|
3933
|
+
* wherever the lane is reconverted from the UI lane.
|
|
3934
|
+
*/
|
|
3935
|
+
let laneCompacted = false;
|
|
3936
|
+
/** Conversational `chat.inject` messages in the lane, anchored to the transcript. */
|
|
3937
|
+
let laneInjections = [];
|
|
3938
|
+
let persistedStateSet = false;
|
|
3617
3939
|
let bootSnapshot;
|
|
3940
|
+
let bootClientData = payload.metadata;
|
|
3941
|
+
if (parseClientData) {
|
|
3942
|
+
try {
|
|
3943
|
+
bootClientData = await parseClientData(payload.metadata);
|
|
3944
|
+
}
|
|
3945
|
+
catch {
|
|
3946
|
+
bootClientData = payload.metadata;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* The `lastOutEventId` the most recent snapshot carried.
|
|
3951
|
+
*
|
|
3952
|
+
* A snapshot written outside a turn — after an action mutates history — has
|
|
3953
|
+
* no turn cursor of its own, and writing `undefined` there would drop the
|
|
3954
|
+
* resume point and make the next boot replay from further back. Retaining it
|
|
3955
|
+
* keeps an action's write cursor-neutral.
|
|
3956
|
+
*/
|
|
3957
|
+
let lastSnapshotOutEventId;
|
|
3958
|
+
const storageTrigger = (trigger) => trigger === "regenerate-message"
|
|
3959
|
+
? "regenerate-message"
|
|
3960
|
+
: trigger === "action" || trigger === "action-turn"
|
|
3961
|
+
? "action"
|
|
3962
|
+
: "submit-message";
|
|
3963
|
+
/**
|
|
3964
|
+
* Hand the runtime's view of the transcript to the storage as a
|
|
3965
|
+
* changeset: the diff against what was last saved, plus the cursors the
|
|
3966
|
+
* next boot resumes from. The shadow only advances when the save
|
|
3967
|
+
* succeeds, so a failed save is folded into the next changeset.
|
|
3968
|
+
*/
|
|
3969
|
+
/** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
|
|
3970
|
+
let transcriptState = null;
|
|
3971
|
+
const saveTranscript = async (opts) => {
|
|
3972
|
+
const { changes, shadow } = (0, transcriptStorage_js_1.diffTranscript)(transcriptShadow, opts.messages, {
|
|
3973
|
+
nonFinalIds: opts.nonFinalIds,
|
|
3974
|
+
});
|
|
3975
|
+
const throughId = opts.messages.at(-1)?.id ?? "";
|
|
3976
|
+
const queued = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
3977
|
+
const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
|
|
3978
|
+
? {
|
|
3979
|
+
v: 1,
|
|
3980
|
+
...(laneCompacted
|
|
3981
|
+
? {
|
|
3982
|
+
compaction: {
|
|
3983
|
+
modelMessages: accumulatedMessages,
|
|
3984
|
+
throughId,
|
|
3985
|
+
},
|
|
3986
|
+
}
|
|
3987
|
+
: {}),
|
|
3988
|
+
...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
|
|
3989
|
+
...(queued.length > 0 ? { queued: [...queued] } : {}),
|
|
3990
|
+
}
|
|
3991
|
+
: null;
|
|
3992
|
+
if (runtimeState !== null || persistedStateSet) {
|
|
3993
|
+
changes.push({ op: "state", value: runtimeState });
|
|
3994
|
+
}
|
|
3995
|
+
transcriptState = runtimeState;
|
|
3996
|
+
const inCursor = chatInputRouter().resumeFloor();
|
|
3997
|
+
await transcriptStorage.save({
|
|
3998
|
+
chatId: payload.chatId,
|
|
3999
|
+
clientData: opts.clientData,
|
|
4000
|
+
turn: opts.turn,
|
|
4001
|
+
trigger: opts.trigger,
|
|
4002
|
+
runId: ctx.run.id,
|
|
4003
|
+
ctx,
|
|
4004
|
+
}, {
|
|
4005
|
+
reason: opts.reason,
|
|
4006
|
+
changes,
|
|
4007
|
+
transcript: {
|
|
4008
|
+
entries: opts.messages.map((message) => ({
|
|
4009
|
+
id: message.id,
|
|
4010
|
+
final: !shadow.nonFinal.has(message.id),
|
|
4011
|
+
message,
|
|
4012
|
+
})),
|
|
4013
|
+
state: transcriptState,
|
|
4014
|
+
},
|
|
4015
|
+
cursors: {
|
|
4016
|
+
lastOutEventId: opts.lastOutEventId,
|
|
4017
|
+
lastInEventId: inCursor !== undefined ? String(inCursor) : undefined,
|
|
4018
|
+
},
|
|
4019
|
+
});
|
|
4020
|
+
transcriptShadow = shadow;
|
|
4021
|
+
persistedStateSet = runtimeState !== null;
|
|
4022
|
+
};
|
|
4023
|
+
/**
|
|
4024
|
+
* Persist the accumulator outside a turn.
|
|
4025
|
+
*
|
|
4026
|
+
* An action is not a turn, so it never reaches the turn-complete path where
|
|
4027
|
+
* the transcript is normally saved, but a `chat.history` mutation changes
|
|
4028
|
+
* the conversation and has to survive the run ending.
|
|
4029
|
+
*
|
|
4030
|
+
* Cursor-neutral: an action has no turn cursor of its own, and writing
|
|
4031
|
+
* `undefined` would drop the resume point the last turn established and make
|
|
4032
|
+
* the next boot replay from further back.
|
|
4033
|
+
*/
|
|
4034
|
+
const writeSnapshotOutsideTurn = async (reason, turnContext) => {
|
|
4035
|
+
if (hydrateMessages)
|
|
4036
|
+
return;
|
|
4037
|
+
try {
|
|
4038
|
+
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
4039
|
+
await saveTranscript({
|
|
4040
|
+
reason: "action",
|
|
4041
|
+
messages: accumulatedUIMessages,
|
|
4042
|
+
turn: turnContext.turn,
|
|
4043
|
+
trigger: "action",
|
|
4044
|
+
clientData: turnContext.clientData,
|
|
4045
|
+
lastOutEventId: lastSnapshotOutEventId,
|
|
4046
|
+
});
|
|
4047
|
+
}, {
|
|
4048
|
+
attributes: {
|
|
4049
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
4050
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
4051
|
+
"chat.snapshot.reason": reason,
|
|
4052
|
+
"chat.messages.count": accumulatedUIMessages.length,
|
|
4053
|
+
},
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
4056
|
+
catch (error) {
|
|
4057
|
+
v3_1.logger.warn("chat.agent: snapshot write outside a turn failed; the change may not survive a continuation", {
|
|
4058
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4059
|
+
sessionId: sessionIdForSnapshot,
|
|
4060
|
+
reason,
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
};
|
|
3618
4064
|
let replayedSettled = [];
|
|
3619
4065
|
let replayedPartial;
|
|
3620
4066
|
let replayedPartialRaw;
|
|
@@ -3624,13 +4070,20 @@ function chatAgent(options) {
|
|
|
3624
4070
|
// default, `inFlightUsers`). The turn-loop checks this queue ahead of
|
|
3625
4071
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
3626
4072
|
const bootInjectedQueue = [];
|
|
4073
|
+
const recoveredSeqByPayload = new WeakMap();
|
|
4074
|
+
const dispatchBootInjected = () => bootInjectedQueue.shift();
|
|
4075
|
+
const settleRecoveredTurn = (wirePayload) => {
|
|
4076
|
+
const settledSeq = recoveredSeqByPayload.get(wirePayload);
|
|
4077
|
+
if (settledSeq !== undefined)
|
|
4078
|
+
chatInputRouter().settleRecovered(settledSeq);
|
|
4079
|
+
};
|
|
3627
4080
|
const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
|
|
3628
4081
|
// `.in` resume cursor, computed at most once per boot. The boot
|
|
3629
4082
|
// block below resolves it (snapshot field or records scan) and the
|
|
3630
4083
|
// resume-cursor block reuses it instead of re-scanning.
|
|
3631
4084
|
let bootInCursor;
|
|
3632
4085
|
let bootInCursorResolved = false;
|
|
3633
|
-
if (
|
|
4086
|
+
if (couldHavePriorState) {
|
|
3634
4087
|
// Single parent span for the whole boot read phase — snapshot
|
|
3635
4088
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
3636
4089
|
// + result counts are attributes on the span.
|
|
@@ -3638,18 +4091,33 @@ function chatAgent(options) {
|
|
|
3638
4091
|
// snapshot read
|
|
3639
4092
|
const snapStart = Date.now();
|
|
3640
4093
|
try {
|
|
3641
|
-
|
|
4094
|
+
const loaded = hydrateMessages
|
|
4095
|
+
? undefined
|
|
4096
|
+
: await transcriptStorage.load({
|
|
4097
|
+
chatId: payload.chatId,
|
|
4098
|
+
clientData: bootClientData,
|
|
4099
|
+
});
|
|
4100
|
+
if (loaded) {
|
|
4101
|
+
transcriptShadow = (0, transcriptStorage_js_1.createTranscriptShadow)(loaded.messages, new Set(loaded.nonFinalIds ?? []));
|
|
4102
|
+
bootTranscriptState = loaded.state;
|
|
4103
|
+
transcriptState = loaded.state ?? null;
|
|
4104
|
+
persistedStateSet = loaded.state !== null && loaded.state !== undefined;
|
|
4105
|
+
bootSnapshot = {
|
|
4106
|
+
messages: loaded.messages,
|
|
4107
|
+
lastOutEventId: loaded.cursors?.lastOutEventId,
|
|
4108
|
+
lastInEventId: loaded.cursors?.lastInEventId,
|
|
4109
|
+
};
|
|
4110
|
+
}
|
|
3642
4111
|
}
|
|
3643
4112
|
catch (error) {
|
|
3644
|
-
|
|
3645
|
-
// is just belt-and-suspenders against tracer/span errors.
|
|
3646
|
-
v3_1.logger.warn("chat.agent: snapshot read failed; continuing without snapshot", {
|
|
4113
|
+
v3_1.logger.warn("chat.agent: transcript load failed; continuing from the stream tail", {
|
|
3647
4114
|
error: error instanceof Error ? error.message : String(error),
|
|
3648
4115
|
sessionId: sessionIdForSnapshot,
|
|
3649
4116
|
});
|
|
3650
4117
|
}
|
|
3651
4118
|
bootSpan.setAttribute("chat.boot.snapshot.durationMs", Date.now() - snapStart);
|
|
3652
|
-
bootSpan.setAttribute("chat.boot.snapshot.present",
|
|
4119
|
+
bootSpan.setAttribute("chat.boot.snapshot.present", bootSnapshot !== undefined &&
|
|
4120
|
+
(bootSnapshot.messages.length > 0 || bootSnapshot.lastOutEventId !== undefined));
|
|
3653
4121
|
bootSpan.setAttribute("chat.boot.snapshot.messageCount", bootSnapshot?.messages?.length ?? 0);
|
|
3654
4122
|
// Seed the trim chain from the snapshot's `lastOutEventId` (the SSE
|
|
3655
4123
|
// id of the previous turn's `turn-complete` control record). The
|
|
@@ -3657,6 +4125,7 @@ function chatAgent(options) {
|
|
|
3657
4125
|
// Without seeding, the new worker would emit no trim on its first
|
|
3658
4126
|
// turn (chain self-bootstraps from turn 2), so this is purely an
|
|
3659
4127
|
// optimization to keep continuation runs bounded from the first turn.
|
|
4128
|
+
lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
|
|
3660
4129
|
if (bootSnapshot?.lastOutEventId !== undefined) {
|
|
3661
4130
|
const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
|
|
3662
4131
|
if (Number.isFinite(seeded)) {
|
|
@@ -3748,16 +4217,14 @@ function chatAgent(options) {
|
|
|
3748
4217
|
}
|
|
3749
4218
|
// ── session.in router ──────────────────────────────────────────
|
|
3750
4219
|
//
|
|
3751
|
-
|
|
3752
|
-
// only a fallback: the boot block above may already have resolved a
|
|
3753
|
-
// cursor from the snapshot, which is used when the boundary itself
|
|
3754
|
-
// carries none.
|
|
4220
|
+
const recoveredSeqNums = replayedInTail.map((r) => r.seqNum);
|
|
3755
4221
|
await installChatInputRouter(payload.chatId, {
|
|
3756
4222
|
fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
|
|
4223
|
+
recoveredSeqNums,
|
|
3757
4224
|
resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
|
|
3758
4225
|
});
|
|
3759
4226
|
// ── Recovery boot + chain reconstruction ────────────────────────
|
|
3760
|
-
|
|
4227
|
+
{
|
|
3761
4228
|
const settledMessages = mergeByIdReplaceWins(bootSnapshot?.messages ?? [], replayedSettled);
|
|
3762
4229
|
const inFlightUsers = replayedInTail.map((r) => r.message);
|
|
3763
4230
|
const partialAssistant = replayedPartial;
|
|
@@ -3836,7 +4303,7 @@ function chatAgent(options) {
|
|
|
3836
4303
|
// branches: at n=1 the orphan partial is dropped and the interrupted
|
|
3837
4304
|
// user is re-dispatched as a fresh turn instead.
|
|
3838
4305
|
let seedChain;
|
|
3839
|
-
let
|
|
4306
|
+
let recoveredEntries;
|
|
3840
4307
|
if (hookChain !== undefined) {
|
|
3841
4308
|
seedChain = hookChain;
|
|
3842
4309
|
}
|
|
@@ -3847,13 +4314,26 @@ function chatAgent(options) {
|
|
|
3847
4314
|
seedChain = settledMessages;
|
|
3848
4315
|
}
|
|
3849
4316
|
if (hookRecoveredTurns !== undefined) {
|
|
3850
|
-
|
|
4317
|
+
const seqNumsByRecoveredId = new Map();
|
|
4318
|
+
for (const entry of replayedInTail) {
|
|
4319
|
+
const existing = seqNumsByRecoveredId.get(entry.message.id);
|
|
4320
|
+
if (existing)
|
|
4321
|
+
existing.push(entry.seqNum);
|
|
4322
|
+
else
|
|
4323
|
+
seqNumsByRecoveredId.set(entry.message.id, [entry.seqNum]);
|
|
4324
|
+
}
|
|
4325
|
+
recoveredEntries = hookRecoveredTurns.map((message) => ({
|
|
4326
|
+
message,
|
|
4327
|
+
seqNum: seqNumsByRecoveredId.get(message.id)?.shift(),
|
|
4328
|
+
}));
|
|
3851
4329
|
}
|
|
3852
4330
|
else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
|
|
3853
|
-
|
|
4331
|
+
recoveredEntries = replayedInTail
|
|
4332
|
+
.slice(1)
|
|
4333
|
+
.map((r) => ({ message: r.message, seqNum: r.seqNum }));
|
|
3854
4334
|
}
|
|
3855
4335
|
else {
|
|
3856
|
-
|
|
4336
|
+
recoveredEntries = replayedInTail.map((r) => ({ message: r.message, seqNum: r.seqNum }));
|
|
3857
4337
|
}
|
|
3858
4338
|
// `beforeBoot` errors bubble — the customer opted into blocking
|
|
3859
4339
|
// persistence and a failure there should fail the run rather than
|
|
@@ -3861,16 +4341,6 @@ function chatAgent(options) {
|
|
|
3861
4341
|
if (hookBeforeBoot) {
|
|
3862
4342
|
await hookBeforeBoot();
|
|
3863
4343
|
}
|
|
3864
|
-
// Advance the session.in cursor past every recovered user so
|
|
3865
|
-
// the live subscription doesn't re-deliver them.
|
|
3866
|
-
if (replayedInTail.length > 0) {
|
|
3867
|
-
const lastRecoveredSeq = replayedInTail[replayedInTail.length - 1].seqNum;
|
|
3868
|
-
const currentCursor = v3_1.sessionStreams.lastSeqNum(payload.chatId, "in");
|
|
3869
|
-
if (currentCursor === undefined || lastRecoveredSeq > currentCursor) {
|
|
3870
|
-
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", lastRecoveredSeq);
|
|
3871
|
-
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", lastRecoveredSeq);
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
4344
|
// Synthesize wire payloads for each recoveredTurn. The turn-loop
|
|
3875
4345
|
// pops these ahead of `messagesInput.waitWithIdleTimeout` so they
|
|
3876
4346
|
// dispatch as normal turns with the existing hook stack.
|
|
@@ -3893,13 +4363,14 @@ function chatAgent(options) {
|
|
|
3893
4363
|
for (const entry of replayedInTail) {
|
|
3894
4364
|
metadataById.set(entry.message.id, entry.metadata);
|
|
3895
4365
|
}
|
|
3896
|
-
|
|
4366
|
+
const dispatchedRecoveredSeqs = new Set();
|
|
4367
|
+
for (const { message: msg, seqNum } of recoveredEntries) {
|
|
3897
4368
|
if (wireMessageId && msg.id === wireMessageId)
|
|
3898
4369
|
continue;
|
|
3899
4370
|
const recoveredMetadata = metadataById.has(msg.id)
|
|
3900
4371
|
? metadataById.get(msg.id)
|
|
3901
4372
|
: payload.metadata;
|
|
3902
|
-
|
|
4373
|
+
const injectedPayload = {
|
|
3903
4374
|
chatId: payload.chatId,
|
|
3904
4375
|
sessionId: payload.sessionId,
|
|
3905
4376
|
metadata: recoveredMetadata,
|
|
@@ -3908,7 +4379,17 @@ function chatAgent(options) {
|
|
|
3908
4379
|
messageId: msg.id,
|
|
3909
4380
|
continuation: payload.continuation,
|
|
3910
4381
|
previousRunId: payload.previousRunId,
|
|
3911
|
-
}
|
|
4382
|
+
};
|
|
4383
|
+
bootInjectedQueue.push(injectedPayload);
|
|
4384
|
+
if (seqNum !== undefined) {
|
|
4385
|
+
recoveredSeqByPayload.set(injectedPayload, seqNum);
|
|
4386
|
+
dispatchedRecoveredSeqs.add(seqNum);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
for (const entry of replayedInTail) {
|
|
4390
|
+
if (!dispatchedRecoveredSeqs.has(entry.seqNum)) {
|
|
4391
|
+
chatInputRouter().settleRecovered(entry.seqNum);
|
|
4392
|
+
}
|
|
3912
4393
|
}
|
|
3913
4394
|
accumulatedUIMessages = seedChain;
|
|
3914
4395
|
// ── Head-start bootstrap ─────────────────────────────────────
|
|
@@ -3920,7 +4401,8 @@ function chatAgent(options) {
|
|
|
3920
4401
|
// path where wire-borne UIMessage[] still seeds the accumulator,
|
|
3921
4402
|
// and it's safe because the route handler isn't subject to the
|
|
3922
4403
|
// `/in/append` 512 KiB cap.
|
|
3923
|
-
if (
|
|
4404
|
+
if (!loadContextHook &&
|
|
4405
|
+
accumulatedUIMessages.length === 0 &&
|
|
3924
4406
|
payload.trigger === "handover-prepare" &&
|
|
3925
4407
|
Array.isArray(payload.headStartMessages) &&
|
|
3926
4408
|
payload.headStartMessages.length > 0) {
|
|
@@ -3949,7 +4431,17 @@ function chatAgent(options) {
|
|
|
3949
4431
|
}
|
|
3950
4432
|
}
|
|
3951
4433
|
try {
|
|
3952
|
-
|
|
4434
|
+
const bootRuntimeState = (0, transcriptStorage_js_1.parseTranscriptRuntimeState)(bootTranscriptState);
|
|
4435
|
+
const restored = await (0, transcriptStorage_js_1.restoreModelLane)(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
|
|
4436
|
+
accumulatedMessages = restored.messages;
|
|
4437
|
+
laneCompacted = restored.compacted;
|
|
4438
|
+
laneInjections = restored.injections;
|
|
4439
|
+
if (bootRuntimeState?.queued && bootRuntimeState.queued.length > 0) {
|
|
4440
|
+
locals_js_1.locals.set(chatBackgroundQueueKey, [
|
|
4441
|
+
...(locals_js_1.locals.get(chatBackgroundQueueKey) ?? []),
|
|
4442
|
+
...bootRuntimeState.queued,
|
|
4443
|
+
]);
|
|
4444
|
+
}
|
|
3953
4445
|
}
|
|
3954
4446
|
catch (error) {
|
|
3955
4447
|
v3_1.logger.warn("chat.agent: toModelMessages failed at boot; starting empty", {
|
|
@@ -3963,6 +4455,12 @@ function chatAgent(options) {
|
|
|
3963
4455
|
// before any hook (`onChatStart`, `onTurnStart`, etc.) fires.
|
|
3964
4456
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3965
4457
|
}
|
|
4458
|
+
locals_js_1.locals.set(chatAgentManagedConfigKey, {
|
|
4459
|
+
registry: promptRegistry,
|
|
4460
|
+
system: agentSystem,
|
|
4461
|
+
cacheControl: agentCacheControl,
|
|
4462
|
+
systemProviderOptions: agentSystemProviderOptions,
|
|
4463
|
+
});
|
|
3966
4464
|
// Token usage tracking across turns
|
|
3967
4465
|
let previousTurnUsage;
|
|
3968
4466
|
let cumulativeUsage = emptyUsage();
|
|
@@ -4051,7 +4549,7 @@ function chatAgent(options) {
|
|
|
4051
4549
|
*/
|
|
4052
4550
|
let dispatchedRecoveredFirstTurn = false;
|
|
4053
4551
|
if (preloaded && bootInjectedQueue.length > 0) {
|
|
4054
|
-
currentWirePayload =
|
|
4552
|
+
currentWirePayload = dispatchBootInjected();
|
|
4055
4553
|
dispatchedRecoveredFirstTurn = true;
|
|
4056
4554
|
}
|
|
4057
4555
|
// Handle preloaded runs — fire onPreload, then wait for the first real message
|
|
@@ -4259,7 +4757,7 @@ function chatAgent(options) {
|
|
|
4259
4757
|
// waiting on the live session.in. Subsequent recovered turns
|
|
4260
4758
|
// get drained by the end-of-turn picker below.
|
|
4261
4759
|
if (bootInjectedQueue.length > 0) {
|
|
4262
|
-
currentWirePayload =
|
|
4760
|
+
currentWirePayload = dispatchBootInjected();
|
|
4263
4761
|
}
|
|
4264
4762
|
else {
|
|
4265
4763
|
const effectiveIdleTimeout = idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds;
|
|
@@ -4320,6 +4818,7 @@ function chatAgent(options) {
|
|
|
4320
4818
|
} // end else (no boot-injected first turn)
|
|
4321
4819
|
}
|
|
4322
4820
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
4821
|
+
let turnClientData = payload.metadata;
|
|
4323
4822
|
// Declared here so the finally can detach it — a handler leaked past
|
|
4324
4823
|
// its turn duplicates every mid-stream message into the shared buffer.
|
|
4325
4824
|
let turnMsgSub;
|
|
@@ -4341,6 +4840,7 @@ function chatAgent(options) {
|
|
|
4341
4840
|
// `cleanupAbortedParts` no longer has to be re-applied below.
|
|
4342
4841
|
const cleanedIncomingMessages = incomingMessages.map((msg) => msg.role === "assistant" ? cleanupAbortedParts(msg) : msg);
|
|
4343
4842
|
const clientData = (parseClientData ? await parseClientData(wireMetadata) : wireMetadata);
|
|
4843
|
+
turnClientData = clientData;
|
|
4344
4844
|
const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages);
|
|
4345
4845
|
// Actions are not turns. They use a different span name
|
|
4346
4846
|
// and don't carry a turn.number. Branched on at `isAction`.
|
|
@@ -4377,6 +4877,7 @@ function chatAgent(options) {
|
|
|
4377
4877
|
locals_js_1.locals.set(chatDeferKey, new Set());
|
|
4378
4878
|
locals_js_1.locals.set(chatCompactionStateKey, undefined);
|
|
4379
4879
|
locals_js_1.locals.set(chatSteeringQueueKey, []);
|
|
4880
|
+
locals_js_1.locals.set(chatPendingBackgroundKey, []);
|
|
4380
4881
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
4381
4882
|
// NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
|
|
4382
4883
|
// by deferred work from the previous turn's onTurnComplete need to
|
|
@@ -4458,6 +4959,10 @@ function chatAgent(options) {
|
|
|
4458
4959
|
// Track new messages for this turn (user input + assistant response).
|
|
4459
4960
|
const turnNewModelMessages = [];
|
|
4460
4961
|
const turnNewUIMessages = [];
|
|
4962
|
+
locals_js_1.locals.set(chatTurnNewUIMessagesKey, turnNewUIMessages);
|
|
4963
|
+
// A head-start handover deliberately resumes from an assistant
|
|
4964
|
+
// message it spliced in, so it isn't a no-op turn.
|
|
4965
|
+
let splicedHandoverPartial = false;
|
|
4461
4966
|
// ── Action handling ──────────────────────────────────────
|
|
4462
4967
|
// Actions arrive on the same input stream but with
|
|
4463
4968
|
// trigger === "action". They are NOT turns — only
|
|
@@ -4468,16 +4973,25 @@ function chatAgent(options) {
|
|
|
4468
4973
|
// an action, return a `StreamTextResult` (auto-piped),
|
|
4469
4974
|
// string, or UIMessage from `onAction`. Turn counter
|
|
4470
4975
|
// does not advance.
|
|
4471
|
-
let
|
|
4976
|
+
let actionResult = undefined;
|
|
4977
|
+
/** Set when `onAction` returned `chat.turn()`: the turn block below runs. */
|
|
4978
|
+
let actionTurn = false;
|
|
4979
|
+
/**
|
|
4980
|
+
* Whether this action changed the conversation, by rolling history
|
|
4981
|
+
* back or by streaming a response. Drives the single snapshot write
|
|
4982
|
+
* at the end — an action never reaches the turn-complete path that
|
|
4983
|
+
* normally does it.
|
|
4984
|
+
*/
|
|
4985
|
+
let actionChangedHistory = false;
|
|
4472
4986
|
if (isAction) {
|
|
4473
4987
|
// Parse and validate the action payload
|
|
4474
4988
|
const parsedAction = parseAction
|
|
4475
4989
|
? await parseAction(currentWirePayload.action)
|
|
4476
4990
|
: currentWirePayload.action;
|
|
4477
4991
|
// Hydrate messages from backend if configured
|
|
4478
|
-
if (
|
|
4992
|
+
if (loadContextHook) {
|
|
4479
4993
|
const hydrated = await tracer_js_1.tracer.startActiveSpan("hydrateMessages()", async () => {
|
|
4480
|
-
return
|
|
4994
|
+
return loadContextHook({
|
|
4481
4995
|
chatId: currentWirePayload.chatId,
|
|
4482
4996
|
turn,
|
|
4483
4997
|
trigger: "action",
|
|
@@ -4497,12 +5011,14 @@ function chatAgent(options) {
|
|
|
4497
5011
|
});
|
|
4498
5012
|
accumulatedUIMessages = [...hydrated];
|
|
4499
5013
|
accumulatedMessages = await toModelMessages(hydrated);
|
|
5014
|
+
laneCompacted = false;
|
|
5015
|
+
laneInjections = [];
|
|
4500
5016
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4501
5017
|
}
|
|
4502
5018
|
// Fire onAction — handler may mutate state via
|
|
4503
5019
|
// `chat.history.*` and / or return a model response.
|
|
4504
5020
|
if (onAction) {
|
|
4505
|
-
|
|
5021
|
+
actionResult = await tracer_js_1.tracer.startActiveSpan("onAction()", async () => {
|
|
4506
5022
|
return await onAction({
|
|
4507
5023
|
action: parsedAction,
|
|
4508
5024
|
chatId: currentWirePayload.chatId,
|
|
@@ -4527,7 +5043,10 @@ function chatAgent(options) {
|
|
|
4527
5043
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
4528
5044
|
accumulatedUIMessages = [...actionOverride];
|
|
4529
5045
|
accumulatedMessages = await toModelMessages(actionOverride);
|
|
5046
|
+
laneCompacted = false;
|
|
5047
|
+
laneInjections = [];
|
|
4530
5048
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5049
|
+
actionChangedHistory = true;
|
|
4531
5050
|
}
|
|
4532
5051
|
}
|
|
4533
5052
|
else {
|
|
@@ -4548,7 +5067,7 @@ function chatAgent(options) {
|
|
|
4548
5067
|
// route handler's first-turn history to the hydrate hook as
|
|
4549
5068
|
// incoming messages instead (gated on the pending handover).
|
|
4550
5069
|
if (turn === 0 &&
|
|
4551
|
-
|
|
5070
|
+
loadContextHook &&
|
|
4552
5071
|
cleanedUIMessages.length === 0 &&
|
|
4553
5072
|
(locals_js_1.locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
|
|
4554
5073
|
Array.isArray(payload.headStartMessages) &&
|
|
@@ -4578,7 +5097,7 @@ function chatAgent(options) {
|
|
|
4578
5097
|
},
|
|
4579
5098
|
}));
|
|
4580
5099
|
}
|
|
4581
|
-
if (
|
|
5100
|
+
if (loadContextHook) {
|
|
4582
5101
|
// Snapshot the ids the accumulator knew BEFORE this
|
|
4583
5102
|
// turn ran — used below to decide whether an
|
|
4584
5103
|
// incoming wire message is genuinely new or just a
|
|
@@ -4596,7 +5115,7 @@ function chatAgent(options) {
|
|
|
4596
5115
|
// was always true for `submit-message` is now true for every
|
|
4597
5116
|
// trigger.
|
|
4598
5117
|
const hydrated = await tracer_js_1.tracer.startActiveSpan("hydrateMessages()", async () => {
|
|
4599
|
-
return
|
|
5118
|
+
return loadContextHook({
|
|
4600
5119
|
chatId: currentWirePayload.chatId,
|
|
4601
5120
|
turn,
|
|
4602
5121
|
trigger: currentWirePayload.trigger,
|
|
@@ -4635,6 +5154,8 @@ function chatAgent(options) {
|
|
|
4635
5154
|
}
|
|
4636
5155
|
accumulatedUIMessages = merged;
|
|
4637
5156
|
accumulatedMessages = await toModelMessages(merged);
|
|
5157
|
+
laneCompacted = false;
|
|
5158
|
+
laneInjections = [];
|
|
4638
5159
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4639
5160
|
// Track new messages for onTurnComplete.newUIMessages.
|
|
4640
5161
|
// Only push for genuinely new ids — HITL continuations
|
|
@@ -4680,6 +5201,8 @@ function chatAgent(options) {
|
|
|
4680
5201
|
accumulatedUIMessages.pop();
|
|
4681
5202
|
}
|
|
4682
5203
|
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5204
|
+
laneCompacted = false;
|
|
5205
|
+
laneInjections = [];
|
|
4683
5206
|
}
|
|
4684
5207
|
else if (cleanedUIMessages.length > 0) {
|
|
4685
5208
|
// Submit-message (and the special-cased
|
|
@@ -4696,6 +5219,7 @@ function chatAgent(options) {
|
|
|
4696
5219
|
// where AI SDK regenerates the id (TRI-9137) still
|
|
4697
5220
|
// applies via `rewriteIncomingIdViaToolCallMap`.
|
|
4698
5221
|
let replaced = false;
|
|
5222
|
+
const replacedPairs = [];
|
|
4699
5223
|
for (const raw of cleanedUIMessages) {
|
|
4700
5224
|
let incoming = raw;
|
|
4701
5225
|
let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
|
|
@@ -4707,7 +5231,9 @@ function chatAgent(options) {
|
|
|
4707
5231
|
}
|
|
4708
5232
|
}
|
|
4709
5233
|
if (idx !== -1) {
|
|
4710
|
-
|
|
5234
|
+
const previous = accumulatedUIMessages[idx];
|
|
5235
|
+
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(previous, incoming);
|
|
5236
|
+
replacedPairs.push({ previous, merged: accumulatedUIMessages[idx] });
|
|
4711
5237
|
replaced = true;
|
|
4712
5238
|
}
|
|
4713
5239
|
else {
|
|
@@ -4717,9 +5243,19 @@ function chatAgent(options) {
|
|
|
4717
5243
|
recordToolCallIdsFromMessage(incoming);
|
|
4718
5244
|
}
|
|
4719
5245
|
if (replaced) {
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
5246
|
+
let inPlace = true;
|
|
5247
|
+
for (const { previous, merged } of replacedPairs) {
|
|
5248
|
+
if (!(await replaceModelRun(accumulatedMessages, previous, merged, 0))) {
|
|
5249
|
+
inPlace = false;
|
|
5250
|
+
break;
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
if (!inPlace) {
|
|
5254
|
+
v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
|
|
5255
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5256
|
+
laneCompacted = false;
|
|
5257
|
+
laneInjections = [];
|
|
5258
|
+
}
|
|
4723
5259
|
}
|
|
4724
5260
|
else {
|
|
4725
5261
|
const incomingModelMessages = await toModelMessages(cleanedUIMessages);
|
|
@@ -4762,10 +5298,34 @@ function chatAgent(options) {
|
|
|
4762
5298
|
messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
|
|
4763
5299
|
});
|
|
4764
5300
|
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
5301
|
+
splicedHandoverPartial = true;
|
|
4765
5302
|
}
|
|
4766
5303
|
}
|
|
4767
5304
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4768
5305
|
} // end if (trigger !== "action")
|
|
5306
|
+
// ── No-op turn ──────────────────────────────────────────
|
|
5307
|
+
//
|
|
5308
|
+
// A submit that added no new user message and leaves the model
|
|
5309
|
+
// chain ending on an assistant message has nothing to answer —
|
|
5310
|
+
// calling the model would prefill its own last reply. Keyed on
|
|
5311
|
+
// the model tail, so a `tool`-terminated chain (a merged tool
|
|
5312
|
+
// approval) still runs.
|
|
5313
|
+
const isNoOpTurn = !isAction &&
|
|
5314
|
+
!splicedHandoverPartial &&
|
|
5315
|
+
currentWirePayload.trigger === "submit-message" &&
|
|
5316
|
+
turnNewUIMessages.length === 0 &&
|
|
5317
|
+
accumulatedMessages[accumulatedMessages.length - 1]?.role === "assistant";
|
|
5318
|
+
if (isNoOpTurn) {
|
|
5319
|
+
msgSub?.off();
|
|
5320
|
+
v3_1.logger.warn("chat.agent: turn added no new user message; skipping the model", {
|
|
5321
|
+
chatId: currentWirePayload.chatId,
|
|
5322
|
+
messageId: currentWirePayload.messageId,
|
|
5323
|
+
});
|
|
5324
|
+
settleRecoveredTurn(currentWirePayload);
|
|
5325
|
+
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5326
|
+
// Not a turn — don't consume an iteration.
|
|
5327
|
+
turn--;
|
|
5328
|
+
}
|
|
4769
5329
|
// ── Action result handling ──────────────────────────────
|
|
4770
5330
|
// For action turns, skip the turn machinery entirely.
|
|
4771
5331
|
// If `onAction` returned a stream / string / UIMessage,
|
|
@@ -4775,34 +5335,34 @@ function chatAgent(options) {
|
|
|
4775
5335
|
// The turn counter is decremented so the next iteration
|
|
4776
5336
|
// sees the same `turn` value — actions don't count.
|
|
4777
5337
|
if (isAction) {
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
|
|
4786
|
-
});
|
|
4787
|
-
await pipeChat(uiStream, {
|
|
4788
|
-
signal: combinedSignal,
|
|
4789
|
-
spanName: "stream response",
|
|
4790
|
-
});
|
|
5338
|
+
if (isActionTurn(actionResult)) {
|
|
5339
|
+
// Persist the edit before the turn starts, so a turn that is
|
|
5340
|
+
// cancelled or runs out of memory continues from the edited
|
|
5341
|
+
// history rather than from the snapshot the edit replaced.
|
|
5342
|
+
// The turn then does its own hooks, completion and snapshot.
|
|
5343
|
+
if (actionChangedHistory) {
|
|
5344
|
+
await writeSnapshotOutsideTurn("action", { turn, clientData });
|
|
4791
5345
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
5346
|
+
actionTurn = true;
|
|
5347
|
+
}
|
|
5348
|
+
else if (actionResult !== undefined) {
|
|
5349
|
+
throw new Error("chat.agent: onAction returned a value. An action is a state edit; to answer " +
|
|
5350
|
+
"after the edit, return chat.turn() and a turn runs on the edited history. " +
|
|
5351
|
+
"Returning a StreamTextResult, string or UIMessage is no longer supported.");
|
|
5352
|
+
}
|
|
5353
|
+
else {
|
|
5354
|
+
msgSub?.off();
|
|
5355
|
+
if (actionChangedHistory) {
|
|
5356
|
+
await writeSnapshotOutsideTurn("action", { turn, clientData });
|
|
4799
5357
|
}
|
|
5358
|
+
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5359
|
+
// Don't consume a turn iteration — actions aren't turns.
|
|
5360
|
+
turn--;
|
|
4800
5361
|
}
|
|
4801
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4802
|
-
// Don't consume a turn iteration — actions aren't turns.
|
|
4803
|
-
turn--;
|
|
4804
5362
|
}
|
|
4805
|
-
|
|
5363
|
+
// A no-op turn skips this block, and with it `followSessionPin`:
|
|
5364
|
+
// there is nothing to answer, so nothing to hand over.
|
|
5365
|
+
if ((!isAction || actionTurn) && !isNoOpTurn) {
|
|
4806
5366
|
// Mint a scoped public access token once per turn, reused for
|
|
4807
5367
|
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
4808
5368
|
const currentRunId = ctx.run.id;
|
|
@@ -4891,6 +5451,8 @@ function chatAgent(options) {
|
|
|
4891
5451
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
4892
5452
|
accumulatedUIMessages = [...turnStartOverride];
|
|
4893
5453
|
accumulatedMessages = await toModelMessages(turnStartOverride);
|
|
5454
|
+
laneCompacted = false;
|
|
5455
|
+
laneInjections = [];
|
|
4894
5456
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4895
5457
|
}
|
|
4896
5458
|
}, {
|
|
@@ -4907,9 +5469,11 @@ function chatAgent(options) {
|
|
|
4907
5469
|
},
|
|
4908
5470
|
});
|
|
4909
5471
|
}
|
|
5472
|
+
await followSessionPin(currentWirePayload.chatId, versionSkew);
|
|
4910
5473
|
// chat.requestUpgrade() called in onTurnStart (or onValidateMessages) —
|
|
4911
|
-
// skip run() and
|
|
4912
|
-
//
|
|
5474
|
+
// skip run() and hand over to a fresh run on the new version. The
|
|
5475
|
+
// successor picks the message up off session.in; the transport only
|
|
5476
|
+
// keeps reading.
|
|
4913
5477
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
4914
5478
|
await writeUpgradeRequiredChunk();
|
|
4915
5479
|
return "exit";
|
|
@@ -4949,7 +5513,12 @@ function chatAgent(options) {
|
|
|
4949
5513
|
const lastAccumulated = accumulatedMessages[accumulatedMessages.length - 1];
|
|
4950
5514
|
const bgQueue = locals_js_1.locals.get(chatBackgroundQueueKey);
|
|
4951
5515
|
if (bgQueue && bgQueue.length > 0 && lastAccumulated?.role !== "tool") {
|
|
4952
|
-
|
|
5516
|
+
const injected = bgQueue.splice(0);
|
|
5517
|
+
accumulatedMessages.push(...injected);
|
|
5518
|
+
laneInjections.push({
|
|
5519
|
+
afterId: accumulatedUIMessages.at(-1)?.id ?? "",
|
|
5520
|
+
messages: injected,
|
|
5521
|
+
});
|
|
4953
5522
|
}
|
|
4954
5523
|
if (isHeadStartFinalTurn) {
|
|
4955
5524
|
// The synthesized partial UIMessage IS the response.
|
|
@@ -4968,6 +5537,9 @@ function chatAgent(options) {
|
|
|
4968
5537
|
const preparedMessages = await applyPrepareMessages(accumulatedMessages, "run");
|
|
4969
5538
|
runResult = await userRun({
|
|
4970
5539
|
...restWire,
|
|
5540
|
+
// A turn requested by chat.turn() is not the action itself:
|
|
5541
|
+
// a run() that short-circuits on "action" must still answer.
|
|
5542
|
+
...(actionTurn ? { trigger: "action-turn" } : {}),
|
|
4971
5543
|
messages: preparedMessages,
|
|
4972
5544
|
clientData,
|
|
4973
5545
|
continuation,
|
|
@@ -4980,6 +5552,7 @@ function chatAgent(options) {
|
|
|
4980
5552
|
signal: combinedSignal,
|
|
4981
5553
|
cancelSignal,
|
|
4982
5554
|
stopSignal,
|
|
5555
|
+
streamText: createBoundStreamText(promptRegistry, agentSystem, agentCacheControl, agentSystemProviderOptions),
|
|
4983
5556
|
});
|
|
4984
5557
|
}
|
|
4985
5558
|
// Auto-pipe if the run function returned a StreamTextResult or similar,
|
|
@@ -5093,7 +5666,21 @@ function chatAgent(options) {
|
|
|
5093
5666
|
if (runOverride) {
|
|
5094
5667
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5095
5668
|
accumulatedUIMessages = [...runOverride];
|
|
5096
|
-
|
|
5669
|
+
/**
|
|
5670
|
+
* Steers the drain consumed are left out of the rebuild and
|
|
5671
|
+
* appended by the reconciliation below instead, so the lane
|
|
5672
|
+
* gets the form the model actually received rather than a
|
|
5673
|
+
* reconversion of the UI message, and gets it once. A steer
|
|
5674
|
+
* the edit removed is dropped from the pending list too, so
|
|
5675
|
+
* the edit is honoured.
|
|
5676
|
+
*/
|
|
5677
|
+
const overrideIds = new Set(runOverride.map((m) => m.id));
|
|
5678
|
+
const pending = (locals_js_1.locals.get(chatPendingSteerKey) ?? []).filter((e) => overrideIds.has(e.ui.id));
|
|
5679
|
+
locals_js_1.locals.set(chatPendingSteerKey, pending);
|
|
5680
|
+
const pendingIds = new Set(pending.map((e) => e.ui.id));
|
|
5681
|
+
accumulatedMessages = await toModelMessages(runOverride.filter((m) => !pendingIds.has(m.id)));
|
|
5682
|
+
laneCompacted = false;
|
|
5683
|
+
laneInjections = [];
|
|
5097
5684
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5098
5685
|
}
|
|
5099
5686
|
// Check if compaction set a model-only override (preserves UI messages).
|
|
@@ -5116,6 +5703,8 @@ function chatAgent(options) {
|
|
|
5116
5703
|
accumulatedMessages = taskCompactionConfig?.compactModelMessages
|
|
5117
5704
|
? await taskCompactionConfig.compactModelMessages(compactEvent)
|
|
5118
5705
|
: modelOnlyOverride;
|
|
5706
|
+
laneCompacted = true;
|
|
5707
|
+
laneInjections = [];
|
|
5119
5708
|
// Apply UI messages: callback or default (preserve all)
|
|
5120
5709
|
if (taskCompactionConfig?.compactUIMessages) {
|
|
5121
5710
|
accumulatedUIMessages = (await taskCompactionConfig.compactUIMessages(compactEvent));
|
|
@@ -5123,10 +5712,20 @@ function chatAgent(options) {
|
|
|
5123
5712
|
}
|
|
5124
5713
|
// Determine if the user stopped generation this turn (not a full run cancel).
|
|
5125
5714
|
const wasStopped = stopController.signal.aborted && !runSignal.aborted;
|
|
5715
|
+
// Give the model accumulator the steering messages the drain
|
|
5716
|
+
// consumed. Appended, never reconverted from the UI lane, so a
|
|
5717
|
+
// model-only compaction summary set just above survives; and done
|
|
5718
|
+
// before the response is appended so the order stays
|
|
5719
|
+
// steer-then-answer. Outside the `capturedResponseMessage`
|
|
5720
|
+
// branches below, so a turn that captured no response is covered.
|
|
5721
|
+
const steerTailThisTurn = reconcilePendingSteer({
|
|
5722
|
+
turnNew: turnNewModelMessages,
|
|
5723
|
+
}).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
|
|
5126
5724
|
// Append the assistant's response (partial or complete) to the accumulator.
|
|
5127
5725
|
// The onFinish callback fires even on abort/stop, so partial responses
|
|
5128
5726
|
// from stopped generation are captured correctly.
|
|
5129
5727
|
let rawResponseMessage;
|
|
5728
|
+
let responseWasSkipped = false;
|
|
5130
5729
|
if (capturedResponseMessage) {
|
|
5131
5730
|
// Keep the raw message before cleanup for users who want custom handling
|
|
5132
5731
|
rawResponseMessage = capturedResponseMessage;
|
|
@@ -5152,43 +5751,56 @@ function chatAgent(options) {
|
|
|
5152
5751
|
};
|
|
5153
5752
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
5154
5753
|
}
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
else {
|
|
5167
|
-
accumulatedUIMessages.push(capturedResponseMessage);
|
|
5168
|
-
}
|
|
5169
|
-
turnNewUIMessages.push(capturedResponseMessage);
|
|
5170
|
-
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5171
|
-
// Record toolCallId → head messageId so a HITL
|
|
5172
|
-
// continuation next turn can recover the head id
|
|
5173
|
-
// even if the AI SDK regenerates it. See
|
|
5174
|
-
// `chatToolCallToMessageIdKey` for the full
|
|
5175
|
-
// rationale (TRI-9137).
|
|
5176
|
-
recordToolCallIdsFromMessage(capturedResponseMessage);
|
|
5177
|
-
try {
|
|
5178
|
-
const responseModelMessages = await toModelMessages([
|
|
5179
|
-
stripProviderMetadata(capturedResponseMessage),
|
|
5180
|
-
]);
|
|
5754
|
+
const responseHasContent = capturedResponseMessage.parts.some((part) => part.type !== "step-start");
|
|
5755
|
+
if (responseHasContent) {
|
|
5756
|
+
// Tool-approval continuations: the AI SDK reuses the trailing
|
|
5757
|
+
// assistant's ID (via originalMessages) so the captured response
|
|
5758
|
+
// carries the same ID as an existing message. Replace in place
|
|
5759
|
+
// instead of pushing a duplicate. For action turns this never
|
|
5760
|
+
// matches because originalMessages is omitted (fresh ID).
|
|
5761
|
+
const existingIdx = capturedResponseMessage.id
|
|
5762
|
+
? accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id)
|
|
5763
|
+
: -1;
|
|
5764
|
+
const previousAtIdx = existingIdx !== -1 ? accumulatedUIMessages[existingIdx] : undefined;
|
|
5181
5765
|
if (existingIdx !== -1) {
|
|
5182
|
-
|
|
5183
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5766
|
+
accumulatedUIMessages[existingIdx] = capturedResponseMessage;
|
|
5184
5767
|
}
|
|
5185
5768
|
else {
|
|
5186
|
-
|
|
5769
|
+
accumulatedUIMessages.push(capturedResponseMessage);
|
|
5770
|
+
}
|
|
5771
|
+
turnNewUIMessages.push(capturedResponseMessage);
|
|
5772
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5773
|
+
// Record toolCallId → head messageId so a HITL
|
|
5774
|
+
// continuation next turn can recover the head id
|
|
5775
|
+
// even if the AI SDK regenerates it. See
|
|
5776
|
+
// `chatToolCallToMessageIdKey` for the full
|
|
5777
|
+
// rationale (TRI-9137).
|
|
5778
|
+
recordToolCallIdsFromMessage(capturedResponseMessage);
|
|
5779
|
+
try {
|
|
5780
|
+
const responseModelMessages = await toModelMessages([
|
|
5781
|
+
stripProviderMetadata(capturedResponseMessage),
|
|
5782
|
+
]);
|
|
5783
|
+
if (existingIdx !== -1) {
|
|
5784
|
+
const ok = previousAtIdx !== undefined &&
|
|
5785
|
+
(await replaceModelRun(accumulatedMessages, previousAtIdx, capturedResponseMessage, steerTailThisTurn));
|
|
5786
|
+
if (!ok) {
|
|
5787
|
+
v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
|
|
5788
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5789
|
+
laneCompacted = false;
|
|
5790
|
+
laneInjections = [];
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
else {
|
|
5794
|
+
accumulatedMessages.push(...responseModelMessages);
|
|
5795
|
+
}
|
|
5796
|
+
turnNewModelMessages.push(...responseModelMessages);
|
|
5797
|
+
}
|
|
5798
|
+
catch {
|
|
5799
|
+
// Conversion failed — skip accumulation for this turn
|
|
5187
5800
|
}
|
|
5188
|
-
turnNewModelMessages.push(...responseModelMessages);
|
|
5189
5801
|
}
|
|
5190
|
-
|
|
5191
|
-
|
|
5802
|
+
else {
|
|
5803
|
+
responseWasSkipped = true;
|
|
5192
5804
|
}
|
|
5193
5805
|
}
|
|
5194
5806
|
// If there's no captured response (manual pipe mode) but there are
|
|
@@ -5288,6 +5900,8 @@ function chatAgent(options) {
|
|
|
5288
5900
|
],
|
|
5289
5901
|
},
|
|
5290
5902
|
];
|
|
5903
|
+
laneCompacted = true;
|
|
5904
|
+
laneInjections = [];
|
|
5291
5905
|
// UI messages: callback or default (preserve all)
|
|
5292
5906
|
if (outerCompaction.compactUIMessages) {
|
|
5293
5907
|
accumulatedUIMessages = (await outerCompaction.compactUIMessages(outerCompactEvent));
|
|
@@ -5377,6 +5991,8 @@ function chatAgent(options) {
|
|
|
5377
5991
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5378
5992
|
accumulatedUIMessages = [...override];
|
|
5379
5993
|
accumulatedMessages = await toModelMessages(override);
|
|
5994
|
+
laneCompacted = false;
|
|
5995
|
+
laneInjections = [];
|
|
5380
5996
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5381
5997
|
// Update event so onTurnComplete sees compacted messages
|
|
5382
5998
|
turnCompleteEvent.messages = accumulatedMessages;
|
|
@@ -5405,9 +6021,23 @@ function chatAgent(options) {
|
|
|
5405
6021
|
capturedPartialResponse = capturedResponseMessage;
|
|
5406
6022
|
turnCompleteEvent.responseMessage = capturedResponseMessage;
|
|
5407
6023
|
turnCompleteEvent.uiMessages = accumulatedUIMessages;
|
|
6024
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
6025
|
+
}
|
|
6026
|
+
else if (responseWasSkipped) {
|
|
6027
|
+
capturedResponseMessage = {
|
|
6028
|
+
...capturedResponseMessage,
|
|
6029
|
+
parts: [...(capturedResponseMessage.parts ?? []), ...lateParts],
|
|
6030
|
+
};
|
|
6031
|
+
accumulatedUIMessages.push(capturedResponseMessage);
|
|
6032
|
+
turnNewUIMessages.push(capturedResponseMessage);
|
|
6033
|
+
capturedPartialResponse = capturedResponseMessage;
|
|
6034
|
+
turnCompleteEvent.responseMessage = capturedResponseMessage;
|
|
6035
|
+
turnCompleteEvent.uiMessages = accumulatedUIMessages;
|
|
6036
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5408
6037
|
}
|
|
5409
6038
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
5410
6039
|
}
|
|
6040
|
+
settleRecoveredTurn(currentWirePayload);
|
|
5411
6041
|
// Write turn-complete control chunk — closes the frontend stream.
|
|
5412
6042
|
const turnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId, turnAccessToken);
|
|
5413
6043
|
// Fire onTurnComplete — stream is closed, use for persistence.
|
|
@@ -5423,6 +6053,8 @@ function chatAgent(options) {
|
|
|
5423
6053
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5424
6054
|
accumulatedUIMessages = [...turnCompleteOverride];
|
|
5425
6055
|
accumulatedMessages = await toModelMessages(turnCompleteOverride);
|
|
6056
|
+
laneCompacted = false;
|
|
6057
|
+
laneInjections = [];
|
|
5426
6058
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5427
6059
|
}
|
|
5428
6060
|
}, {
|
|
@@ -5476,13 +6108,18 @@ function chatAgent(options) {
|
|
|
5476
6108
|
if (!hydrateMessages) {
|
|
5477
6109
|
try {
|
|
5478
6110
|
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
6111
|
+
lastSnapshotOutEventId =
|
|
6112
|
+
turnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
|
|
6113
|
+
await saveTranscript({
|
|
6114
|
+
reason: "turn-complete",
|
|
5483
6115
|
messages: accumulatedUIMessages,
|
|
5484
|
-
|
|
5485
|
-
|
|
6116
|
+
turn,
|
|
6117
|
+
trigger: storageTrigger(currentWirePayload.trigger),
|
|
6118
|
+
clientData,
|
|
6119
|
+
lastOutEventId: lastSnapshotOutEventId,
|
|
6120
|
+
nonFinalIds: wasStopped && capturedResponseMessage?.id
|
|
6121
|
+
? new Set([capturedResponseMessage.id])
|
|
6122
|
+
: undefined,
|
|
5486
6123
|
});
|
|
5487
6124
|
}, {
|
|
5488
6125
|
attributes: {
|
|
@@ -5512,13 +6149,20 @@ function chatAgent(options) {
|
|
|
5512
6149
|
// produced these from in-flight user messages on session.in
|
|
5513
6150
|
// that the dead predecessor never acknowledged.
|
|
5514
6151
|
if (bootInjectedQueue.length > 0) {
|
|
5515
|
-
currentWirePayload =
|
|
6152
|
+
currentWirePayload = dispatchBootInjected();
|
|
5516
6153
|
return "continue";
|
|
5517
6154
|
}
|
|
5518
|
-
// chat.requestUpgrade() was called — exit the loop
|
|
5519
|
-
//
|
|
6155
|
+
// chat.requestUpgrade() was called — exit the loop; the handover
|
|
6156
|
+
// has already triggered a new run on the latest version.
|
|
5520
6157
|
// chat.endRun() — same exit, no upgrade semantics.
|
|
6158
|
+
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
6159
|
+
await performChatClose();
|
|
6160
|
+
return "exit";
|
|
6161
|
+
}
|
|
5521
6162
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
6163
|
+
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
6164
|
+
await persistUpgradeHandoff();
|
|
6165
|
+
}
|
|
5522
6166
|
return "exit";
|
|
5523
6167
|
}
|
|
5524
6168
|
// Wait for the next message — stay idle briefly, then suspend
|
|
@@ -5620,11 +6264,17 @@ function chatAgent(options) {
|
|
|
5620
6264
|
let errorTurnCompleteResult;
|
|
5621
6265
|
try {
|
|
5622
6266
|
await withChatWriter(async (writer) => {
|
|
5623
|
-
const errorText = turnError instanceof Error
|
|
6267
|
+
const errorText = turnError instanceof Error && turnError.message
|
|
6268
|
+
? turnError.message
|
|
6269
|
+
: "An unexpected error occurred";
|
|
5624
6270
|
writer.write({ type: "error", errorText });
|
|
5625
6271
|
});
|
|
5626
6272
|
// Signal turn complete so the client knows this turn is done
|
|
5627
6273
|
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
6274
|
+
// A later action's snapshot reuses this cursor, so it has to move
|
|
6275
|
+
// here too or that snapshot resumes from before the failed turn.
|
|
6276
|
+
lastSnapshotOutEventId =
|
|
6277
|
+
errorTurnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
|
|
5628
6278
|
}
|
|
5629
6279
|
catch {
|
|
5630
6280
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
@@ -5669,15 +6319,47 @@ function chatAgent(options) {
|
|
|
5669
6319
|
: partialIdx === -1
|
|
5670
6320
|
? [...erroredUIMessages, partialResponse]
|
|
5671
6321
|
: erroredUIMessages.map((m, i) => i === partialIdx ? partialResponse : m);
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
6322
|
+
/**
|
|
6323
|
+
* Seeded from the per-turn list, not just the wire message and the
|
|
6324
|
+
* partial, so a steering message the drain consumed is reported too.
|
|
6325
|
+
* An app persisting from `newUIMessages` would otherwise lose the
|
|
6326
|
+
* instruction whenever the turn it steered went on to fail.
|
|
6327
|
+
*/
|
|
6328
|
+
const buildErroredNew = () => {
|
|
6329
|
+
const out = [];
|
|
6330
|
+
const addUnique = (m) => {
|
|
6331
|
+
if (m && !out.some((existing) => existing.id === m.id))
|
|
6332
|
+
out.push(m);
|
|
6333
|
+
};
|
|
6334
|
+
addUnique(erroredWireMessage);
|
|
6335
|
+
for (const m of (locals_js_1.locals.get(chatTurnNewUIMessagesKey) ?? [])) {
|
|
6336
|
+
addUnique(m);
|
|
6337
|
+
}
|
|
6338
|
+
if (includePartial)
|
|
6339
|
+
addUnique(partialResponse);
|
|
6340
|
+
return out;
|
|
6341
|
+
};
|
|
6342
|
+
let erroredNewUIMessages = buildErroredNew();
|
|
5676
6343
|
let erroredNewModelMessages = [];
|
|
6344
|
+
const reconciledSteer = reconcilePendingSteer();
|
|
6345
|
+
const backgroundTailThisTurn = reconcilePendingBackground();
|
|
5677
6346
|
if (!responseCommitted) {
|
|
5678
6347
|
try {
|
|
5679
6348
|
if (erroredNewUIMessages.length > 0) {
|
|
5680
|
-
|
|
6349
|
+
/**
|
|
6350
|
+
* Built in order from the recorded forms rather than by
|
|
6351
|
+
* converting the UI list, so a steer appears in the delta as
|
|
6352
|
+
* the model received it (what `prepare` produced), matching the
|
|
6353
|
+
* lane. The wire message and partial are converted as before.
|
|
6354
|
+
*/
|
|
6355
|
+
const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
|
|
6356
|
+
for (const m of erroredNewUIMessages) {
|
|
6357
|
+
const recorded = steerModelById.get(m.id);
|
|
6358
|
+
if (recorded)
|
|
6359
|
+
erroredNewModelMessages.push(...recorded);
|
|
6360
|
+
else
|
|
6361
|
+
erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
|
|
6362
|
+
}
|
|
5681
6363
|
}
|
|
5682
6364
|
if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
|
|
5683
6365
|
if (partialIdx === -1) {
|
|
@@ -5685,7 +6367,14 @@ function chatAgent(options) {
|
|
|
5685
6367
|
accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
|
|
5686
6368
|
}
|
|
5687
6369
|
else {
|
|
5688
|
-
|
|
6370
|
+
const ok = await replaceModelRun(accumulatedMessages, erroredUIMessages[partialIdx], partialResponse, reconciledSteer.reduce((n, e) => n + e.model.length, 0) +
|
|
6371
|
+
backgroundTailThisTurn);
|
|
6372
|
+
if (!ok) {
|
|
6373
|
+
v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
|
|
6374
|
+
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6375
|
+
laneCompacted = false;
|
|
6376
|
+
laneInjections = [];
|
|
6377
|
+
}
|
|
5689
6378
|
}
|
|
5690
6379
|
accumulatedUIMessages = erroredUIMessagesWithPartial;
|
|
5691
6380
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -5694,7 +6383,7 @@ function chatAgent(options) {
|
|
|
5694
6383
|
catch {
|
|
5695
6384
|
erroredNewModelMessages = [];
|
|
5696
6385
|
erroredUIMessagesWithPartial = erroredUIMessages;
|
|
5697
|
-
erroredNewUIMessages =
|
|
6386
|
+
erroredNewUIMessages = buildErroredNew().filter((m) => m !== partialResponse);
|
|
5698
6387
|
}
|
|
5699
6388
|
}
|
|
5700
6389
|
if (onTurnComplete) {
|
|
@@ -5748,13 +6437,14 @@ function chatAgent(options) {
|
|
|
5748
6437
|
// neither the snapshot nor the replayable `.in` tail.
|
|
5749
6438
|
if (!hydrateMessages) {
|
|
5750
6439
|
try {
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
version: 1,
|
|
5754
|
-
savedAt: Date.now(),
|
|
6440
|
+
await saveTranscript({
|
|
6441
|
+
reason: "turn-error",
|
|
5755
6442
|
messages: erroredUIMessagesWithPartial,
|
|
5756
|
-
|
|
5757
|
-
|
|
6443
|
+
turn,
|
|
6444
|
+
trigger: storageTrigger(currentWirePayload.trigger),
|
|
6445
|
+
clientData: turnClientData,
|
|
6446
|
+
lastOutEventId: lastSnapshotOutEventId,
|
|
6447
|
+
nonFinalIds: includePartial && partialResponse ? new Set([partialResponse.id]) : undefined,
|
|
5758
6448
|
});
|
|
5759
6449
|
}
|
|
5760
6450
|
catch (error) {
|
|
@@ -5764,15 +6454,22 @@ function chatAgent(options) {
|
|
|
5764
6454
|
});
|
|
5765
6455
|
}
|
|
5766
6456
|
}
|
|
6457
|
+
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
6458
|
+
await performChatClose();
|
|
6459
|
+
return;
|
|
6460
|
+
}
|
|
5767
6461
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
5768
6462
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
6463
|
+
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
6464
|
+
await persistUpgradeHandoff();
|
|
6465
|
+
}
|
|
5769
6466
|
return;
|
|
5770
6467
|
}
|
|
5771
6468
|
// Drain remaining recovered turns before idling — a thrown
|
|
5772
6469
|
// recovered turn shouldn't strand the rest of the boot queue
|
|
5773
6470
|
// until an unrelated live message arrives.
|
|
5774
6471
|
if (bootInjectedQueue.length > 0) {
|
|
5775
|
-
currentWirePayload =
|
|
6472
|
+
currentWirePayload = dispatchBootInjected();
|
|
5776
6473
|
continue;
|
|
5777
6474
|
}
|
|
5778
6475
|
// Wait for the next message — same as after a successful turn
|
|
@@ -5788,6 +6485,12 @@ function chatAgent(options) {
|
|
|
5788
6485
|
return; // Timed out — end run gracefully
|
|
5789
6486
|
}
|
|
5790
6487
|
currentWirePayload = next.output;
|
|
6488
|
+
// Same close check the success path makes. Without it a close
|
|
6489
|
+
// record that lands after a failed turn is consumed as if it were
|
|
6490
|
+
// a turn payload, and the loop runs on against a closed session.
|
|
6491
|
+
if (currentWirePayload.trigger === "close") {
|
|
6492
|
+
return;
|
|
6493
|
+
}
|
|
5791
6494
|
// Continue to next iteration of the for loop
|
|
5792
6495
|
}
|
|
5793
6496
|
finally {
|
|
@@ -5796,6 +6499,11 @@ function chatAgent(options) {
|
|
|
5796
6499
|
}
|
|
5797
6500
|
}
|
|
5798
6501
|
finally {
|
|
6502
|
+
// Safety net for a close requested on a path that exits without
|
|
6503
|
+
// reaching one of the loop's close checks (a turn timeout, an OOM
|
|
6504
|
+
// re-throw). `performChatClose` is idempotent, so the ordinary path
|
|
6505
|
+
// having already run it costs nothing here.
|
|
6506
|
+
await performChatClose();
|
|
5799
6507
|
// `stopSub` is registered post-preload so the close-during-preload
|
|
5800
6508
|
// early-return path may exit before it ever attached. Guard the
|
|
5801
6509
|
// cleanup so a missing subscription doesn't throw.
|
|
@@ -6133,15 +6841,22 @@ function isStopped() {
|
|
|
6133
6841
|
// Version upgrade
|
|
6134
6842
|
// ---------------------------------------------------------------------------
|
|
6135
6843
|
/**
|
|
6136
|
-
*
|
|
6137
|
-
*
|
|
6844
|
+
* Hand the conversation over to another deployment.
|
|
6845
|
+
*
|
|
6846
|
+
* The handover happens immediately and server-side: a successor run is created
|
|
6847
|
+
* and picks the conversation up from `session.in`. The transport keeps reading
|
|
6848
|
+
* the same session output, so no client action is needed and nothing waits for
|
|
6849
|
+
* the next message.
|
|
6850
|
+
*
|
|
6851
|
+
* Without a target the session's pin is cleared, so the successor lands on the
|
|
6852
|
+
* latest deployed version; with `externalDeploymentId` the session is re-pinned
|
|
6853
|
+
* to that deployment.
|
|
6138
6854
|
*
|
|
6139
6855
|
* When called from `onTurnStart` or `onValidateMessages`, `run()` is skipped
|
|
6140
|
-
* entirely
|
|
6141
|
-
* same message on the new version.
|
|
6856
|
+
* entirely and the successor answers the message that opened the turn.
|
|
6142
6857
|
*
|
|
6143
6858
|
* When called from `run()` or `chat.defer()`, the current turn completes
|
|
6144
|
-
* normally and the
|
|
6859
|
+
* normally and the handover happens afterward.
|
|
6145
6860
|
*
|
|
6146
6861
|
* Call from `onTurnStart`, `onValidateMessages`, `onChatResume`, `run()`,
|
|
6147
6862
|
* or inside `chat.defer()`.
|
|
@@ -6161,8 +6876,37 @@ function isStopped() {
|
|
|
6161
6876
|
* });
|
|
6162
6877
|
* ```
|
|
6163
6878
|
*/
|
|
6164
|
-
function requestUpgrade() {
|
|
6879
|
+
function requestUpgrade(options) {
|
|
6165
6880
|
locals_js_1.locals.set(chatUpgradeRequestedKey, true);
|
|
6881
|
+
// Without a target the handoff clears the session's pin; with one it re-pins to that deployment.
|
|
6882
|
+
const target = options?.externalDeploymentId?.trim();
|
|
6883
|
+
if (target)
|
|
6884
|
+
locals_js_1.locals.set(chatUpgradeExternalDeploymentIdKey, target);
|
|
6885
|
+
}
|
|
6886
|
+
/** @internal Requests a handoff when the session's pin no longer names this deployment. */
|
|
6887
|
+
async function followSessionPin(chatId, policy) {
|
|
6888
|
+
if (!chatId) {
|
|
6889
|
+
return;
|
|
6890
|
+
}
|
|
6891
|
+
const deployedExternalId = locals_js_1.locals.get(chatAgentRunContextKey)?.deployment?.externalId;
|
|
6892
|
+
if (policy !== "hold" && !deployedExternalId) {
|
|
6893
|
+
v3_1.logger.debug("chat.versionSkew: cannot follow the session pin", {
|
|
6894
|
+
chatId,
|
|
6895
|
+
reason: "the run context carries no deployment.externalId",
|
|
6896
|
+
});
|
|
6897
|
+
}
|
|
6898
|
+
const target = await (0, chatVersionSkew_js_1.resolvePinToFollow)({
|
|
6899
|
+
policy,
|
|
6900
|
+
deployedExternalId,
|
|
6901
|
+
upgradeAlreadyRequested: locals_js_1.locals.get(chatUpgradeRequestedKey) === true,
|
|
6902
|
+
readPin: async () => (await sessions_js_1.sessions.retrieve(chatId, { retry: { maxAttempts: 2, randomize: true } }))
|
|
6903
|
+
.triggerConfig,
|
|
6904
|
+
});
|
|
6905
|
+
if (!target) {
|
|
6906
|
+
return;
|
|
6907
|
+
}
|
|
6908
|
+
v3_1.logger.info("chat.versionSkew: following the session pin", { chatId, target });
|
|
6909
|
+
requestUpgrade({ externalDeploymentId: target });
|
|
6166
6910
|
}
|
|
6167
6911
|
/**
|
|
6168
6912
|
* Hand off the current custom agent Session to a fresh run.
|
|
@@ -6201,20 +6945,31 @@ async function endAndContinue() {
|
|
|
6201
6945
|
if ((locals_js_1.locals.get(chatActiveSessionIteratorsKey) ?? 0) > 0) {
|
|
6202
6946
|
throw new Error("chat.endAndContinue() cannot be called while a chat.createSession() iterator is active. Close the iterator, then call chat.endAndContinue().");
|
|
6203
6947
|
}
|
|
6204
|
-
await performEndAndContinue();
|
|
6948
|
+
await performEndAndContinue({ reason: "continuation" });
|
|
6205
6949
|
}
|
|
6206
6950
|
/** @internal Shared server handoff used by managed and custom agent loops. */
|
|
6207
|
-
async function performEndAndContinue() {
|
|
6951
|
+
async function performEndAndContinue(options) {
|
|
6208
6952
|
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
6209
6953
|
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
6210
6954
|
if (!chatId || !callingRunId) {
|
|
6211
6955
|
throw new Error("Cannot end and continue without an active chat agent run");
|
|
6212
6956
|
}
|
|
6957
|
+
const externalDeploymentId = options.externalDeploymentId;
|
|
6213
6958
|
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
6214
|
-
await apiClient.endAndContinueSession(chatId, {
|
|
6959
|
+
const result = await apiClient.endAndContinueSession(chatId, {
|
|
6215
6960
|
callingRunId,
|
|
6216
|
-
reason:
|
|
6961
|
+
reason: options.reason,
|
|
6962
|
+
...(externalDeploymentId ? { externalDeploymentId } : {}),
|
|
6217
6963
|
});
|
|
6964
|
+
if (result?.pendingVersion !== true) {
|
|
6965
|
+
return;
|
|
6966
|
+
}
|
|
6967
|
+
// The successor parked. Say so on `.out` while this run still can — the transport's
|
|
6968
|
+
// subscription survives the swap, so the client learns without waiting for its next send.
|
|
6969
|
+
const [error] = await (0, v3_1.tryCatch)(getChatSession().out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION));
|
|
6970
|
+
if (error) {
|
|
6971
|
+
v3_1.logger.warn("could not signal a parked handoff", { chatId, error });
|
|
6972
|
+
}
|
|
6218
6973
|
}
|
|
6219
6974
|
/**
|
|
6220
6975
|
* Exit the run after the current turn completes, without waiting for the
|
|
@@ -6245,6 +7000,124 @@ async function performEndAndContinue() {
|
|
|
6245
7000
|
function endRun() {
|
|
6246
7001
|
locals_js_1.locals.set(chatEndRunRequestedKey, true);
|
|
6247
7002
|
}
|
|
7003
|
+
/**
|
|
7004
|
+
* End the whole conversation, permanently. The session row is closed, further
|
|
7005
|
+
* appends are refused, and the run exits without scheduling a continuation.
|
|
7006
|
+
*
|
|
7007
|
+
* This is the session-level stop. {@link endRun} ends the current run and lets
|
|
7008
|
+
* the next message start a fresh one; `chat.close()` ends the session itself,
|
|
7009
|
+
* so there is no next message. Use it for a budget cap, a completed goal,
|
|
7010
|
+
* abuse detection, or a user signing out.
|
|
7011
|
+
*
|
|
7012
|
+
* In a `chat.agent`, call it from `run()`, `prepareStep`, or
|
|
7013
|
+
* `onBeforeTurnComplete`. Called mid-step, it aborts the in-flight
|
|
7014
|
+
* `streamText` the same way the stop signal does, so the partial response is
|
|
7015
|
+
* still captured and streamed. The turn then completes normally, a terminal
|
|
7016
|
+
* `session-closed` record carrying `reason` is written to the response stream,
|
|
7017
|
+
* and the loop exits.
|
|
7018
|
+
*
|
|
7019
|
+
* Prefer `onBeforeTurnComplete` over `onTurnComplete`. It carries the same
|
|
7020
|
+
* fields but still runs while the stream is open, so the closed state rides
|
|
7021
|
+
* out on the turn's final record. `onTurnComplete` runs after that record, so
|
|
7022
|
+
* a close decided there does not reach a reader that has already finished the
|
|
7023
|
+
* turn, and the user only finds out when their next message is refused.
|
|
7024
|
+
*
|
|
7025
|
+
* In a `chat.customAgent`, call it anywhere in your own loop. The close is
|
|
7026
|
+
* performed when `run()` returns, so it lands whether you break out of a
|
|
7027
|
+
* `chat.createSession` loop, return early, or hand-roll the loop entirely.
|
|
7028
|
+
*
|
|
7029
|
+
* Closing is one-way: a closed session cannot be reopened. Its transcript
|
|
7030
|
+
* stays readable.
|
|
7031
|
+
*
|
|
7032
|
+
* @example
|
|
7033
|
+
* ```ts
|
|
7034
|
+
* chat.agent({
|
|
7035
|
+
* id: "budgeted-agent",
|
|
7036
|
+
* onBeforeTurnComplete: async ({ usage }) => {
|
|
7037
|
+
* if (await overBudget(usage)) {
|
|
7038
|
+
* chat.close({ reason: "Monthly budget reached" });
|
|
7039
|
+
* }
|
|
7040
|
+
* },
|
|
7041
|
+
* });
|
|
7042
|
+
* ```
|
|
7043
|
+
*/
|
|
7044
|
+
function close(options) {
|
|
7045
|
+
if (!locals_js_1.locals.get(chatExternalIdKey)) {
|
|
7046
|
+
throw new Error("chat.close() can only be called from inside a chat.agent() or chat.customAgent() run");
|
|
7047
|
+
}
|
|
7048
|
+
// Bound the reason once, here. It goes out on S2 record headers as well as
|
|
7049
|
+
// the close API, and an oversized value would fail the turn-complete write
|
|
7050
|
+
// that carries the turn boundary, costing the client far more than the
|
|
7051
|
+
// reason text.
|
|
7052
|
+
// Trailing high surrogate: the cut landed between the two halves of an
|
|
7053
|
+
// astral character, and encoding the orphan to UTF-8 for a record header
|
|
7054
|
+
// yields a replacement character. Drop it rather than ship mojibake.
|
|
7055
|
+
const reason = options?.reason
|
|
7056
|
+
?.slice(0, CHAT_CLOSE_REASON_MAX_LENGTH)
|
|
7057
|
+
.replace(/[\uD800-\uDBFF]$/, "");
|
|
7058
|
+
locals_js_1.locals.set(chatCloseRequestedKey, reason ? { reason } : {});
|
|
7059
|
+
// Mid-step call: unblock the in-flight streamText exactly like the stop
|
|
7060
|
+
// signal, so the turn can reach its turn boundary instead of running the
|
|
7061
|
+
// model out to completion after the decision to close has been made.
|
|
7062
|
+
locals_js_1.locals.get(chatStopControllerKey)?.abort(reason ?? "closed");
|
|
7063
|
+
}
|
|
7064
|
+
/**
|
|
7065
|
+
* @internal Terminal close sequence, run once at whichever exit site observes
|
|
7066
|
+
* the close request. Writes the standalone `session-closed` record, then closes
|
|
7067
|
+
* the session row.
|
|
7068
|
+
*
|
|
7069
|
+
* The record lands after the turn's `turn-complete`, so a client reading that
|
|
7070
|
+
* turn's stream has already terminated on it and will not see this one. It is
|
|
7071
|
+
* there for a reconnect and for replay. What a live client reads is the
|
|
7072
|
+
* `session-closed` header stamped onto `turn-complete` itself by
|
|
7073
|
+
* `writeTurnCompleteChunk`, which fires whenever the close was decided before
|
|
7074
|
+
* the turn ended. A close decided from `onTurnComplete` is past that point, so
|
|
7075
|
+
* the client learns from the 409 on its next send.
|
|
7076
|
+
*/
|
|
7077
|
+
async function performChatClose() {
|
|
7078
|
+
const request = locals_js_1.locals.get(chatCloseRequestedKey);
|
|
7079
|
+
if (!request || locals_js_1.locals.get(chatClosePerformedKey))
|
|
7080
|
+
return;
|
|
7081
|
+
const reason = request.reason;
|
|
7082
|
+
// Two flags, not one. The record is a client-visible event and must not be
|
|
7083
|
+
// written twice, but the row close is the part that actually ends the
|
|
7084
|
+
// conversation: flagging it as done before it succeeds would let a transient
|
|
7085
|
+
// failure leave the session open with no later call willing to retry.
|
|
7086
|
+
if (!locals_js_1.locals.get(chatCloseRecordWrittenKey)) {
|
|
7087
|
+
locals_js_1.locals.set(chatCloseRecordWrittenKey, true);
|
|
7088
|
+
try {
|
|
7089
|
+
const session = getChatSession();
|
|
7090
|
+
await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED, reason ? [[v3_1.SESSION_CLOSED_REASON_HEADER, reason]] : undefined);
|
|
7091
|
+
}
|
|
7092
|
+
catch (error) {
|
|
7093
|
+
v3_1.logger.warn("chat.close: failed to write the session-closed record", {
|
|
7094
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7095
|
+
});
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
7099
|
+
if (!chatId)
|
|
7100
|
+
return;
|
|
7101
|
+
try {
|
|
7102
|
+
await sessions_js_1.sessions.close(chatId, {
|
|
7103
|
+
...(reason ? { reason } : {}),
|
|
7104
|
+
...(locals_js_1.locals.get(chatAgentRunContextKey)?.run.id
|
|
7105
|
+
? { callingRunId: locals_js_1.locals.get(chatAgentRunContextKey).run.id }
|
|
7106
|
+
: {}),
|
|
7107
|
+
});
|
|
7108
|
+
locals_js_1.locals.set(chatClosePerformedKey, true);
|
|
7109
|
+
}
|
|
7110
|
+
catch (error) {
|
|
7111
|
+
// Deliberately NOT flagged as performed: the close API is idempotent, so a
|
|
7112
|
+
// later exit site on this run gets to retry it. Losing every retry to a
|
|
7113
|
+
// transient failure would leave the row open and the conversation alive.
|
|
7114
|
+
// Non-fatal either way — the run still exits.
|
|
7115
|
+
v3_1.logger.error("chat.close: failed to close the session", {
|
|
7116
|
+
chatId,
|
|
7117
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7118
|
+
});
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
6248
7121
|
// ---------------------------------------------------------------------------
|
|
6249
7122
|
// Per-turn deferred work
|
|
6250
7123
|
// ---------------------------------------------------------------------------
|
|
@@ -6306,9 +7179,18 @@ function chatDefer(promiseOrFn) {
|
|
|
6306
7179
|
* ```
|
|
6307
7180
|
*/
|
|
6308
7181
|
function injectBackgroundContext(messages) {
|
|
6309
|
-
const
|
|
6310
|
-
|
|
6311
|
-
|
|
7182
|
+
const systemBlocks = messages.filter((message) => message.role === "system");
|
|
7183
|
+
const conversational = messages.filter((message) => message.role !== "system");
|
|
7184
|
+
if (systemBlocks.length > 0) {
|
|
7185
|
+
const instructions = locals_js_1.locals.get(chatInjectedInstructionsKey) ?? [];
|
|
7186
|
+
instructions.push(...systemBlocks);
|
|
7187
|
+
locals_js_1.locals.set(chatInjectedInstructionsKey, instructions);
|
|
7188
|
+
}
|
|
7189
|
+
if (conversational.length > 0) {
|
|
7190
|
+
const queue = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
7191
|
+
queue.push(...conversational);
|
|
7192
|
+
locals_js_1.locals.set(chatBackgroundQueueKey, queue);
|
|
7193
|
+
}
|
|
6312
7194
|
}
|
|
6313
7195
|
// ---------------------------------------------------------------------------
|
|
6314
7196
|
// Aborted message cleanup
|
|
@@ -6721,10 +7603,12 @@ class ChatMessageAccumulator {
|
|
|
6721
7603
|
// a duplicate, mirroring the chat.agent accumulator.
|
|
6722
7604
|
const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
|
|
6723
7605
|
if (existingIdx !== -1) {
|
|
7606
|
+
const previous = this.uiMessages[existingIdx];
|
|
6724
7607
|
this.uiMessages[existingIdx] = response;
|
|
6725
7608
|
try {
|
|
6726
|
-
|
|
6727
|
-
|
|
7609
|
+
if (!(await replaceModelRun(this.modelMessages, previous, response, 0))) {
|
|
7610
|
+
this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
|
|
7611
|
+
}
|
|
6728
7612
|
}
|
|
6729
7613
|
catch {
|
|
6730
7614
|
// Conversion failed — leave the existing model messages in place
|
|
@@ -6760,6 +7644,28 @@ class ChatMessageAccumulator {
|
|
|
6760
7644
|
const modelMsgs = await toModelMessages([message]);
|
|
6761
7645
|
this._steeringQueue.push({ uiMessage: message, modelMessages: modelMsgs });
|
|
6762
7646
|
}
|
|
7647
|
+
/**
|
|
7648
|
+
* Record the messages a steering drain consumed.
|
|
7649
|
+
*
|
|
7650
|
+
* The drain only puts them in this step's prompt, so without this they
|
|
7651
|
+
* shape one answer and then exist in neither lane: not in `uiMessages`,
|
|
7652
|
+
* which is what an app persists from, and not in `modelMessages`, which is
|
|
7653
|
+
* what every later turn sends.
|
|
7654
|
+
*
|
|
7655
|
+
* Both lanes are appended to. The model lane is never reconverted from the
|
|
7656
|
+
* UI lane, because `compactIfNeeded` replaces it with a summary and leaves
|
|
7657
|
+
* the UI lane whole: a reconversion would restore everything the summary
|
|
7658
|
+
* replaced.
|
|
7659
|
+
*/
|
|
7660
|
+
async absorbSteering(claimed, injected) {
|
|
7661
|
+
const fresh = claimed.filter((m) => !this.uiMessages.some((e) => e.id === m.id));
|
|
7662
|
+
if (fresh.length === 0)
|
|
7663
|
+
return;
|
|
7664
|
+
this.uiMessages.push(...fresh);
|
|
7665
|
+
// Record what the model received. Only when the whole batch is new is
|
|
7666
|
+
// `injected` known to describe exactly these messages.
|
|
7667
|
+
this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
|
|
7668
|
+
}
|
|
6763
7669
|
/**
|
|
6764
7670
|
* Get and clear unconsumed steering messages.
|
|
6765
7671
|
*/
|
|
@@ -6792,7 +7698,8 @@ class ChatMessageAccumulator {
|
|
|
6792
7698
|
}
|
|
6793
7699
|
// 2. Pending message injection
|
|
6794
7700
|
if (pm && queue.length > 0) {
|
|
6795
|
-
const injected = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
|
|
7701
|
+
const { injected, claimed } = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
|
|
7702
|
+
await this.absorbSteering(claimed, injected);
|
|
6796
7703
|
if (injected.length > 0) {
|
|
6797
7704
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
6798
7705
|
}
|
|
@@ -6981,7 +7888,7 @@ function trackActiveChatSessionIterator(iterator) {
|
|
|
6981
7888
|
* ```
|
|
6982
7889
|
*/
|
|
6983
7890
|
function createChatSession(payload, options) {
|
|
6984
|
-
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, } = options;
|
|
7891
|
+
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, versionSkew: sessionVersionSkew, } = options;
|
|
6985
7892
|
const idleTimeoutInSeconds = sessionIdleTimeoutOpt ?? 30;
|
|
6986
7893
|
return {
|
|
6987
7894
|
[Symbol.asyncIterator]() {
|
|
@@ -7070,8 +7977,16 @@ function createChatSession(payload, options) {
|
|
|
7070
7977
|
* without suspending.
|
|
7071
7978
|
*/
|
|
7072
7979
|
if (turn > 0) {
|
|
7980
|
+
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
7981
|
+
await performChatClose();
|
|
7982
|
+
stop.cleanup();
|
|
7983
|
+
return { done: true, value: undefined };
|
|
7984
|
+
}
|
|
7073
7985
|
// chat.requestUpgrade() / chat.endRun() — exit before waiting
|
|
7074
7986
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
7987
|
+
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
7988
|
+
await persistUpgradeHandoff();
|
|
7989
|
+
}
|
|
7075
7990
|
stop.cleanup();
|
|
7076
7991
|
return { done: true, value: undefined };
|
|
7077
7992
|
}
|
|
@@ -7174,6 +8089,7 @@ function createChatSession(payload, options) {
|
|
|
7174
8089
|
}
|
|
7175
8090
|
accumulator.applyHandover(pendingHandoverSignal);
|
|
7176
8091
|
}
|
|
8092
|
+
await followSessionPin(currentPayload.chatId, sessionVersionSkew);
|
|
7177
8093
|
// chat.requestUpgrade() called before this turn — signal transport and exit
|
|
7178
8094
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
7179
8095
|
await writeUpgradeRequiredChunk();
|
|
@@ -7390,7 +8306,8 @@ function createChatSession(payload, options) {
|
|
|
7390
8306
|
}
|
|
7391
8307
|
}
|
|
7392
8308
|
if (sessionPendingMessages) {
|
|
7393
|
-
const injected = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
|
|
8309
|
+
const { injected, claimed } = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
|
|
8310
|
+
await accumulator.absorbSteering(claimed, injected);
|
|
7394
8311
|
if (injected.length > 0) {
|
|
7395
8312
|
resultMessages = [...(resultMessages ?? stepMsgs), ...injected];
|
|
7396
8313
|
}
|
|
@@ -7404,6 +8321,11 @@ function createChatSession(payload, options) {
|
|
|
7404
8321
|
async return() {
|
|
7405
8322
|
activeMsgSub?.off();
|
|
7406
8323
|
activeMsgSub = undefined;
|
|
8324
|
+
// Reached when the consumer leaves the `for await` early (`break`,
|
|
8325
|
+
// `return`, a throw). A `chat.close()` from the loop body would
|
|
8326
|
+
// otherwise be dropped: the exit that performs it lives in `next()`,
|
|
8327
|
+
// and `next()` is never called again.
|
|
8328
|
+
await performChatClose();
|
|
7407
8329
|
// `stop` only exists once next() has booted the iterator.
|
|
7408
8330
|
stop?.cleanup();
|
|
7409
8331
|
return { done: true, value: undefined };
|
|
@@ -7660,6 +8582,11 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
7660
8582
|
const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
|
|
7661
8583
|
const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
|
|
7662
8584
|
const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
|
|
8585
|
+
// Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
|
|
8586
|
+
// action default, which neither truthiness nor `??` would allow.
|
|
8587
|
+
const externalDeploymentId = params.triggerConfig?.externalDeploymentId !== undefined
|
|
8588
|
+
? params.triggerConfig.externalDeploymentId
|
|
8589
|
+
: options?.triggerConfig?.externalDeploymentId;
|
|
7663
8590
|
const triggerConfig = {
|
|
7664
8591
|
basePayload: {
|
|
7665
8592
|
messages: [],
|
|
@@ -7686,6 +8613,10 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
7686
8613
|
lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
|
|
7687
8614
|
}
|
|
7688
8615
|
: {}),
|
|
8616
|
+
...(params.triggerConfig?.ttl !== undefined || options?.triggerConfig?.ttl !== undefined
|
|
8617
|
+
? { ttl: params.triggerConfig?.ttl ?? options?.triggerConfig?.ttl }
|
|
8618
|
+
: {}),
|
|
8619
|
+
...(externalDeploymentId !== undefined ? { externalDeploymentId } : {}),
|
|
7689
8620
|
...(idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds } : {}),
|
|
7690
8621
|
};
|
|
7691
8622
|
const startBody = {
|
|
@@ -7730,6 +8661,7 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
7730
8661
|
publicAccessToken,
|
|
7731
8662
|
runId: created.runId,
|
|
7732
8663
|
sessionId: created.id,
|
|
8664
|
+
...(created.pendingVersion ? { pendingVersion: true } : {}),
|
|
7733
8665
|
};
|
|
7734
8666
|
};
|
|
7735
8667
|
}
|
|
@@ -7763,7 +8695,8 @@ async function callSessionsCreateWithOverride(args) {
|
|
|
7763
8695
|
const init = {
|
|
7764
8696
|
method: "POST",
|
|
7765
8697
|
headers: overrideRequestHeaders(accessToken),
|
|
7766
|
-
|
|
8698
|
+
// This path bypasses `sessions.start`, so it resolves the pin itself.
|
|
8699
|
+
body: JSON.stringify((0, externalDeploymentId_js_1.withResolvedExternalDeploymentId)(args.body)),
|
|
7767
8700
|
};
|
|
7768
8701
|
const response = args.fetchOverride
|
|
7769
8702
|
? await args.fetchOverride(url, init, ctx)
|
|
@@ -7828,6 +8761,41 @@ async function mintPublicTokenWithOverride(args) {
|
|
|
7828
8761
|
expirationTime: args.expirationTime,
|
|
7829
8762
|
});
|
|
7830
8763
|
}
|
|
8764
|
+
/**
|
|
8765
|
+
* Creates a server-side helper that reads a conversation from a transcript
|
|
8766
|
+
* storage, for rendering history before the chat connects. Works the same
|
|
8767
|
+
* for every storage, the platform default included, so the browser never
|
|
8768
|
+
* reads a store directly and the secret key stays on the server.
|
|
8769
|
+
*
|
|
8770
|
+
* Wrap it in a Next.js server action (or any server-side handler), scope it
|
|
8771
|
+
* to the authenticated user through `clientData`, and pass the result to
|
|
8772
|
+
* `useLoadTranscript` in the browser.
|
|
8773
|
+
*
|
|
8774
|
+
* @example
|
|
8775
|
+
* ```ts
|
|
8776
|
+
* // actions.ts
|
|
8777
|
+
* "use server";
|
|
8778
|
+
* import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
|
|
8779
|
+
*
|
|
8780
|
+
* export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
|
|
8781
|
+
* ```
|
|
8782
|
+
*/
|
|
8783
|
+
function createChatLoadTranscriptAction(storage, options) {
|
|
8784
|
+
return async (params) => {
|
|
8785
|
+
if (!params.chatId) {
|
|
8786
|
+
throw new Error("chat.createLoadTranscriptAction: params.chatId is required.");
|
|
8787
|
+
}
|
|
8788
|
+
if (options?.apiClient) {
|
|
8789
|
+
const { apiClient, ...rest } = options;
|
|
8790
|
+
return v3_1.apiClientManager.runWithConfig(apiClient, () => createChatLoadTranscriptAction(storage, rest)(params));
|
|
8791
|
+
}
|
|
8792
|
+
const limit = params.limit ?? options?.limit;
|
|
8793
|
+
return storage.load({ chatId: params.chatId, clientData: params.clientData }, {
|
|
8794
|
+
...(limit !== undefined ? { limit } : {}),
|
|
8795
|
+
...(params.before !== undefined ? { before: params.before } : {}),
|
|
8796
|
+
});
|
|
8797
|
+
};
|
|
8798
|
+
}
|
|
7831
8799
|
exports.chat = {
|
|
7832
8800
|
/** Create a chat agent. See {@link chatAgent}. */
|
|
7833
8801
|
agent: chatAgent,
|
|
@@ -7839,8 +8807,12 @@ exports.chat = {
|
|
|
7839
8807
|
withClientData,
|
|
7840
8808
|
/** Create a server-side helper for starting (or resuming) a Session for a chatId. See {@link createChatStartSessionAction}. */
|
|
7841
8809
|
createStartSessionAction: createChatStartSessionAction,
|
|
8810
|
+
/** Returns a server-side helper that reads a conversation from a transcript storage. */
|
|
8811
|
+
createLoadTranscriptAction: createChatLoadTranscriptAction,
|
|
7842
8812
|
/** Pipe a stream to the chat transport. See {@link pipeChat}. */
|
|
7843
8813
|
pipe: pipeChat,
|
|
8814
|
+
/** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */
|
|
8815
|
+
turn: chatTurn,
|
|
7844
8816
|
/** Create a per-run typed local. See {@link chatLocal}. */
|
|
7845
8817
|
local: chatLocal,
|
|
7846
8818
|
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
|
|
@@ -7861,6 +8833,8 @@ exports.chat = {
|
|
|
7861
8833
|
endAndContinue,
|
|
7862
8834
|
/** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */
|
|
7863
8835
|
endRun,
|
|
8836
|
+
/** End the conversation permanently: close the session and exit the run. See {@link close}. */
|
|
8837
|
+
close,
|
|
7864
8838
|
/** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
|
|
7865
8839
|
cleanupAbortedParts,
|
|
7866
8840
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
@@ -8008,6 +8982,16 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
8008
8982
|
if (consumedCursor !== undefined) {
|
|
8009
8983
|
extraHeaders.push([v3_1.SESSION_IN_CONSUMED_ID_HEADER, String(consumedCursor)]);
|
|
8010
8984
|
}
|
|
8985
|
+
// A close decided before this turn ended rides out on turn-complete. Readers
|
|
8986
|
+
// terminate their stream on turn-complete, so a standalone record written
|
|
8987
|
+
// after it only reaches a reconnect — this header is what a live client sees.
|
|
8988
|
+
const pendingClose = locals_js_1.locals.get(chatCloseRequestedKey);
|
|
8989
|
+
if (pendingClose) {
|
|
8990
|
+
extraHeaders.push([v3_1.SESSION_CLOSED_HEADER, "true"]);
|
|
8991
|
+
if (pendingClose.reason) {
|
|
8992
|
+
extraHeaders.push([v3_1.SESSION_CLOSED_REASON_HEADER, pendingClose.reason]);
|
|
8993
|
+
}
|
|
8994
|
+
}
|
|
8011
8995
|
const result = await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE, extraHeaders);
|
|
8012
8996
|
const T_N = result.lastEventId ? Number.parseInt(result.lastEventId, 10) : undefined;
|
|
8013
8997
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
@@ -8065,12 +9049,47 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
8065
9049
|
*
|
|
8066
9050
|
* @internal
|
|
8067
9051
|
*/
|
|
9052
|
+
/**
|
|
9053
|
+
* Persists an upgrade requested after the turn has already run.
|
|
9054
|
+
*
|
|
9055
|
+
* The pre-turn sites reach {@link performEndAndContinue} through
|
|
9056
|
+
* {@link writeUpgradeRequiredChunk}, which is what clears (or re-points) the
|
|
9057
|
+
* session's stored `externalDeploymentId`. The post-turn exits had no such path,
|
|
9058
|
+
* so a `chat.requestUpgrade()` from `run()` or `chat.defer()` left the pin intact
|
|
9059
|
+
* and every continuation re-pinned to the deployment the agent asked to leave.
|
|
9060
|
+
*
|
|
9061
|
+
* No `upgrade-required` chunk is written here: the turn already produced its
|
|
9062
|
+
* answer, so there is nothing for a client to be told about.
|
|
9063
|
+
*/
|
|
9064
|
+
async function persistUpgradeHandoff() {
|
|
9065
|
+
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
9066
|
+
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
9067
|
+
if (!chatId || !callingRunId) {
|
|
9068
|
+
return;
|
|
9069
|
+
}
|
|
9070
|
+
try {
|
|
9071
|
+
await performEndAndContinue({
|
|
9072
|
+
reason: "upgrade",
|
|
9073
|
+
externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
9074
|
+
});
|
|
9075
|
+
}
|
|
9076
|
+
catch (error) {
|
|
9077
|
+
v3_1.logger.warn("upgrade handoff failed; session keeps its current version pin", {
|
|
9078
|
+
chatId,
|
|
9079
|
+
callingRunId,
|
|
9080
|
+
error,
|
|
9081
|
+
});
|
|
9082
|
+
}
|
|
9083
|
+
}
|
|
8068
9084
|
async function writeUpgradeRequiredChunk() {
|
|
8069
9085
|
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
8070
9086
|
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
8071
9087
|
if (chatId && callingRunId) {
|
|
8072
9088
|
try {
|
|
8073
|
-
await performEndAndContinue(
|
|
9089
|
+
await performEndAndContinue({
|
|
9090
|
+
reason: "upgrade",
|
|
9091
|
+
externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
9092
|
+
});
|
|
8074
9093
|
}
|
|
8075
9094
|
catch (error) {
|
|
8076
9095
|
// Non-fatal: the next `.in/append` re-triggers via the probe.
|