@trigger.dev/sdk 0.0.0-prerelease-20260909071038 → 0.0.0-prerelease-streamfix-20260909094302
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 +0 -2
- package/dist/commonjs/v3/ai-shared.d.ts +0 -15
- package/dist/commonjs/v3/ai-shared.js +1 -35
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +25 -275
- package/dist/commonjs/v3/ai.js +308 -1283
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +2 -11
- package/dist/commonjs/v3/chat-client.js +111 -227
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +0 -88
- package/dist/commonjs/v3/chat-react.js +1 -130
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +6 -42
- package/dist/commonjs/v3/chat-server.js +7 -52
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +10 -92
- package/dist/commonjs/v3/chat.js +73 -328
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +2 -15
- package/dist/commonjs/v3/sessions.js +1 -12
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.js +36 -30
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/test/index.d.ts +0 -1
- package/dist/commonjs/v3/test/index.js +1 -3
- package/dist/commonjs/v3/test/index.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -47
- package/dist/commonjs/v3/test/mock-chat-agent.js +4 -115
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +0 -6
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- 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 +0 -15
- package/dist/esm/v3/ai-shared.js +0 -33
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +25 -275
- package/dist/esm/v3/ai.js +305 -1275
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +2 -11
- package/dist/esm/v3/chat-client.js +112 -228
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +0 -88
- package/dist/esm/v3/chat-react.js +1 -127
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +6 -42
- package/dist/esm/v3/chat-server.js +8 -53
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +10 -92
- package/dist/esm/v3/chat.js +74 -329
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +2 -15
- package/dist/esm/v3/sessions.js +1 -11
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.js +23 -17
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/test/index.d.ts +0 -1
- package/dist/esm/v3/test/index.js +0 -1
- package/dist/esm/v3/test/index.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -47
- package/dist/esm/v3/test/mock-chat-agent.js +6 -117
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +0 -6
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +23 -55
- package/docs/ai-chat/anatomy.mdx +3 -3
- package/docs/ai-chat/backend.mdx +48 -125
- package/docs/ai-chat/background-injection.mdx +19 -69
- package/docs/ai-chat/client-protocol.mdx +4 -5
- package/docs/ai-chat/compaction.mdx +7 -13
- package/docs/ai-chat/custom-agents.mdx +0 -23
- package/docs/ai-chat/fast-starts.mdx +20 -27
- package/docs/ai-chat/frontend.mdx +14 -19
- package/docs/ai-chat/lifecycle-hooks.mdx +2 -6
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
- package/docs/ai-chat/patterns/database-persistence.mdx +2 -6
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +23 -34
- package/docs/ai-chat/patterns/skills.mdx +10 -7
- package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
- package/docs/ai-chat/pending-messages.mdx +3 -3
- package/docs/ai-chat/prompt-caching.mdx +25 -23
- package/docs/ai-chat/quick-start.mdx +11 -11
- package/docs/ai-chat/reference.mdx +9 -67
- package/docs/ai-chat/sessions.mdx +1 -6
- package/docs/ai-chat/testing.mdx +1 -2
- package/docs/ai-chat/tools.mdx +13 -18
- package/docs/ai-chat/upgrade-guide.mdx +2 -2
- package/docs/apikeys.mdx +45 -27
- package/docs/config/extensions/syncEnvVars.mdx +0 -6
- package/docs/deploy-environment-variables.mdx +2 -23
- package/docs/deployment/overview.mdx +8 -4
- package/docs/deployment/preview-branches.mdx +4 -4
- package/docs/deployment/version-skew-protection.mdx +0 -62
- package/docs/manual-setup.mdx +7 -7
- package/docs/mcp-tools.mdx +0 -9
- package/docs/quick-start.mdx +3 -3
- package/docs/realtime/auth.mdx +1 -1
- package/docs/self-hosting/security.mdx +0 -5
- package/docs/tasks/scheduled.mdx +0 -24
- package/docs/triggering.mdx +1 -1
- package/package.json +4 -4
- package/skills/trigger-authoring-chat-agent/SKILL.md +29 -41
- package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +0 -2
- package/dist/commonjs/v3/chatSnapshotIo.js +0 -174
- package/dist/commonjs/v3/chatSnapshotIo.js.map +0 -1
- package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
- package/dist/commonjs/v3/chatVersionSkew.js +0 -30
- package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
- package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
- package/dist/commonjs/v3/externalDeploymentId.js +0 -43
- package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +0 -40
- package/dist/commonjs/v3/test/transcript-storage-tests.js +0 -258
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +0 -1
- package/dist/commonjs/v3/transcriptStorage.d.ts +0 -229
- package/dist/commonjs/v3/transcriptStorage.js +0 -321
- package/dist/commonjs/v3/transcriptStorage.js.map +0 -1
- package/dist/esm/v3/chatSnapshotIo.d.ts +0 -2
- package/dist/esm/v3/chatSnapshotIo.js +0 -166
- package/dist/esm/v3/chatSnapshotIo.js.map +0 -1
- package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
- package/dist/esm/v3/chatVersionSkew.js +0 -27
- package/dist/esm/v3/chatVersionSkew.js.map +0 -1
- package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
- package/dist/esm/v3/externalDeploymentId.js +0 -38
- package/dist/esm/v3/externalDeploymentId.js.map +0 -1
- package/dist/esm/v3/test/transcript-storage-tests.d.ts +0 -40
- package/dist/esm/v3/test/transcript-storage-tests.js +0 -255
- package/dist/esm/v3/test/transcript-storage-tests.js.map +0 -1
- package/dist/esm/v3/transcriptStorage.d.ts +0 -229
- package/dist/esm/v3/transcriptStorage.js +0 -309
- package/dist/esm/v3/transcriptStorage.js.map +0 -1
- package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
- package/docs/ai-chat/transcript-storage.mdx +0 -233
- package/docs/reports.mdx +0 -157
- package/docs/troubleshooting-zod.mdx +0 -158
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.chat = exports.
|
|
4
|
-
exports.
|
|
3
|
+
exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
|
|
4
|
+
exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
|
|
5
|
+
exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
|
|
6
|
+
exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
|
|
7
|
+
exports.__writeChatSnapshotProductionPathForTests = __writeChatSnapshotProductionPathForTests;
|
|
5
8
|
exports.__mergeByIdReplaceWinsForTests = __mergeByIdReplaceWinsForTests;
|
|
6
9
|
exports.__setReplaySessionOutTailImplForTests = __setReplaySessionOutTailImplForTests;
|
|
7
10
|
exports.__replaySessionOutTailProductionPathForTests = __replaySessionOutTailProductionPathForTests;
|
|
@@ -16,16 +19,6 @@ const v3_1 = require("@trigger.dev/core/v3");
|
|
|
16
19
|
// ESM-only `ai@7` (see ../imports/ai-runtime.ts).
|
|
17
20
|
const api_1 = require("@opentelemetry/api");
|
|
18
21
|
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
|
-
}
|
|
29
22
|
const ai_shared_js_1 = require("./ai-shared.js");
|
|
30
23
|
Object.defineProperty(exports, "PENDING_MESSAGE_INJECTED_TYPE", { enumerable: true, get: function () { return ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE; } });
|
|
31
24
|
Object.defineProperty(exports, "upsertIncomingMessage", { enumerable: true, get: function () { return ai_shared_js_1.upsertIncomingMessage; } });
|
|
@@ -42,8 +35,6 @@ const metadata_js_1 = require("./metadata.js");
|
|
|
42
35
|
// pulled in transitively here never reach a client chunk.
|
|
43
36
|
const agentSkillsRuntime_js_1 = require("./agentSkillsRuntime.js");
|
|
44
37
|
const aiAutoTelemetry_js_1 = require("./aiAutoTelemetry.js");
|
|
45
|
-
const externalDeploymentId_js_1 = require("./externalDeploymentId.js");
|
|
46
|
-
const chatVersionSkew_js_1 = require("./chatVersionSkew.js");
|
|
47
38
|
const sessions_js_1 = require("./sessions.js");
|
|
48
39
|
const shared_js_1 = require("./shared.js");
|
|
49
40
|
const streams_js_1 = require("./streams.js");
|
|
@@ -130,16 +121,182 @@ async function findLatestSessionInCursor(chatId) {
|
|
|
130
121
|
}
|
|
131
122
|
return latestCursor;
|
|
132
123
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
124
|
+
let readChatSnapshotImpl;
|
|
125
|
+
function __setReadChatSnapshotImplForTests(impl) {
|
|
126
|
+
readChatSnapshotImpl = impl;
|
|
127
|
+
}
|
|
128
|
+
let writeChatSnapshotImpl;
|
|
129
|
+
function __setWriteChatSnapshotImplForTests(impl) {
|
|
130
|
+
writeChatSnapshotImpl = impl;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Read the persisted snapshot for a session. Returns `undefined` on:
|
|
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
|
+
}
|
|
143
300
|
/**
|
|
144
301
|
* Merge two `UIMessage[]` lists by `id`, with the second list winning on
|
|
145
302
|
* collision. Used at run boot to combine the snapshot's persisted history
|
|
@@ -1615,13 +1772,6 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1615
1772
|
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? replayWindowEnd, replayWindowEnd);
|
|
1616
1773
|
}
|
|
1617
1774
|
}
|
|
1618
|
-
// A boot that replayed `.in` itself has already answered everything up to
|
|
1619
|
-
// `recoveredThrough`, so the floor has to cover it before the tail opens.
|
|
1620
|
-
if (options?.recoveredThrough !== undefined) {
|
|
1621
|
-
const recovered = options.recoveredThrough;
|
|
1622
|
-
checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
|
|
1623
|
-
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? checkpoint.resumeFrom, checkpoint.resumeFrom);
|
|
1624
|
-
}
|
|
1625
1775
|
const router = entry.router;
|
|
1626
1776
|
router.restore(checkpoint);
|
|
1627
1777
|
const floor = router.resumeFrom();
|
|
@@ -1630,10 +1780,6 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1630
1780
|
v3_1.sessionStreams.setLastDispatchedSeqNum(chatId, "in", floor);
|
|
1631
1781
|
}
|
|
1632
1782
|
v3_1.sessionStreams.onRecord(chatId, "in", (record) => {
|
|
1633
|
-
// The floor is the tail's `Last-Event-ID`, but a reconnect can still
|
|
1634
|
-
// re-deliver below it and a replayable route would re-queue it.
|
|
1635
|
-
if (floor !== undefined && record.seqNum <= floor)
|
|
1636
|
-
return true;
|
|
1637
1783
|
router.ingest(record);
|
|
1638
1784
|
return true;
|
|
1639
1785
|
});
|
|
@@ -1805,35 +1951,6 @@ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
|
|
|
1805
1951
|
* @internal
|
|
1806
1952
|
*/
|
|
1807
1953
|
const chatBackgroundQueueKey = locals_js_1.locals.create("chat.backgroundQueue");
|
|
1808
|
-
/**
|
|
1809
|
-
* Background injections a step-boundary drain handed to the model this turn,
|
|
1810
|
-
* with the transcript message they followed. Reconciled into the model lane
|
|
1811
|
-
* and the persisted injections once the turn's response is in.
|
|
1812
|
-
*/
|
|
1813
|
-
const chatPendingBackgroundKey = locals_js_1.locals.create("chat.pendingBackground");
|
|
1814
|
-
/**
|
|
1815
|
-
* System-role context injected mid-conversation, held for the instructions lane.
|
|
1816
|
-
*
|
|
1817
|
-
* Kept apart from the message queue because ai@7 rejects a system message inside
|
|
1818
|
-
* `messages` for every provider — `standardizePrompt` throws upstream of any
|
|
1819
|
-
* provider call, and its own advice is to use the instructions option. Instructions
|
|
1820
|
-
* accept `Array<SystemModelMessage>`, so a system-role injection has a correct
|
|
1821
|
-
* home: appended as another system block rather than smuggled into the transcript.
|
|
1822
|
-
*
|
|
1823
|
-
* This is also the only way to inject *trusted* context. A message injected as
|
|
1824
|
-
* `user` is untrusted by construction, and a well-aligned model treats it that
|
|
1825
|
-
* way — it will say so, and re-derive the answer from tools instead.
|
|
1826
|
-
*/
|
|
1827
|
-
const chatInjectedInstructionsKey = locals_js_1.locals.create("chat.injectedInstructions");
|
|
1828
|
-
/**
|
|
1829
|
-
* What a turn already consumed from the instructions lane, so a second
|
|
1830
|
-
* `toStreamTextOptions()` call in the same turn sees the same blocks.
|
|
1831
|
-
*
|
|
1832
|
-
* Consumed blocks are moved here rather than left in the pending lane: leaving
|
|
1833
|
-
* them there means an injection made during the consumed turn sits behind them,
|
|
1834
|
-
* and clearing the lane on the next turn destroys both.
|
|
1835
|
-
*/
|
|
1836
|
-
const chatInstructionsConsumedKey = locals_js_1.locals.create("chat.injectedInstructionsConsumed");
|
|
1837
1954
|
/**
|
|
1838
1955
|
* Run-scoped pipe counter. Stored in locals so concurrent runs in the
|
|
1839
1956
|
* same worker don't share state.
|
|
@@ -2344,63 +2461,18 @@ const chatToolsOptionKey = locals_js_1.locals.create("chat.toolsOption");
|
|
|
2344
2461
|
const chatResolvedToolsKey = locals_js_1.locals.create("chat.resolvedTools");
|
|
2345
2462
|
/** @internal Flag set by `chat.requestUpgrade()` to exit the loop after the current turn. */
|
|
2346
2463
|
const chatUpgradeRequestedKey = locals_js_1.locals.create("chat.upgradeRequested");
|
|
2347
|
-
/** @internal Target for the upgrade handoff, set by `chat.requestUpgrade({ externalDeploymentId })`. */
|
|
2348
|
-
const chatUpgradeExternalDeploymentIdKey = locals_js_1.locals.create("chat.upgradeExternalDeploymentId");
|
|
2349
2464
|
/**
|
|
2350
2465
|
* @internal Flag set by `chat.endRun()` to exit the loop after the current
|
|
2351
2466
|
* turn completes, without any upgrade semantics. Checked at the same
|
|
2352
2467
|
* post-turn / pre-wait sites as `chatUpgradeRequestedKey`.
|
|
2353
2468
|
*/
|
|
2354
2469
|
const chatEndRunRequestedKey = locals_js_1.locals.create("chat.endRunRequested");
|
|
2355
|
-
/**
|
|
2356
|
-
* @internal Set by `chat.close()`. Holds the close request (and its reason)
|
|
2357
|
-
* for the rest of the run: the loop writes the terminal `session-closed`
|
|
2358
|
-
* record, closes the session row, and exits at the same post-turn /
|
|
2359
|
-
* pre-wait sites as `chatEndRunRequestedKey`.
|
|
2360
|
-
*/
|
|
2361
|
-
const chatCloseRequestedKey = locals_js_1.locals.create("chat.closeRequested");
|
|
2362
|
-
/** @internal Matches the server's `CloseSessionRequestBody.reason` cap. */
|
|
2363
|
-
const CHAT_CLOSE_REASON_MAX_LENGTH = 256;
|
|
2364
|
-
/** @internal Set once the session row is closed, so the close happens once. */
|
|
2365
|
-
const chatClosePerformedKey = locals_js_1.locals.create("chat.closePerformed");
|
|
2366
|
-
/**
|
|
2367
|
-
* @internal Set once the terminal `.out` record is written. Tracked apart from
|
|
2368
|
-
* {@link chatClosePerformedKey} so a retried close does not emit a second
|
|
2369
|
-
* client-visible event.
|
|
2370
|
-
*/
|
|
2371
|
-
const chatCloseRecordWrittenKey = locals_js_1.locals.create("chat.closeRecordWritten");
|
|
2372
2470
|
/** @internal */
|
|
2373
2471
|
const chatAgentCompactionKey = locals_js_1.locals.create("chat.agentCompaction");
|
|
2374
2472
|
/** @internal */
|
|
2375
2473
|
const chatPendingMessagesKey = locals_js_1.locals.create("chat.pendingMessages");
|
|
2376
2474
|
/** @internal */
|
|
2377
2475
|
const chatSteeringQueueKey = locals_js_1.locals.create("chat.steeringQueue");
|
|
2378
|
-
/**
|
|
2379
|
-
* This turn's new messages, as `onTurnComplete.newUIMessages` will see them.
|
|
2380
|
-
*
|
|
2381
|
-
* Held in locals because `drainSteeringQueue` runs outside the turn closure and
|
|
2382
|
-
* has to append the messages it injects. Without that, an injected message
|
|
2383
|
-
* reaches the model and the browser but no hook, so an app persisting from
|
|
2384
|
-
* `onTurnComplete` never learns it existed.
|
|
2385
|
-
*/
|
|
2386
|
-
const chatTurnNewUIMessagesKey = locals_js_1.locals.create("chat.turnNewUIMessages");
|
|
2387
|
-
/**
|
|
2388
|
-
* Steering messages a drain consumed that the model accumulator has not been
|
|
2389
|
-
* given yet.
|
|
2390
|
-
*
|
|
2391
|
-
* The two accumulators are maintained separately, and the model one is
|
|
2392
|
-
* normally advanced by appending each turn's delta. A drained message is
|
|
2393
|
-
* appended to the UI one but reaches the model only through the `prepareStep`
|
|
2394
|
-
* return value, which is per-step: without this the model lane never learns
|
|
2395
|
-
* the message exists and every later turn of the run answers without it,
|
|
2396
|
-
* while the browser, the snapshot and `chat.history.*` all still show it.
|
|
2397
|
-
*
|
|
2398
|
-
* Held as the messages rather than a "rebuild me" flag because the model lane
|
|
2399
|
-
* can only be appended to, never reconstructed. Compaction replaces it with a
|
|
2400
|
-
* summary and deliberately leaves the UI lane whole, so reconverting the UI
|
|
2401
|
-
* lane restores every message the summary replaced.
|
|
2402
|
-
*/
|
|
2403
|
-
const chatPendingSteerKey = locals_js_1.locals.create("chat.pendingSteer");
|
|
2404
2476
|
/** @internal — IDs of messages that were successfully injected via prepareStep */
|
|
2405
2477
|
const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
|
|
2406
2478
|
/** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
|
|
@@ -2753,32 +2825,20 @@ function chatCompactionStep(options) {
|
|
|
2753
2825
|
return result.type === "skipped" ? undefined : result;
|
|
2754
2826
|
};
|
|
2755
2827
|
}
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
* each entry's own conversion is used. With it, `prepare` returned one list
|
|
2760
|
-
* for the whole batch, so the first claimed message carries all of it and the
|
|
2761
|
-
* rest carry none, which keeps the total exactly what the model received.
|
|
2762
|
-
*/
|
|
2763
|
-
function modelFormOf(m, batch, injected) {
|
|
2764
|
-
return batch[0] === m ? injected : [];
|
|
2765
|
-
}
|
|
2828
|
+
// ---------------------------------------------------------------------------
|
|
2829
|
+
// Steering queue drain — shared by toStreamTextOptions, session, accumulator
|
|
2830
|
+
// ---------------------------------------------------------------------------
|
|
2766
2831
|
/**
|
|
2767
2832
|
* Drain the steering queue as a batch. Calls `shouldInject` once with all
|
|
2768
2833
|
* pending messages. If it returns true, calls `prepareMessages` once to
|
|
2769
2834
|
* transform the batch, then clears the queue.
|
|
2770
|
-
* Returns the model messages to inject
|
|
2771
|
-
*
|
|
2772
|
-
* `claimed` is returned rather than only published to locals because each
|
|
2773
|
-
* surface files it somewhere different: `chat.agent` has an accumulator in
|
|
2774
|
-
* locals, while `chat.createSession` keeps its own. Publishing to locals alone
|
|
2775
|
-
* is silently a no-op for any surface that never set the key.
|
|
2835
|
+
* Returns the model messages to inject (empty if none).
|
|
2776
2836
|
* @internal
|
|
2777
2837
|
*/
|
|
2778
2838
|
async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
2779
2839
|
const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
|
|
2780
2840
|
if (!queue || queue.length === 0)
|
|
2781
|
-
return
|
|
2841
|
+
return [];
|
|
2782
2842
|
const ctx = locals_js_1.locals.get(chatTurnContextKey);
|
|
2783
2843
|
const stepNumber = steps.length - 1;
|
|
2784
2844
|
/**
|
|
@@ -2801,7 +2861,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2801
2861
|
// Call shouldInject once for the whole batch
|
|
2802
2862
|
const shouldInject = config.shouldInject ? await config.shouldInject(batchEvent) : false;
|
|
2803
2863
|
if (!shouldInject)
|
|
2804
|
-
return
|
|
2864
|
+
return [];
|
|
2805
2865
|
const textOfUIMessage = (m) => (m.parts ?? [])
|
|
2806
2866
|
.filter((p) => p.type === "text")
|
|
2807
2867
|
.map((p) => p.text)
|
|
@@ -2847,7 +2907,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2847
2907
|
queue.splice(at, 1);
|
|
2848
2908
|
}
|
|
2849
2909
|
if (claimed.length === 0)
|
|
2850
|
-
return
|
|
2910
|
+
return [];
|
|
2851
2911
|
/**
|
|
2852
2912
|
* Give the claim back if the transform fails. `prepare` is caller code and
|
|
2853
2913
|
* can throw; the records have already left the router by this point, so
|
|
@@ -2877,37 +2937,6 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2877
2937
|
for (const m of claimedUIMessages)
|
|
2878
2938
|
injectedIds.add(m.id);
|
|
2879
2939
|
}
|
|
2880
|
-
// Record them as part of the conversation.
|
|
2881
|
-
//
|
|
2882
|
-
// The model has them and the browser has them; without this the
|
|
2883
|
-
// accumulator does not, so they reach neither `uiMessages` nor
|
|
2884
|
-
// `newUIMessages` on `onTurnComplete` and an app that persists from there
|
|
2885
|
-
// silently loses the instruction the answer was shaped by. Appending here
|
|
2886
|
-
// rather than at turn end keeps them in the order they happened: after the
|
|
2887
|
-
// message that started the turn, before the response that answers it.
|
|
2888
|
-
//
|
|
2889
|
-
// De-duplicated by id because a step boundary can drain more than once per
|
|
2890
|
-
// turn, and because a message that failed to inject falls back to becoming
|
|
2891
|
-
// its own turn, where it is accumulated the normal way.
|
|
2892
|
-
const currentUIMessages = locals_js_1.locals.get(chatCurrentUIMessagesKey);
|
|
2893
|
-
const turnNew = locals_js_1.locals.get(chatTurnNewUIMessagesKey);
|
|
2894
|
-
for (const m of claimedUIMessages) {
|
|
2895
|
-
if (currentUIMessages && !currentUIMessages.some((existing) => existing.id === m.id)) {
|
|
2896
|
-
currentUIMessages.push(m);
|
|
2897
|
-
}
|
|
2898
|
-
if (turnNew && !turnNew.some((existing) => existing.id === m.id)) {
|
|
2899
|
-
turnNew.push(m);
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
if (claimedUIMessages.length > 0 && currentUIMessages) {
|
|
2903
|
-
const pendingSteer = locals_js_1.locals.get(chatPendingSteerKey) ?? [];
|
|
2904
|
-
for (const m of claimedUIMessages) {
|
|
2905
|
-
if (!pendingSteer.some((existing) => existing.ui.id === m.id)) {
|
|
2906
|
-
pendingSteer.push({ ui: m, model: modelFormOf(m, claimedUIMessages, injected) });
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
locals_js_1.locals.set(chatPendingSteerKey, pendingSteer);
|
|
2910
|
-
}
|
|
2911
2940
|
// Write injection confirmation chunk to the stream so the frontend
|
|
2912
2941
|
// knows which messages were injected and where in the response.
|
|
2913
2942
|
if (injected.length > 0) {
|
|
@@ -2949,7 +2978,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2949
2978
|
/* non-fatal */
|
|
2950
2979
|
}
|
|
2951
2980
|
}
|
|
2952
|
-
return
|
|
2981
|
+
return injected;
|
|
2953
2982
|
}, {
|
|
2954
2983
|
attributes: {
|
|
2955
2984
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-message-forward",
|
|
@@ -3175,149 +3204,32 @@ function buildSkillTools(skills) {
|
|
|
3175
3204
|
return { loadSkill, readFile, bash };
|
|
3176
3205
|
}
|
|
3177
3206
|
/**
|
|
3178
|
-
*
|
|
3179
|
-
*
|
|
3180
|
-
*
|
|
3181
|
-
*
|
|
3182
|
-
*
|
|
3183
|
-
*
|
|
3184
|
-
*
|
|
3185
|
-
*
|
|
3186
|
-
* Caller options win for everything the caller owns (model, messages, signal,
|
|
3187
|
-
* stopWhen). The three that would otherwise clobber managed behaviour are
|
|
3188
|
-
* merged instead of replaced:
|
|
3189
|
-
*
|
|
3190
|
-
* - `tools` are passed into the helper, so skill tools survive.
|
|
3191
|
-
* - `prepareStep` is composed after the managed one, so a caller's per-step
|
|
3192
|
-
* overrides apply on top of steering and compaction instead of disabling them.
|
|
3193
|
-
*
|
|
3194
|
-
* `system` may be set at the call site, on `chat.agent({ system })`, or
|
|
3195
|
-
* through `chat.prompt.set()`, but only in one of them. Setting it in two
|
|
3196
|
-
* places throws: no shape merges two system values on every supported
|
|
3197
|
-
* version, and dropping one silently is the failure this seam exists to
|
|
3198
|
-
* prevent. Injected instructions append to whichever one is in play.
|
|
3199
|
-
*/
|
|
3200
|
-
/**
|
|
3201
|
-
* The agent-level managed options (`registry`, `system`, `cacheControl`,
|
|
3202
|
-
* `systemProviderOptions`), published for the run so that
|
|
3203
|
-
* `chat.toStreamTextOptions()` applies them too. Without this only the bound
|
|
3204
|
-
* `streamText` saw them, and the documented spread form silently ran without
|
|
3205
|
-
* the agent's system prompt or model.
|
|
3206
|
-
*/
|
|
3207
|
-
const chatAgentManagedConfigKey = locals_js_1.locals.create("chat.agentManagedConfig");
|
|
3208
|
-
/**
|
|
3209
|
-
* The caller's `streamText` options merged with the agent's managed ones.
|
|
3207
|
+
* Returns an options object ready to spread into `streamText()`.
|
|
3208
|
+
*
|
|
3209
|
+
* Includes `system`, `experimental_telemetry`, and any config fields
|
|
3210
|
+
* (temperature, maxTokens, etc.) from the stored prompt.
|
|
3211
|
+
*
|
|
3212
|
+
* When a `registry` is provided and the prompt has a `model` string,
|
|
3213
|
+
* the resolved `LanguageModel` is included as `model`.
|
|
3210
3214
|
*
|
|
3211
|
-
*
|
|
3212
|
-
* the caller did not name has to survive the merge, and the way to be sure of
|
|
3213
|
-
* that is to look at the merged object rather than at what the model received.
|
|
3215
|
+
* If no prompt has been set, returns `{}` (no-op spread).
|
|
3214
3216
|
*/
|
|
3215
|
-
function buildManagedStreamTextOptions(options, config) {
|
|
3216
|
-
const { registry, system: agentSystem, cacheControl, systemProviderOptions, tools: agentTools, } = config;
|
|
3217
|
-
/**
|
|
3218
|
-
* Only the three keys that collide are intercepted. Everything else, telemetry
|
|
3219
|
-
* included, stays in `rest` and reaches `streamText` untouched, with the
|
|
3220
|
-
* caller's value winning because `rest` is spread after `managed`. Pulling a
|
|
3221
|
-
* key out to "handle" it is how a caller's option gets silently dropped.
|
|
3222
|
-
*/
|
|
3223
|
-
const { tools, system: callerSystem, prepareStep: callerPrepareStep, ...rest } = options;
|
|
3224
|
-
const managed = toStreamTextOptions({
|
|
3225
|
-
registry,
|
|
3226
|
-
system: callerSystem ?? agentSystem,
|
|
3227
|
-
cacheControl,
|
|
3228
|
-
systemProviderOptions,
|
|
3229
|
-
/**
|
|
3230
|
-
* A call site that names `tools` replaces the agent's set rather than
|
|
3231
|
-
* adding to it, so narrowing the tools for one call still works. Omitting
|
|
3232
|
-
* `tools` falls back to the agent's, which is what an `onAction`
|
|
3233
|
-
* regenerate needs: without it a regenerated answer can call nothing.
|
|
3234
|
-
*/
|
|
3235
|
-
tools: (tools ?? agentTools),
|
|
3236
|
-
});
|
|
3237
|
-
const promptSystem = locals_js_1.locals.get(chatPromptKey)?.text;
|
|
3238
|
-
/**
|
|
3239
|
-
* Two managed sources conflict too, not only a caller against a managed one.
|
|
3240
|
-
* `toStreamTextOptions` resolves `prompt?.text || agentSystem`, so the prompt
|
|
3241
|
-
* would win and `chat.agent({ system })` would go nowhere.
|
|
3242
|
-
*/
|
|
3243
|
-
if (promptSystem && agentSystem) {
|
|
3244
|
-
throw new Error("chat.agent: `system` is set both on chat.agent({ system }) and by chat.prompt.set(), and only one " +
|
|
3245
|
-
"of them can apply. The prompt would win and the agent's `system` would go nowhere. Keep it in one " +
|
|
3246
|
-
"place, and add per-turn context with chat.inject({ role: 'system' }).");
|
|
3247
|
-
}
|
|
3248
|
-
const managedSystem = promptSystem || agentSystem;
|
|
3249
|
-
if (callerSystem !== undefined && managedSystem) {
|
|
3250
|
-
throw new Error("chat.agent: `system` is already set " +
|
|
3251
|
-
(promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
|
|
3252
|
-
", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
|
|
3253
|
-
"per-turn context with chat.inject({ role: 'system' }) rather than a second system value.");
|
|
3254
|
-
}
|
|
3255
|
-
const managedPrepareStep = managed.prepareStep;
|
|
3256
|
-
if (typeof callerPrepareStep === "function") {
|
|
3257
|
-
managed.prepareStep = async (arg) => {
|
|
3258
|
-
const first = managedPrepareStep ? await managedPrepareStep(arg) : undefined;
|
|
3259
|
-
const second = await callerPrepareStep({ ...arg, ...(first ?? {}) });
|
|
3260
|
-
return { ...(first ?? {}), ...(second ?? {}) };
|
|
3261
|
-
};
|
|
3262
|
-
}
|
|
3263
|
-
return { ...managed, ...rest };
|
|
3264
|
-
}
|
|
3265
|
-
/** @internal Test hook for {@link buildManagedStreamTextOptions}. */
|
|
3266
|
-
exports.__buildManagedStreamTextOptionsForTests = buildManagedStreamTextOptions;
|
|
3267
|
-
function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSystemProviderOptions) {
|
|
3268
|
-
const bound = (options = {}) => (0, ai_runtime_js_1.streamText)(buildManagedStreamTextOptions(options, {
|
|
3269
|
-
registry,
|
|
3270
|
-
system: agentSystem,
|
|
3271
|
-
cacheControl: agentCacheControl,
|
|
3272
|
-
systemProviderOptions: agentSystemProviderOptions,
|
|
3273
|
-
/** Read per call, so per-turn tools resolved after binding are included. */
|
|
3274
|
-
tools: locals_js_1.locals.get(chatResolvedToolsKey),
|
|
3275
|
-
}));
|
|
3276
|
-
return bound;
|
|
3277
|
-
}
|
|
3278
3217
|
function toStreamTextOptions(options) {
|
|
3279
|
-
const agentDefaults = locals_js_1.locals.get(chatAgentManagedConfigKey);
|
|
3280
|
-
if (agentDefaults) {
|
|
3281
|
-
options = {
|
|
3282
|
-
registry: agentDefaults.registry,
|
|
3283
|
-
system: agentDefaults.system,
|
|
3284
|
-
cacheControl: agentDefaults.cacheControl,
|
|
3285
|
-
systemProviderOptions: agentDefaults.systemProviderOptions,
|
|
3286
|
-
...options,
|
|
3287
|
-
};
|
|
3288
|
-
}
|
|
3289
3218
|
const prompt = locals_js_1.locals.get(chatPromptKey);
|
|
3290
3219
|
const skills = locals_js_1.locals.get(chatSkillsKey);
|
|
3291
3220
|
const result = {};
|
|
3292
3221
|
// Build the combined system prompt: stored prompt + skills preamble.
|
|
3293
|
-
const
|
|
3294
|
-
const baseSystemText = typeof baseSystem === "string"
|
|
3295
|
-
? baseSystem
|
|
3296
|
-
: typeof baseSystem?.content === "string"
|
|
3297
|
-
? baseSystem.content
|
|
3298
|
-
: "";
|
|
3299
|
-
const promptText = prompt?.text || baseSystemText;
|
|
3222
|
+
const promptText = prompt?.text ?? "";
|
|
3300
3223
|
const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
|
|
3301
3224
|
if (promptText || skillsText) {
|
|
3302
3225
|
const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
* `cacheControl` sugar, the ones carried on a structured `system` message,
|
|
3307
|
-
* then whatever `chat.prompt.set()` stored.
|
|
3308
|
-
*
|
|
3309
|
-
* A structured `system` counts only when its own text is the one being
|
|
3310
|
-
* sent. When `chat.prompt.set()` supplied the text, its provider options
|
|
3311
|
-
* are the ones that describe it.
|
|
3312
|
-
*/
|
|
3313
|
-
const baseSystemProviderOptions = !prompt?.text && baseSystem && typeof baseSystem !== "string"
|
|
3314
|
-
? baseSystem.providerOptions
|
|
3315
|
-
: undefined;
|
|
3226
|
+
// Resolve system-prompt provider options for caching. Precedence (most
|
|
3227
|
+
// specific wins, no deep merge): explicit `systemProviderOptions` →
|
|
3228
|
+
// `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
|
|
3316
3229
|
const systemProviderOptions = options?.systemProviderOptions ??
|
|
3317
3230
|
(options?.cacheControl
|
|
3318
3231
|
? { anthropic: { cacheControl: options.cacheControl } }
|
|
3319
3232
|
: undefined) ??
|
|
3320
|
-
baseSystemProviderOptions ??
|
|
3321
3233
|
locals_js_1.locals.get(chatPromptProviderOptionsKey);
|
|
3322
3234
|
// A bare string stays a bare string (the unchanged default). With provider
|
|
3323
3235
|
// options, emit a structured `SystemModelMessage` so the provider can cache
|
|
@@ -3326,88 +3238,6 @@ function toStreamTextOptions(options) {
|
|
|
3326
3238
|
? { role: "system", content: systemText, providerOptions: systemProviderOptions }
|
|
3327
3239
|
: systemText;
|
|
3328
3240
|
}
|
|
3329
|
-
/**
|
|
3330
|
-
* Append anything injected as system context, in whichever shape the installed
|
|
3331
|
-
* AI SDK accepts.
|
|
3332
|
-
*
|
|
3333
|
-
* `system` widened over time: on ai@5 it is `string` only, and from ai@6 it is
|
|
3334
|
-
* `string | SystemModelMessage | Array<SystemModelMessage>`. This package's peer
|
|
3335
|
-
* range still spans all three, so emitting an array unconditionally would break
|
|
3336
|
-
* v5 consumers — for whom a system-role injection used to work, since v5 accepted
|
|
3337
|
-
* a system message inside `messages` that v7 rejects.
|
|
3338
|
-
*
|
|
3339
|
-
* So: concatenate into one string when the base is a plain string, which every
|
|
3340
|
-
* version accepts and which loses nothing (separate blocks only matter for
|
|
3341
|
-
* per-block `providerOptions`). Use the array form only when the base is already
|
|
3342
|
-
* a structured message — that path requires v6+ regardless, because it is how
|
|
3343
|
-
* prompt caching marks the system block, and flattening it would silently throw
|
|
3344
|
-
* the cache away.
|
|
3345
|
-
*
|
|
3346
|
-
* Either way the injected text goes last: the base prompt keeps its position for
|
|
3347
|
-
* caching, and the addition reads as a later amendment. A changed prefix does
|
|
3348
|
-
* cost the first call its cache hit, on turns that actually injected.
|
|
3349
|
-
*/
|
|
3350
|
-
/**
|
|
3351
|
-
* Consumed once per turn, not once per read, and moved out of the lane rather
|
|
3352
|
-
* than marked read in place.
|
|
3353
|
-
*
|
|
3354
|
-
* Per turn, because a `run()` that builds options twice (a cheap classifier
|
|
3355
|
-
* pass and then the answer) has to see the injection in both, and draining on
|
|
3356
|
-
* read hands it to whichever call ran first. Moved out, because blocks left in
|
|
3357
|
-
* the lane sit in front of anything injected during the same turn, and
|
|
3358
|
-
* clearing the lane on the next turn then destroys both. Outside a turn there
|
|
3359
|
-
* is no turn to scope the stash to, so the lane drains on read there.
|
|
3360
|
-
*/
|
|
3361
|
-
const injectedInstructions = locals_js_1.locals.get(chatInjectedInstructionsKey);
|
|
3362
|
-
const currentTurn = locals_js_1.locals.get(chatTurnContextKey)?.turn;
|
|
3363
|
-
const consumedThisTurn = currentTurn === undefined ? undefined : locals_js_1.locals.get(chatInstructionsConsumedKey);
|
|
3364
|
-
let injectedBlocks = [];
|
|
3365
|
-
if (consumedThisTurn && consumedThisTurn.turn === currentTurn) {
|
|
3366
|
-
injectedBlocks = consumedThisTurn.blocks;
|
|
3367
|
-
// Anything injected since the stash was taken joins it, so an instruction
|
|
3368
|
-
// added after an action read the lane still reaches the real turn that
|
|
3369
|
-
// shares the action's turn number, rather than the one after.
|
|
3370
|
-
if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3371
|
-
injectedBlocks.push(...injectedInstructions.splice(0));
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
else if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3375
|
-
injectedBlocks = injectedInstructions.splice(0);
|
|
3376
|
-
if (currentTurn !== undefined) {
|
|
3377
|
-
locals_js_1.locals.set(chatInstructionsConsumedKey, { turn: currentTurn, blocks: injectedBlocks });
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
if (injectedBlocks.length > 0) {
|
|
3381
|
-
const blocks = injectedBlocks;
|
|
3382
|
-
const injectedText = blocks
|
|
3383
|
-
.map((block) => (typeof block.content === "string" ? block.content : ""))
|
|
3384
|
-
.filter(Boolean)
|
|
3385
|
-
.join("\n\n");
|
|
3386
|
-
const base = result.system;
|
|
3387
|
-
if (base === undefined) {
|
|
3388
|
-
result.system = injectedText;
|
|
3389
|
-
}
|
|
3390
|
-
else if (typeof base === "string") {
|
|
3391
|
-
result.system = [base, injectedText].filter(Boolean).join("\n\n");
|
|
3392
|
-
}
|
|
3393
|
-
else {
|
|
3394
|
-
// Merged into the existing block rather than added as a second one. An array
|
|
3395
|
-
// of system blocks would keep the base block's cache entry, but ai@5 rejects
|
|
3396
|
-
// it outright ("Invalid prompt: system must be a string") while accepting a
|
|
3397
|
-
// single structured block, and this package's peer range still spans v5.
|
|
3398
|
-
// Choosing per version would mean resolving the installed version at runtime,
|
|
3399
|
-
// which is not something to build on: `import.meta.url` is illegal in this
|
|
3400
|
-
// package's CommonJS output, and a bundled task may have no resolvable `ai`
|
|
3401
|
-
// to read. One shape that works everywhere beats a cache hit.
|
|
3402
|
-
const baseBlock = base;
|
|
3403
|
-
result.system = {
|
|
3404
|
-
...baseBlock,
|
|
3405
|
-
content: [typeof baseBlock.content === "string" ? baseBlock.content : "", injectedText]
|
|
3406
|
-
.filter(Boolean)
|
|
3407
|
-
.join("\n\n"),
|
|
3408
|
-
};
|
|
3409
|
-
}
|
|
3410
|
-
}
|
|
3411
3241
|
// Prompt-related options (only if chat.prompt.set() was called)
|
|
3412
3242
|
if (prompt) {
|
|
3413
3243
|
// Resolve model via registry if both are present
|
|
@@ -3462,7 +3292,7 @@ function toStreamTextOptions(options) {
|
|
|
3462
3292
|
}
|
|
3463
3293
|
// 2. Pending message injection (steering)
|
|
3464
3294
|
if (taskPendingMessages) {
|
|
3465
|
-
const
|
|
3295
|
+
const injected = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
|
|
3466
3296
|
if (injected.length > 0) {
|
|
3467
3297
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
3468
3298
|
}
|
|
@@ -3472,89 +3302,18 @@ function toStreamTextOptions(options) {
|
|
|
3472
3302
|
if (bgQueue && bgQueue.length > 0) {
|
|
3473
3303
|
const injected = bgQueue.splice(0); // drain
|
|
3474
3304
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
3475
|
-
const pendingBackground = locals_js_1.locals.get(chatPendingBackgroundKey) ?? [];
|
|
3476
|
-
pendingBackground.push({
|
|
3477
|
-
afterId: locals_js_1.locals.get(chatCurrentUIMessagesKey)?.at(-1)?.id ?? "",
|
|
3478
|
-
messages: injected,
|
|
3479
|
-
});
|
|
3480
|
-
locals_js_1.locals.set(chatPendingBackgroundKey, pendingBackground);
|
|
3481
3305
|
}
|
|
3482
3306
|
return resultMessages ? { messages: resultMessages } : undefined;
|
|
3483
3307
|
};
|
|
3484
3308
|
}
|
|
3485
3309
|
return result;
|
|
3486
3310
|
}
|
|
3487
|
-
const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
|
|
3488
|
-
/**
|
|
3489
|
-
* Turn the current action into a turn.
|
|
3490
|
-
*
|
|
3491
|
-
* Return it from `onAction` after editing history. The action's own work is
|
|
3492
|
-
* finished first (the edit is applied and snapshotted), then a turn runs on the
|
|
3493
|
-
* result exactly as a message turn does: `onTurnStart`, `run()` with the edited
|
|
3494
|
-
* history, `onBeforeTurnComplete`, `onTurnComplete`, and the turn counter
|
|
3495
|
-
* advances. That gives the answer everything a turn has, the system prompt,
|
|
3496
|
-
* tools, steering, compaction, injected instructions and persistence, with no
|
|
3497
|
-
* action-specific handling.
|
|
3498
|
-
*
|
|
3499
|
-
* @example
|
|
3500
|
-
* ```ts
|
|
3501
|
-
* onAction: async ({ action }) => {
|
|
3502
|
-
* if (action.type === "regenerate") {
|
|
3503
|
-
* chat.history.slice(0, -1);
|
|
3504
|
-
* return chat.turn();
|
|
3505
|
-
* }
|
|
3506
|
-
* if (action.type === "undo") chat.history.slice(0, -2); // no turn
|
|
3507
|
-
* },
|
|
3508
|
-
* ```
|
|
3509
|
-
*/
|
|
3510
|
-
function chatTurn() {
|
|
3511
|
-
return { [actionTurnBrand]: true };
|
|
3512
|
-
}
|
|
3513
|
-
function isActionTurn(value) {
|
|
3514
|
-
return typeof value === "object" && value !== null && value[actionTurnBrand] === true;
|
|
3515
|
-
}
|
|
3516
|
-
/**
|
|
3517
|
-
* Replace, in a model lane, the run of messages one UI message contributed.
|
|
3518
|
-
*
|
|
3519
|
-
* Used when a UI message is replaced in place (a tool-approval continuation
|
|
3520
|
-
* merging onto the trailing assistant, a captured response reusing an existing
|
|
3521
|
-
* id, a partial replacing an existing message). Reconverting the whole lane
|
|
3522
|
-
* from the UI lane would also replace a compaction summary with the full
|
|
3523
|
-
* transcript and drop the model forms `pendingMessages.prepare` produced.
|
|
3524
|
-
*
|
|
3525
|
-
* The replaced message is the trailing one, so its run is the lane's tail,
|
|
3526
|
-
* before any steer forms appended after it this turn (`tailAfter`). If the
|
|
3527
|
-
* tail does not match the old message's conversion, nothing is changed and
|
|
3528
|
-
* `false` is returned so the caller can fall back to a full reconversion.
|
|
3529
|
-
*/
|
|
3530
|
-
async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
|
|
3531
|
-
const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
|
|
3532
|
-
const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
|
|
3533
|
-
const end = lane.length - tailAfter;
|
|
3534
|
-
const start = end - oldRun.length;
|
|
3535
|
-
if (start < 0 || end > lane.length)
|
|
3536
|
-
return false;
|
|
3537
|
-
if (JSON.stringify(lane.slice(start, end)) !== JSON.stringify(oldRun))
|
|
3538
|
-
return false;
|
|
3539
|
-
lane.splice(start, oldRun.length, ...newRun);
|
|
3540
|
-
return true;
|
|
3541
|
-
}
|
|
3542
3311
|
function isUIMessageStreamable(value) {
|
|
3543
3312
|
return (typeof value === "object" &&
|
|
3544
3313
|
value !== null &&
|
|
3545
3314
|
"toUIMessageStream" in value &&
|
|
3546
3315
|
typeof value.toUIMessageStream === "function");
|
|
3547
3316
|
}
|
|
3548
|
-
const warnedHydrateMessagesDeprecated = new Set();
|
|
3549
|
-
function warnHydrateMessagesDeprecatedOnce(agentId) {
|
|
3550
|
-
if (warnedHydrateMessagesDeprecated.has(agentId))
|
|
3551
|
-
return;
|
|
3552
|
-
warnedHydrateMessagesDeprecated.add(agentId);
|
|
3553
|
-
console.warn(`[chat.agent] \`hydrateMessages\` on "${agentId}" is deprecated. Give the agent a transcript ` +
|
|
3554
|
-
"storage instead: `save` receives every change to the conversation and `loadContext` " +
|
|
3555
|
-
"lets the application own the model's context, with crash recovery and durable " +
|
|
3556
|
-
"compaction that `hydrateMessages` never had.");
|
|
3557
|
-
}
|
|
3558
3317
|
let warnedMissingOnAction = false;
|
|
3559
3318
|
function warnMissingOnActionOnce() {
|
|
3560
3319
|
if (warnedMissingOnAction)
|
|
@@ -3687,22 +3446,10 @@ function chatCustomAgent(options) {
|
|
|
3687
3446
|
await installChatInputRouter(payload.chatId, {
|
|
3688
3447
|
resuming: Boolean(payload.continuation),
|
|
3689
3448
|
});
|
|
3690
|
-
// A custom agent's loop is the customer's, so there is no exit site the
|
|
3691
|
-
// SDK controls. Perform a requested close when `run()` returns, whatever
|
|
3692
|
-
// shape the loop had. Idempotent, so the createSession iterator having
|
|
3693
|
-
// already closed on its own exit costs nothing.
|
|
3694
|
-
const withClose = async (result) => {
|
|
3695
|
-
try {
|
|
3696
|
-
return await result;
|
|
3697
|
-
}
|
|
3698
|
-
finally {
|
|
3699
|
-
await performChatClose();
|
|
3700
|
-
}
|
|
3701
|
-
};
|
|
3702
3449
|
// Keep the schema-free path identical to the original custom-agent
|
|
3703
3450
|
// wrapper, including when userRun starts executing.
|
|
3704
3451
|
if (!parseClientData) {
|
|
3705
|
-
return
|
|
3452
|
+
return userRun(payload, runOptions);
|
|
3706
3453
|
}
|
|
3707
3454
|
const isHandoverBoot = payload.trigger === "handover-prepare";
|
|
3708
3455
|
const isMessagelessBoot = payload.trigger === "preload" ||
|
|
@@ -3718,7 +3465,7 @@ function chatCustomAgent(options) {
|
|
|
3718
3465
|
writeErrorToStream: !isMessagelessBoot && !isHandoverBoot,
|
|
3719
3466
|
});
|
|
3720
3467
|
if (validated.ok) {
|
|
3721
|
-
return
|
|
3468
|
+
return userRun(validated.payload, runOptions);
|
|
3722
3469
|
}
|
|
3723
3470
|
if (isHandoverBoot) {
|
|
3724
3471
|
const signal = await waitForHandover({
|
|
@@ -3754,7 +3501,7 @@ function chatCustomAgent(options) {
|
|
|
3754
3501
|
sessionId: next.output.sessionId ?? payload.sessionId,
|
|
3755
3502
|
idleTimeoutInSeconds: next.output.idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds,
|
|
3756
3503
|
};
|
|
3757
|
-
return
|
|
3504
|
+
return userRun(recoveredPayload, runOptions);
|
|
3758
3505
|
},
|
|
3759
3506
|
});
|
|
3760
3507
|
// Register clientDataSchema so the CLI converts it to JSONSchema
|
|
@@ -3767,19 +3514,7 @@ function chatCustomAgent(options) {
|
|
|
3767
3514
|
return task;
|
|
3768
3515
|
}
|
|
3769
3516
|
function chatAgent(options) {
|
|
3770
|
-
const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages,
|
|
3771
|
-
if (hydrateMessages) {
|
|
3772
|
-
if (storage) {
|
|
3773
|
-
throw new Error(`chat.agent: "${options.id}" sets both \`hydrateMessages\` and \`storage\`. ` +
|
|
3774
|
-
"`hydrateMessages` is deprecated and replaced by the storage: `save` receives every " +
|
|
3775
|
-
"change and `loadContext` on the storage owns the model's context. Remove `hydrateMessages`.");
|
|
3776
|
-
}
|
|
3777
|
-
if (typeof (transcriptStorageOverride ?? transcriptStorage_js_1.defaultStorage).loadContext === "function") {
|
|
3778
|
-
throw new Error(`chat.agent: "${options.id}" sets \`hydrateMessages\` and uses a transcript storage with ` +
|
|
3779
|
-
"`loadContext`. Both would own the model's context; keep one.");
|
|
3780
|
-
}
|
|
3781
|
-
warnHydrateMessagesDeprecatedOnce(options.id);
|
|
3782
|
-
}
|
|
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;
|
|
3783
3518
|
const parseClientData = clientDataSchema ? (0, v3_1.getSchemaParseFn)(clientDataSchema) : undefined;
|
|
3784
3519
|
const parseAction = actionSchema ? (0, v3_1.getSchemaParseFn)(actionSchema) : undefined;
|
|
3785
3520
|
// chat.agent does not expose generic retry options (see docstring on
|
|
@@ -3862,42 +3597,6 @@ function chatAgent(options) {
|
|
|
3862
3597
|
// durable snapshot + `session.out` replay (or `hydrateMessages` if
|
|
3863
3598
|
// registered) — the wire is delta-only now, no longer a seed.
|
|
3864
3599
|
let accumulatedMessages = [];
|
|
3865
|
-
/**
|
|
3866
|
-
* Give the model accumulator the background injections a step-boundary
|
|
3867
|
-
* drain handed to the model this turn, and record them for persistence.
|
|
3868
|
-
* Returns how many model messages were appended.
|
|
3869
|
-
*/
|
|
3870
|
-
const reconcilePendingBackground = () => {
|
|
3871
|
-
const pending = locals_js_1.locals.get(chatPendingBackgroundKey);
|
|
3872
|
-
if (!pending || pending.length === 0)
|
|
3873
|
-
return 0;
|
|
3874
|
-
locals_js_1.locals.set(chatPendingBackgroundKey, []);
|
|
3875
|
-
let appended = 0;
|
|
3876
|
-
for (const entry of pending) {
|
|
3877
|
-
accumulatedMessages.push(...entry.messages);
|
|
3878
|
-
laneInjections.push(entry);
|
|
3879
|
-
appended += entry.messages.length;
|
|
3880
|
-
}
|
|
3881
|
-
return appended;
|
|
3882
|
-
};
|
|
3883
|
-
/**
|
|
3884
|
-
* Give the model accumulator the steering messages a drain consumed,
|
|
3885
|
-
* in the form the model actually received. Appended, never reconverted
|
|
3886
|
-
* from the UI lane, so a model-only compaction summary survives. Called
|
|
3887
|
-
* on both the success and the error path, before the response or the
|
|
3888
|
-
* partial joins the lane, so the order stays steer-then-answer.
|
|
3889
|
-
*/
|
|
3890
|
-
const reconcilePendingSteer = (options) => {
|
|
3891
|
-
const pending = locals_js_1.locals.get(chatPendingSteerKey);
|
|
3892
|
-
if (!pending || pending.length === 0)
|
|
3893
|
-
return [];
|
|
3894
|
-
locals_js_1.locals.set(chatPendingSteerKey, []);
|
|
3895
|
-
for (const entry of pending) {
|
|
3896
|
-
accumulatedMessages.push(...entry.model);
|
|
3897
|
-
options?.turnNew?.push(...entry.model);
|
|
3898
|
-
}
|
|
3899
|
-
return pending;
|
|
3900
|
-
};
|
|
3901
3600
|
// Accumulated UI messages for persistence. Mirrors the model accumulator
|
|
3902
3601
|
// but in frontend-friendly UIMessage format (with parts, id, etc.).
|
|
3903
3602
|
let accumulatedUIMessages = [];
|
|
@@ -3915,157 +3614,7 @@ function chatAgent(options) {
|
|
|
3915
3614
|
// collectively cost ~600ms on every first-message TTFC. Both reads
|
|
3916
3615
|
// swallow errors internally; the agent stays available either way.
|
|
3917
3616
|
const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
|
|
3918
|
-
const transcriptStorage = storage ??
|
|
3919
|
-
transcriptStorageOverride ??
|
|
3920
|
-
transcriptStorage_js_1.defaultStorage;
|
|
3921
|
-
const storageLoadContext = transcriptStorage.loadContext?.bind(transcriptStorage);
|
|
3922
|
-
/**
|
|
3923
|
-
* Who supplies the model's context each turn: the deprecated
|
|
3924
|
-
* `hydrateMessages` hook, the storage's `loadContext`, or (undefined)
|
|
3925
|
-
* the runtime's own transcript.
|
|
3926
|
-
*/
|
|
3927
|
-
const loadContextHook = hydrateMessages
|
|
3928
|
-
? (event) => hydrateMessages(event)
|
|
3929
|
-
: storageLoadContext
|
|
3930
|
-
? (event) => storageLoadContext({ chatId: event.chatId, clientData: event.clientData }, event)
|
|
3931
|
-
: undefined;
|
|
3932
|
-
let transcriptShadow = (0, transcriptStorage_js_1.createTranscriptShadow)([]);
|
|
3933
|
-
let bootTranscriptState = null;
|
|
3934
|
-
/**
|
|
3935
|
-
* True while the model lane holds a compaction summary, so it cannot be
|
|
3936
|
-
* rebuilt from the transcript and has to be persisted as state. Reset
|
|
3937
|
-
* wherever the lane is reconverted from the UI lane.
|
|
3938
|
-
*/
|
|
3939
|
-
let laneCompacted = false;
|
|
3940
|
-
/** Conversational `chat.inject` messages in the lane, anchored to the transcript. */
|
|
3941
|
-
let laneInjections = [];
|
|
3942
|
-
let persistedStateSet = false;
|
|
3943
3617
|
let bootSnapshot;
|
|
3944
|
-
let bootClientData = payload.metadata;
|
|
3945
|
-
if (parseClientData) {
|
|
3946
|
-
try {
|
|
3947
|
-
bootClientData = await parseClientData(payload.metadata);
|
|
3948
|
-
}
|
|
3949
|
-
catch {
|
|
3950
|
-
bootClientData = payload.metadata;
|
|
3951
|
-
}
|
|
3952
|
-
}
|
|
3953
|
-
/**
|
|
3954
|
-
* The `lastOutEventId` the most recent snapshot carried.
|
|
3955
|
-
*
|
|
3956
|
-
* A snapshot written outside a turn — after an action mutates history — has
|
|
3957
|
-
* no turn cursor of its own, and writing `undefined` there would drop the
|
|
3958
|
-
* resume point and make the next boot replay from further back. Retaining it
|
|
3959
|
-
* keeps an action's write cursor-neutral.
|
|
3960
|
-
*/
|
|
3961
|
-
let lastSnapshotOutEventId;
|
|
3962
|
-
const storageTrigger = (trigger) => trigger === "regenerate-message"
|
|
3963
|
-
? "regenerate-message"
|
|
3964
|
-
: trigger === "action" || trigger === "action-turn"
|
|
3965
|
-
? "action"
|
|
3966
|
-
: "submit-message";
|
|
3967
|
-
/**
|
|
3968
|
-
* Hand the runtime's view of the transcript to the storage as a
|
|
3969
|
-
* changeset: the diff against what was last saved, plus the cursors the
|
|
3970
|
-
* next boot resumes from. The shadow only advances when the save
|
|
3971
|
-
* succeeds, so a failed save is folded into the next changeset.
|
|
3972
|
-
*/
|
|
3973
|
-
/** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
|
|
3974
|
-
let transcriptState = null;
|
|
3975
|
-
const saveTranscript = async (opts) => {
|
|
3976
|
-
const { changes, shadow } = (0, transcriptStorage_js_1.diffTranscript)(transcriptShadow, opts.messages, {
|
|
3977
|
-
nonFinalIds: opts.nonFinalIds,
|
|
3978
|
-
});
|
|
3979
|
-
const throughId = opts.messages.at(-1)?.id ?? "";
|
|
3980
|
-
const queued = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
3981
|
-
const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
|
|
3982
|
-
? {
|
|
3983
|
-
v: 1,
|
|
3984
|
-
...(laneCompacted
|
|
3985
|
-
? {
|
|
3986
|
-
compaction: {
|
|
3987
|
-
modelMessages: accumulatedMessages,
|
|
3988
|
-
throughId,
|
|
3989
|
-
fingerprint: (0, transcriptStorage_js_1.prefixFingerprint)(shadow, throughId),
|
|
3990
|
-
},
|
|
3991
|
-
}
|
|
3992
|
-
: {}),
|
|
3993
|
-
...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
|
|
3994
|
-
...(queued.length > 0 ? { queued: [...queued] } : {}),
|
|
3995
|
-
}
|
|
3996
|
-
: null;
|
|
3997
|
-
if (runtimeState !== null || persistedStateSet) {
|
|
3998
|
-
changes.push({ op: "state", value: runtimeState });
|
|
3999
|
-
}
|
|
4000
|
-
transcriptState = runtimeState;
|
|
4001
|
-
const inCursor = chatInputRouter().resumeFloor();
|
|
4002
|
-
await transcriptStorage.save({
|
|
4003
|
-
chatId: payload.chatId,
|
|
4004
|
-
clientData: opts.clientData,
|
|
4005
|
-
turn: opts.turn,
|
|
4006
|
-
trigger: opts.trigger,
|
|
4007
|
-
runId: ctx.run.id,
|
|
4008
|
-
ctx,
|
|
4009
|
-
}, {
|
|
4010
|
-
reason: opts.reason,
|
|
4011
|
-
changes,
|
|
4012
|
-
transcript: {
|
|
4013
|
-
entries: opts.messages.map((message) => ({
|
|
4014
|
-
id: message.id,
|
|
4015
|
-
final: !shadow.nonFinal.has(message.id),
|
|
4016
|
-
message,
|
|
4017
|
-
})),
|
|
4018
|
-
state: transcriptState,
|
|
4019
|
-
},
|
|
4020
|
-
cursors: {
|
|
4021
|
-
lastOutEventId: opts.lastOutEventId,
|
|
4022
|
-
lastInEventId: inCursor !== undefined ? String(inCursor) : undefined,
|
|
4023
|
-
},
|
|
4024
|
-
});
|
|
4025
|
-
transcriptShadow = shadow;
|
|
4026
|
-
persistedStateSet = runtimeState !== null;
|
|
4027
|
-
};
|
|
4028
|
-
/**
|
|
4029
|
-
* Persist the accumulator outside a turn.
|
|
4030
|
-
*
|
|
4031
|
-
* An action is not a turn, so it never reaches the turn-complete path where
|
|
4032
|
-
* the transcript is normally saved, but a `chat.history` mutation changes
|
|
4033
|
-
* the conversation and has to survive the run ending.
|
|
4034
|
-
*
|
|
4035
|
-
* Cursor-neutral: an action has no turn cursor of its own, and writing
|
|
4036
|
-
* `undefined` would drop the resume point the last turn established and make
|
|
4037
|
-
* the next boot replay from further back.
|
|
4038
|
-
*/
|
|
4039
|
-
const writeSnapshotOutsideTurn = async (reason, turnContext) => {
|
|
4040
|
-
if (hydrateMessages)
|
|
4041
|
-
return;
|
|
4042
|
-
try {
|
|
4043
|
-
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
4044
|
-
await saveTranscript({
|
|
4045
|
-
reason: "action",
|
|
4046
|
-
messages: accumulatedUIMessages,
|
|
4047
|
-
turn: turnContext.turn,
|
|
4048
|
-
trigger: "action",
|
|
4049
|
-
clientData: turnContext.clientData,
|
|
4050
|
-
lastOutEventId: lastSnapshotOutEventId,
|
|
4051
|
-
});
|
|
4052
|
-
}, {
|
|
4053
|
-
attributes: {
|
|
4054
|
-
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
4055
|
-
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
4056
|
-
"chat.snapshot.reason": reason,
|
|
4057
|
-
"chat.messages.count": accumulatedUIMessages.length,
|
|
4058
|
-
},
|
|
4059
|
-
});
|
|
4060
|
-
}
|
|
4061
|
-
catch (error) {
|
|
4062
|
-
v3_1.logger.warn("chat.agent: snapshot write outside a turn failed; the change may not survive a continuation", {
|
|
4063
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4064
|
-
sessionId: sessionIdForSnapshot,
|
|
4065
|
-
reason,
|
|
4066
|
-
});
|
|
4067
|
-
}
|
|
4068
|
-
};
|
|
4069
3618
|
let replayedSettled = [];
|
|
4070
3619
|
let replayedPartial;
|
|
4071
3620
|
let replayedPartialRaw;
|
|
@@ -4081,7 +3630,7 @@ function chatAgent(options) {
|
|
|
4081
3630
|
// resume-cursor block reuses it instead of re-scanning.
|
|
4082
3631
|
let bootInCursor;
|
|
4083
3632
|
let bootInCursorResolved = false;
|
|
4084
|
-
if (couldHavePriorState) {
|
|
3633
|
+
if (!hydrateMessages && couldHavePriorState) {
|
|
4085
3634
|
// Single parent span for the whole boot read phase — snapshot
|
|
4086
3635
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
4087
3636
|
// + result counts are attributes on the span.
|
|
@@ -4089,33 +3638,18 @@ function chatAgent(options) {
|
|
|
4089
3638
|
// snapshot read
|
|
4090
3639
|
const snapStart = Date.now();
|
|
4091
3640
|
try {
|
|
4092
|
-
|
|
4093
|
-
? undefined
|
|
4094
|
-
: await transcriptStorage.load({
|
|
4095
|
-
chatId: payload.chatId,
|
|
4096
|
-
clientData: bootClientData,
|
|
4097
|
-
});
|
|
4098
|
-
if (loaded) {
|
|
4099
|
-
transcriptShadow = (0, transcriptStorage_js_1.createTranscriptShadow)(loaded.messages, new Set(loaded.nonFinalIds ?? []));
|
|
4100
|
-
bootTranscriptState = loaded.state;
|
|
4101
|
-
transcriptState = loaded.state ?? null;
|
|
4102
|
-
persistedStateSet = loaded.state !== null && loaded.state !== undefined;
|
|
4103
|
-
bootSnapshot = {
|
|
4104
|
-
messages: loaded.messages,
|
|
4105
|
-
lastOutEventId: loaded.cursors?.lastOutEventId,
|
|
4106
|
-
lastInEventId: loaded.cursors?.lastInEventId,
|
|
4107
|
-
};
|
|
4108
|
-
}
|
|
3641
|
+
bootSnapshot = await readChatSnapshot(sessionIdForSnapshot);
|
|
4109
3642
|
}
|
|
4110
3643
|
catch (error) {
|
|
4111
|
-
|
|
3644
|
+
// `readChatSnapshot` already swallows + warns internally; this catch
|
|
3645
|
+
// is just belt-and-suspenders against tracer/span errors.
|
|
3646
|
+
v3_1.logger.warn("chat.agent: snapshot read failed; continuing without snapshot", {
|
|
4112
3647
|
error: error instanceof Error ? error.message : String(error),
|
|
4113
3648
|
sessionId: sessionIdForSnapshot,
|
|
4114
3649
|
});
|
|
4115
3650
|
}
|
|
4116
3651
|
bootSpan.setAttribute("chat.boot.snapshot.durationMs", Date.now() - snapStart);
|
|
4117
|
-
bootSpan.setAttribute("chat.boot.snapshot.present", bootSnapshot
|
|
4118
|
-
(bootSnapshot.messages.length > 0 || bootSnapshot.lastOutEventId !== undefined));
|
|
3652
|
+
bootSpan.setAttribute("chat.boot.snapshot.present", !!bootSnapshot);
|
|
4119
3653
|
bootSpan.setAttribute("chat.boot.snapshot.messageCount", bootSnapshot?.messages?.length ?? 0);
|
|
4120
3654
|
// Seed the trim chain from the snapshot's `lastOutEventId` (the SSE
|
|
4121
3655
|
// id of the previous turn's `turn-complete` control record). The
|
|
@@ -4123,7 +3657,6 @@ function chatAgent(options) {
|
|
|
4123
3657
|
// Without seeding, the new worker would emit no trim on its first
|
|
4124
3658
|
// turn (chain self-bootstraps from turn 2), so this is purely an
|
|
4125
3659
|
// optimization to keep continuation runs bounded from the first turn.
|
|
4126
|
-
lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
|
|
4127
3660
|
if (bootSnapshot?.lastOutEventId !== undefined) {
|
|
4128
3661
|
const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
|
|
4129
3662
|
if (Number.isFinite(seeded)) {
|
|
@@ -4218,17 +3751,13 @@ function chatAgent(options) {
|
|
|
4218
3751
|
// Reads the turn boundary and subscribes in one call. `bootInCursor` is
|
|
4219
3752
|
// only a fallback: the boot block above may already have resolved a
|
|
4220
3753
|
// cursor from the snapshot, which is used when the boundary itself
|
|
4221
|
-
// carries none.
|
|
4222
|
-
// `bootInjectedQueue` below, so it goes into the floor here — folded in
|
|
4223
|
-
// after the subscription opens, the live tail re-delivers it as a turn.
|
|
4224
|
-
const lastRecoveredInSeq = replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1].seqNum : undefined;
|
|
3754
|
+
// carries none.
|
|
4225
3755
|
await installChatInputRouter(payload.chatId, {
|
|
4226
3756
|
fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
|
|
4227
|
-
recoveredThrough: lastRecoveredInSeq,
|
|
4228
3757
|
resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
|
|
4229
3758
|
});
|
|
4230
3759
|
// ── Recovery boot + chain reconstruction ────────────────────────
|
|
4231
|
-
{
|
|
3760
|
+
if (!hydrateMessages) {
|
|
4232
3761
|
const settledMessages = mergeByIdReplaceWins(bootSnapshot?.messages ?? [], replayedSettled);
|
|
4233
3762
|
const inFlightUsers = replayedInTail.map((r) => r.message);
|
|
4234
3763
|
const partialAssistant = replayedPartial;
|
|
@@ -4332,6 +3861,16 @@ function chatAgent(options) {
|
|
|
4332
3861
|
if (hookBeforeBoot) {
|
|
4333
3862
|
await hookBeforeBoot();
|
|
4334
3863
|
}
|
|
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
|
+
}
|
|
4335
3874
|
// Synthesize wire payloads for each recoveredTurn. The turn-loop
|
|
4336
3875
|
// pops these ahead of `messagesInput.waitWithIdleTimeout` so they
|
|
4337
3876
|
// dispatch as normal turns with the existing hook stack.
|
|
@@ -4381,8 +3920,7 @@ function chatAgent(options) {
|
|
|
4381
3920
|
// path where wire-borne UIMessage[] still seeds the accumulator,
|
|
4382
3921
|
// and it's safe because the route handler isn't subject to the
|
|
4383
3922
|
// `/in/append` 512 KiB cap.
|
|
4384
|
-
if (
|
|
4385
|
-
accumulatedUIMessages.length === 0 &&
|
|
3923
|
+
if (accumulatedUIMessages.length === 0 &&
|
|
4386
3924
|
payload.trigger === "handover-prepare" &&
|
|
4387
3925
|
Array.isArray(payload.headStartMessages) &&
|
|
4388
3926
|
payload.headStartMessages.length > 0) {
|
|
@@ -4411,17 +3949,7 @@ function chatAgent(options) {
|
|
|
4411
3949
|
}
|
|
4412
3950
|
}
|
|
4413
3951
|
try {
|
|
4414
|
-
|
|
4415
|
-
const restored = await (0, transcriptStorage_js_1.restoreModelLane)(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
|
|
4416
|
-
accumulatedMessages = restored.messages;
|
|
4417
|
-
laneCompacted = restored.compacted;
|
|
4418
|
-
laneInjections = restored.injections;
|
|
4419
|
-
if (bootRuntimeState?.queued && bootRuntimeState.queued.length > 0) {
|
|
4420
|
-
locals_js_1.locals.set(chatBackgroundQueueKey, [
|
|
4421
|
-
...(locals_js_1.locals.get(chatBackgroundQueueKey) ?? []),
|
|
4422
|
-
...bootRuntimeState.queued,
|
|
4423
|
-
]);
|
|
4424
|
-
}
|
|
3952
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
4425
3953
|
}
|
|
4426
3954
|
catch (error) {
|
|
4427
3955
|
v3_1.logger.warn("chat.agent: toModelMessages failed at boot; starting empty", {
|
|
@@ -4435,12 +3963,6 @@ function chatAgent(options) {
|
|
|
4435
3963
|
// before any hook (`onChatStart`, `onTurnStart`, etc.) fires.
|
|
4436
3964
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4437
3965
|
}
|
|
4438
|
-
locals_js_1.locals.set(chatAgentManagedConfigKey, {
|
|
4439
|
-
registry: promptRegistry,
|
|
4440
|
-
system: agentSystem,
|
|
4441
|
-
cacheControl: agentCacheControl,
|
|
4442
|
-
systemProviderOptions: agentSystemProviderOptions,
|
|
4443
|
-
});
|
|
4444
3966
|
// Token usage tracking across turns
|
|
4445
3967
|
let previousTurnUsage;
|
|
4446
3968
|
let cumulativeUsage = emptyUsage();
|
|
@@ -4798,7 +4320,6 @@ function chatAgent(options) {
|
|
|
4798
4320
|
} // end else (no boot-injected first turn)
|
|
4799
4321
|
}
|
|
4800
4322
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
4801
|
-
let turnClientData = payload.metadata;
|
|
4802
4323
|
// Declared here so the finally can detach it — a handler leaked past
|
|
4803
4324
|
// its turn duplicates every mid-stream message into the shared buffer.
|
|
4804
4325
|
let turnMsgSub;
|
|
@@ -4820,7 +4341,6 @@ function chatAgent(options) {
|
|
|
4820
4341
|
// `cleanupAbortedParts` no longer has to be re-applied below.
|
|
4821
4342
|
const cleanedIncomingMessages = incomingMessages.map((msg) => msg.role === "assistant" ? cleanupAbortedParts(msg) : msg);
|
|
4822
4343
|
const clientData = (parseClientData ? await parseClientData(wireMetadata) : wireMetadata);
|
|
4823
|
-
turnClientData = clientData;
|
|
4824
4344
|
const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages);
|
|
4825
4345
|
// Actions are not turns. They use a different span name
|
|
4826
4346
|
// and don't carry a turn.number. Branched on at `isAction`.
|
|
@@ -4857,7 +4377,6 @@ function chatAgent(options) {
|
|
|
4857
4377
|
locals_js_1.locals.set(chatDeferKey, new Set());
|
|
4858
4378
|
locals_js_1.locals.set(chatCompactionStateKey, undefined);
|
|
4859
4379
|
locals_js_1.locals.set(chatSteeringQueueKey, []);
|
|
4860
|
-
locals_js_1.locals.set(chatPendingBackgroundKey, []);
|
|
4861
4380
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
4862
4381
|
// NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
|
|
4863
4382
|
// by deferred work from the previous turn's onTurnComplete need to
|
|
@@ -4939,10 +4458,6 @@ function chatAgent(options) {
|
|
|
4939
4458
|
// Track new messages for this turn (user input + assistant response).
|
|
4940
4459
|
const turnNewModelMessages = [];
|
|
4941
4460
|
const turnNewUIMessages = [];
|
|
4942
|
-
locals_js_1.locals.set(chatTurnNewUIMessagesKey, turnNewUIMessages);
|
|
4943
|
-
// A head-start handover deliberately resumes from an assistant
|
|
4944
|
-
// message it spliced in, so it isn't a no-op turn.
|
|
4945
|
-
let splicedHandoverPartial = false;
|
|
4946
4461
|
// ── Action handling ──────────────────────────────────────
|
|
4947
4462
|
// Actions arrive on the same input stream but with
|
|
4948
4463
|
// trigger === "action". They are NOT turns — only
|
|
@@ -4953,25 +4468,16 @@ function chatAgent(options) {
|
|
|
4953
4468
|
// an action, return a `StreamTextResult` (auto-piped),
|
|
4954
4469
|
// string, or UIMessage from `onAction`. Turn counter
|
|
4955
4470
|
// does not advance.
|
|
4956
|
-
let
|
|
4957
|
-
/** Set when `onAction` returned `chat.turn()`: the turn block below runs. */
|
|
4958
|
-
let actionTurn = false;
|
|
4959
|
-
/**
|
|
4960
|
-
* Whether this action changed the conversation, by rolling history
|
|
4961
|
-
* back or by streaming a response. Drives the single snapshot write
|
|
4962
|
-
* at the end — an action never reaches the turn-complete path that
|
|
4963
|
-
* normally does it.
|
|
4964
|
-
*/
|
|
4965
|
-
let actionChangedHistory = false;
|
|
4471
|
+
let actionStreamResult = undefined;
|
|
4966
4472
|
if (isAction) {
|
|
4967
4473
|
// Parse and validate the action payload
|
|
4968
4474
|
const parsedAction = parseAction
|
|
4969
4475
|
? await parseAction(currentWirePayload.action)
|
|
4970
4476
|
: currentWirePayload.action;
|
|
4971
4477
|
// Hydrate messages from backend if configured
|
|
4972
|
-
if (
|
|
4478
|
+
if (hydrateMessages) {
|
|
4973
4479
|
const hydrated = await tracer_js_1.tracer.startActiveSpan("hydrateMessages()", async () => {
|
|
4974
|
-
return
|
|
4480
|
+
return hydrateMessages({
|
|
4975
4481
|
chatId: currentWirePayload.chatId,
|
|
4976
4482
|
turn,
|
|
4977
4483
|
trigger: "action",
|
|
@@ -4991,14 +4497,12 @@ function chatAgent(options) {
|
|
|
4991
4497
|
});
|
|
4992
4498
|
accumulatedUIMessages = [...hydrated];
|
|
4993
4499
|
accumulatedMessages = await toModelMessages(hydrated);
|
|
4994
|
-
laneCompacted = false;
|
|
4995
|
-
laneInjections = [];
|
|
4996
4500
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4997
4501
|
}
|
|
4998
4502
|
// Fire onAction — handler may mutate state via
|
|
4999
4503
|
// `chat.history.*` and / or return a model response.
|
|
5000
4504
|
if (onAction) {
|
|
5001
|
-
|
|
4505
|
+
actionStreamResult = await tracer_js_1.tracer.startActiveSpan("onAction()", async () => {
|
|
5002
4506
|
return await onAction({
|
|
5003
4507
|
action: parsedAction,
|
|
5004
4508
|
chatId: currentWirePayload.chatId,
|
|
@@ -5023,10 +4527,7 @@ function chatAgent(options) {
|
|
|
5023
4527
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5024
4528
|
accumulatedUIMessages = [...actionOverride];
|
|
5025
4529
|
accumulatedMessages = await toModelMessages(actionOverride);
|
|
5026
|
-
laneCompacted = false;
|
|
5027
|
-
laneInjections = [];
|
|
5028
4530
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5029
|
-
actionChangedHistory = true;
|
|
5030
4531
|
}
|
|
5031
4532
|
}
|
|
5032
4533
|
else {
|
|
@@ -5047,7 +4548,7 @@ function chatAgent(options) {
|
|
|
5047
4548
|
// route handler's first-turn history to the hydrate hook as
|
|
5048
4549
|
// incoming messages instead (gated on the pending handover).
|
|
5049
4550
|
if (turn === 0 &&
|
|
5050
|
-
|
|
4551
|
+
hydrateMessages &&
|
|
5051
4552
|
cleanedUIMessages.length === 0 &&
|
|
5052
4553
|
(locals_js_1.locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
|
|
5053
4554
|
Array.isArray(payload.headStartMessages) &&
|
|
@@ -5077,7 +4578,7 @@ function chatAgent(options) {
|
|
|
5077
4578
|
},
|
|
5078
4579
|
}));
|
|
5079
4580
|
}
|
|
5080
|
-
if (
|
|
4581
|
+
if (hydrateMessages) {
|
|
5081
4582
|
// Snapshot the ids the accumulator knew BEFORE this
|
|
5082
4583
|
// turn ran — used below to decide whether an
|
|
5083
4584
|
// incoming wire message is genuinely new or just a
|
|
@@ -5095,7 +4596,7 @@ function chatAgent(options) {
|
|
|
5095
4596
|
// was always true for `submit-message` is now true for every
|
|
5096
4597
|
// trigger.
|
|
5097
4598
|
const hydrated = await tracer_js_1.tracer.startActiveSpan("hydrateMessages()", async () => {
|
|
5098
|
-
return
|
|
4599
|
+
return hydrateMessages({
|
|
5099
4600
|
chatId: currentWirePayload.chatId,
|
|
5100
4601
|
turn,
|
|
5101
4602
|
trigger: currentWirePayload.trigger,
|
|
@@ -5134,8 +4635,6 @@ function chatAgent(options) {
|
|
|
5134
4635
|
}
|
|
5135
4636
|
accumulatedUIMessages = merged;
|
|
5136
4637
|
accumulatedMessages = await toModelMessages(merged);
|
|
5137
|
-
laneCompacted = false;
|
|
5138
|
-
laneInjections = [];
|
|
5139
4638
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5140
4639
|
// Track new messages for onTurnComplete.newUIMessages.
|
|
5141
4640
|
// Only push for genuinely new ids — HITL continuations
|
|
@@ -5181,8 +4680,6 @@ function chatAgent(options) {
|
|
|
5181
4680
|
accumulatedUIMessages.pop();
|
|
5182
4681
|
}
|
|
5183
4682
|
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5184
|
-
laneCompacted = false;
|
|
5185
|
-
laneInjections = [];
|
|
5186
4683
|
}
|
|
5187
4684
|
else if (cleanedUIMessages.length > 0) {
|
|
5188
4685
|
// Submit-message (and the special-cased
|
|
@@ -5199,7 +4696,6 @@ function chatAgent(options) {
|
|
|
5199
4696
|
// where AI SDK regenerates the id (TRI-9137) still
|
|
5200
4697
|
// applies via `rewriteIncomingIdViaToolCallMap`.
|
|
5201
4698
|
let replaced = false;
|
|
5202
|
-
const replacedPairs = [];
|
|
5203
4699
|
for (const raw of cleanedUIMessages) {
|
|
5204
4700
|
let incoming = raw;
|
|
5205
4701
|
let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
|
|
@@ -5211,9 +4707,7 @@ function chatAgent(options) {
|
|
|
5211
4707
|
}
|
|
5212
4708
|
}
|
|
5213
4709
|
if (idx !== -1) {
|
|
5214
|
-
|
|
5215
|
-
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(previous, incoming);
|
|
5216
|
-
replacedPairs.push({ previous, merged: accumulatedUIMessages[idx] });
|
|
4710
|
+
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(accumulatedUIMessages[idx], incoming);
|
|
5217
4711
|
replaced = true;
|
|
5218
4712
|
}
|
|
5219
4713
|
else {
|
|
@@ -5223,19 +4717,9 @@ function chatAgent(options) {
|
|
|
5223
4717
|
recordToolCallIdsFromMessage(incoming);
|
|
5224
4718
|
}
|
|
5225
4719
|
if (replaced) {
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
inPlace = false;
|
|
5230
|
-
break;
|
|
5231
|
-
}
|
|
5232
|
-
}
|
|
5233
|
-
if (!inPlace) {
|
|
5234
|
-
v3_1.logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
|
|
5235
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5236
|
-
laneCompacted = false;
|
|
5237
|
-
laneInjections = [];
|
|
5238
|
-
}
|
|
4720
|
+
// Replacement changes structure — reconvert all model
|
|
4721
|
+
// messages instead of appending.
|
|
4722
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5239
4723
|
}
|
|
5240
4724
|
else {
|
|
5241
4725
|
const incomingModelMessages = await toModelMessages(cleanedUIMessages);
|
|
@@ -5278,33 +4762,10 @@ function chatAgent(options) {
|
|
|
5278
4762
|
messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
|
|
5279
4763
|
});
|
|
5280
4764
|
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
5281
|
-
splicedHandoverPartial = true;
|
|
5282
4765
|
}
|
|
5283
4766
|
}
|
|
5284
4767
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5285
4768
|
} // end if (trigger !== "action")
|
|
5286
|
-
// ── No-op turn ──────────────────────────────────────────
|
|
5287
|
-
//
|
|
5288
|
-
// A submit that added no new user message and leaves the model
|
|
5289
|
-
// chain ending on an assistant message has nothing to answer —
|
|
5290
|
-
// calling the model would prefill its own last reply. Keyed on
|
|
5291
|
-
// the model tail, so a `tool`-terminated chain (a merged tool
|
|
5292
|
-
// approval) still runs.
|
|
5293
|
-
const isNoOpTurn = !isAction &&
|
|
5294
|
-
!splicedHandoverPartial &&
|
|
5295
|
-
currentWirePayload.trigger === "submit-message" &&
|
|
5296
|
-
turnNewUIMessages.length === 0 &&
|
|
5297
|
-
accumulatedMessages[accumulatedMessages.length - 1]?.role === "assistant";
|
|
5298
|
-
if (isNoOpTurn) {
|
|
5299
|
-
msgSub?.off();
|
|
5300
|
-
v3_1.logger.warn("chat.agent: turn added no new user message; skipping the model", {
|
|
5301
|
-
chatId: currentWirePayload.chatId,
|
|
5302
|
-
messageId: currentWirePayload.messageId,
|
|
5303
|
-
});
|
|
5304
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5305
|
-
// Not a turn — don't consume an iteration.
|
|
5306
|
-
turn--;
|
|
5307
|
-
}
|
|
5308
4769
|
// ── Action result handling ──────────────────────────────
|
|
5309
4770
|
// For action turns, skip the turn machinery entirely.
|
|
5310
4771
|
// If `onAction` returned a stream / string / UIMessage,
|
|
@@ -5314,34 +4775,34 @@ function chatAgent(options) {
|
|
|
5314
4775
|
// The turn counter is decremented so the next iteration
|
|
5315
4776
|
// sees the same `turn` value — actions don't count.
|
|
5316
4777
|
if (isAction) {
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
4778
|
+
msgSub?.off();
|
|
4779
|
+
if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 &&
|
|
4780
|
+
isUIMessageStreamable(actionStreamResult)) {
|
|
4781
|
+
try {
|
|
4782
|
+
const resolvedOptions = resolveUIMessageStreamOptions();
|
|
4783
|
+
const uiStream = actionStreamResult.toUIMessageStream({
|
|
4784
|
+
...resolvedOptions,
|
|
4785
|
+
generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
|
|
4786
|
+
});
|
|
4787
|
+
await pipeChat(uiStream, {
|
|
4788
|
+
signal: combinedSignal,
|
|
4789
|
+
spanName: "stream response",
|
|
4790
|
+
});
|
|
5324
4791
|
}
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
else {
|
|
5333
|
-
msgSub?.off();
|
|
5334
|
-
if (actionChangedHistory) {
|
|
5335
|
-
await writeSnapshotOutsideTurn("action", { turn, clientData });
|
|
4792
|
+
catch (error) {
|
|
4793
|
+
if (error instanceof Error &&
|
|
4794
|
+
error.name === "AbortError" &&
|
|
4795
|
+
runSignal.aborted) {
|
|
4796
|
+
return "exit";
|
|
4797
|
+
}
|
|
4798
|
+
throw error;
|
|
5336
4799
|
}
|
|
5337
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5338
|
-
// Don't consume a turn iteration — actions aren't turns.
|
|
5339
|
-
turn--;
|
|
5340
4800
|
}
|
|
4801
|
+
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4802
|
+
// Don't consume a turn iteration — actions aren't turns.
|
|
4803
|
+
turn--;
|
|
5341
4804
|
}
|
|
5342
|
-
|
|
5343
|
-
// there is nothing to answer, so nothing to hand over.
|
|
5344
|
-
if ((!isAction || actionTurn) && !isNoOpTurn) {
|
|
4805
|
+
if (!isAction) {
|
|
5345
4806
|
// Mint a scoped public access token once per turn, reused for
|
|
5346
4807
|
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
5347
4808
|
const currentRunId = ctx.run.id;
|
|
@@ -5430,8 +4891,6 @@ function chatAgent(options) {
|
|
|
5430
4891
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5431
4892
|
accumulatedUIMessages = [...turnStartOverride];
|
|
5432
4893
|
accumulatedMessages = await toModelMessages(turnStartOverride);
|
|
5433
|
-
laneCompacted = false;
|
|
5434
|
-
laneInjections = [];
|
|
5435
4894
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5436
4895
|
}
|
|
5437
4896
|
}, {
|
|
@@ -5448,11 +4907,9 @@ function chatAgent(options) {
|
|
|
5448
4907
|
},
|
|
5449
4908
|
});
|
|
5450
4909
|
}
|
|
5451
|
-
await followSessionPin(currentWirePayload.chatId, versionSkew);
|
|
5452
4910
|
// chat.requestUpgrade() called in onTurnStart (or onValidateMessages) —
|
|
5453
|
-
// skip run() and
|
|
5454
|
-
//
|
|
5455
|
-
// keeps reading.
|
|
4911
|
+
// skip run() and signal the transport to re-trigger the same message
|
|
4912
|
+
// on the new version.
|
|
5456
4913
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
5457
4914
|
await writeUpgradeRequiredChunk();
|
|
5458
4915
|
return "exit";
|
|
@@ -5492,12 +4949,7 @@ function chatAgent(options) {
|
|
|
5492
4949
|
const lastAccumulated = accumulatedMessages[accumulatedMessages.length - 1];
|
|
5493
4950
|
const bgQueue = locals_js_1.locals.get(chatBackgroundQueueKey);
|
|
5494
4951
|
if (bgQueue && bgQueue.length > 0 && lastAccumulated?.role !== "tool") {
|
|
5495
|
-
|
|
5496
|
-
accumulatedMessages.push(...injected);
|
|
5497
|
-
laneInjections.push({
|
|
5498
|
-
afterId: accumulatedUIMessages.at(-1)?.id ?? "",
|
|
5499
|
-
messages: injected,
|
|
5500
|
-
});
|
|
4952
|
+
accumulatedMessages.push(...bgQueue.splice(0));
|
|
5501
4953
|
}
|
|
5502
4954
|
if (isHeadStartFinalTurn) {
|
|
5503
4955
|
// The synthesized partial UIMessage IS the response.
|
|
@@ -5516,9 +4968,6 @@ function chatAgent(options) {
|
|
|
5516
4968
|
const preparedMessages = await applyPrepareMessages(accumulatedMessages, "run");
|
|
5517
4969
|
runResult = await userRun({
|
|
5518
4970
|
...restWire,
|
|
5519
|
-
// A turn requested by chat.turn() is not the action itself:
|
|
5520
|
-
// a run() that short-circuits on "action" must still answer.
|
|
5521
|
-
...(actionTurn ? { trigger: "action-turn" } : {}),
|
|
5522
4971
|
messages: preparedMessages,
|
|
5523
4972
|
clientData,
|
|
5524
4973
|
continuation,
|
|
@@ -5531,7 +4980,6 @@ function chatAgent(options) {
|
|
|
5531
4980
|
signal: combinedSignal,
|
|
5532
4981
|
cancelSignal,
|
|
5533
4982
|
stopSignal,
|
|
5534
|
-
streamText: createBoundStreamText(promptRegistry, agentSystem, agentCacheControl, agentSystemProviderOptions),
|
|
5535
4983
|
});
|
|
5536
4984
|
}
|
|
5537
4985
|
// Auto-pipe if the run function returned a StreamTextResult or similar,
|
|
@@ -5645,21 +5093,7 @@ function chatAgent(options) {
|
|
|
5645
5093
|
if (runOverride) {
|
|
5646
5094
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5647
5095
|
accumulatedUIMessages = [...runOverride];
|
|
5648
|
-
|
|
5649
|
-
* Steers the drain consumed are left out of the rebuild and
|
|
5650
|
-
* appended by the reconciliation below instead, so the lane
|
|
5651
|
-
* gets the form the model actually received rather than a
|
|
5652
|
-
* reconversion of the UI message, and gets it once. A steer
|
|
5653
|
-
* the edit removed is dropped from the pending list too, so
|
|
5654
|
-
* the edit is honoured.
|
|
5655
|
-
*/
|
|
5656
|
-
const overrideIds = new Set(runOverride.map((m) => m.id));
|
|
5657
|
-
const pending = (locals_js_1.locals.get(chatPendingSteerKey) ?? []).filter((e) => overrideIds.has(e.ui.id));
|
|
5658
|
-
locals_js_1.locals.set(chatPendingSteerKey, pending);
|
|
5659
|
-
const pendingIds = new Set(pending.map((e) => e.ui.id));
|
|
5660
|
-
accumulatedMessages = await toModelMessages(runOverride.filter((m) => !pendingIds.has(m.id)));
|
|
5661
|
-
laneCompacted = false;
|
|
5662
|
-
laneInjections = [];
|
|
5096
|
+
accumulatedMessages = await toModelMessages(runOverride);
|
|
5663
5097
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5664
5098
|
}
|
|
5665
5099
|
// Check if compaction set a model-only override (preserves UI messages).
|
|
@@ -5682,8 +5116,6 @@ function chatAgent(options) {
|
|
|
5682
5116
|
accumulatedMessages = taskCompactionConfig?.compactModelMessages
|
|
5683
5117
|
? await taskCompactionConfig.compactModelMessages(compactEvent)
|
|
5684
5118
|
: modelOnlyOverride;
|
|
5685
|
-
laneCompacted = true;
|
|
5686
|
-
laneInjections = [];
|
|
5687
5119
|
// Apply UI messages: callback or default (preserve all)
|
|
5688
5120
|
if (taskCompactionConfig?.compactUIMessages) {
|
|
5689
5121
|
accumulatedUIMessages = (await taskCompactionConfig.compactUIMessages(compactEvent));
|
|
@@ -5691,15 +5123,6 @@ function chatAgent(options) {
|
|
|
5691
5123
|
}
|
|
5692
5124
|
// Determine if the user stopped generation this turn (not a full run cancel).
|
|
5693
5125
|
const wasStopped = stopController.signal.aborted && !runSignal.aborted;
|
|
5694
|
-
// Give the model accumulator the steering messages the drain
|
|
5695
|
-
// consumed. Appended, never reconverted from the UI lane, so a
|
|
5696
|
-
// model-only compaction summary set just above survives; and done
|
|
5697
|
-
// before the response is appended so the order stays
|
|
5698
|
-
// steer-then-answer. Outside the `capturedResponseMessage`
|
|
5699
|
-
// branches below, so a turn that captured no response is covered.
|
|
5700
|
-
const steerTailThisTurn = reconcilePendingSteer({
|
|
5701
|
-
turnNew: turnNewModelMessages,
|
|
5702
|
-
}).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
|
|
5703
5126
|
// Append the assistant's response (partial or complete) to the accumulator.
|
|
5704
5127
|
// The onFinish callback fires even on abort/stop, so partial responses
|
|
5705
5128
|
// from stopped generation are captured correctly.
|
|
@@ -5737,7 +5160,6 @@ function chatAgent(options) {
|
|
|
5737
5160
|
const existingIdx = capturedResponseMessage.id
|
|
5738
5161
|
? accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id)
|
|
5739
5162
|
: -1;
|
|
5740
|
-
const previousAtIdx = existingIdx !== -1 ? accumulatedUIMessages[existingIdx] : undefined;
|
|
5741
5163
|
if (existingIdx !== -1) {
|
|
5742
5164
|
accumulatedUIMessages[existingIdx] = capturedResponseMessage;
|
|
5743
5165
|
}
|
|
@@ -5757,14 +5179,8 @@ function chatAgent(options) {
|
|
|
5757
5179
|
stripProviderMetadata(capturedResponseMessage),
|
|
5758
5180
|
]);
|
|
5759
5181
|
if (existingIdx !== -1) {
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
if (!ok) {
|
|
5763
|
-
v3_1.logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
|
|
5764
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5765
|
-
laneCompacted = false;
|
|
5766
|
-
laneInjections = [];
|
|
5767
|
-
}
|
|
5182
|
+
// Reconvert all model messages since we replaced rather than appended
|
|
5183
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5768
5184
|
}
|
|
5769
5185
|
else {
|
|
5770
5186
|
accumulatedMessages.push(...responseModelMessages);
|
|
@@ -5872,8 +5288,6 @@ function chatAgent(options) {
|
|
|
5872
5288
|
],
|
|
5873
5289
|
},
|
|
5874
5290
|
];
|
|
5875
|
-
laneCompacted = true;
|
|
5876
|
-
laneInjections = [];
|
|
5877
5291
|
// UI messages: callback or default (preserve all)
|
|
5878
5292
|
if (outerCompaction.compactUIMessages) {
|
|
5879
5293
|
accumulatedUIMessages = (await outerCompaction.compactUIMessages(outerCompactEvent));
|
|
@@ -5963,8 +5377,6 @@ function chatAgent(options) {
|
|
|
5963
5377
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
5964
5378
|
accumulatedUIMessages = [...override];
|
|
5965
5379
|
accumulatedMessages = await toModelMessages(override);
|
|
5966
|
-
laneCompacted = false;
|
|
5967
|
-
laneInjections = [];
|
|
5968
5380
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5969
5381
|
// Update event so onTurnComplete sees compacted messages
|
|
5970
5382
|
turnCompleteEvent.messages = accumulatedMessages;
|
|
@@ -6011,8 +5423,6 @@ function chatAgent(options) {
|
|
|
6011
5423
|
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
6012
5424
|
accumulatedUIMessages = [...turnCompleteOverride];
|
|
6013
5425
|
accumulatedMessages = await toModelMessages(turnCompleteOverride);
|
|
6014
|
-
laneCompacted = false;
|
|
6015
|
-
laneInjections = [];
|
|
6016
5426
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
6017
5427
|
}
|
|
6018
5428
|
}, {
|
|
@@ -6066,18 +5476,13 @@ function chatAgent(options) {
|
|
|
6066
5476
|
if (!hydrateMessages) {
|
|
6067
5477
|
try {
|
|
6068
5478
|
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
5479
|
+
const snapshotInCursor = chatInputRouter().resumeFloor();
|
|
5480
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
5481
|
+
version: 1,
|
|
5482
|
+
savedAt: Date.now(),
|
|
6073
5483
|
messages: accumulatedUIMessages,
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
clientData,
|
|
6077
|
-
lastOutEventId: lastSnapshotOutEventId,
|
|
6078
|
-
nonFinalIds: wasStopped && capturedResponseMessage?.id
|
|
6079
|
-
? new Set([capturedResponseMessage.id])
|
|
6080
|
-
: undefined,
|
|
5484
|
+
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
5485
|
+
lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
|
|
6081
5486
|
});
|
|
6082
5487
|
}, {
|
|
6083
5488
|
attributes: {
|
|
@@ -6110,17 +5515,10 @@ function chatAgent(options) {
|
|
|
6110
5515
|
currentWirePayload = bootInjectedQueue.shift();
|
|
6111
5516
|
return "continue";
|
|
6112
5517
|
}
|
|
6113
|
-
// chat.requestUpgrade() was called — exit the loop
|
|
6114
|
-
//
|
|
5518
|
+
// chat.requestUpgrade() was called — exit the loop so the
|
|
5519
|
+
// transport triggers a new run on the latest version.
|
|
6115
5520
|
// chat.endRun() — same exit, no upgrade semantics.
|
|
6116
|
-
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
6117
|
-
await performChatClose();
|
|
6118
|
-
return "exit";
|
|
6119
|
-
}
|
|
6120
5521
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
6121
|
-
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
6122
|
-
await persistUpgradeHandoff();
|
|
6123
|
-
}
|
|
6124
5522
|
return "exit";
|
|
6125
5523
|
}
|
|
6126
5524
|
// Wait for the next message — stay idle briefly, then suspend
|
|
@@ -6227,10 +5625,6 @@ function chatAgent(options) {
|
|
|
6227
5625
|
});
|
|
6228
5626
|
// Signal turn complete so the client knows this turn is done
|
|
6229
5627
|
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
6230
|
-
// A later action's snapshot reuses this cursor, so it has to move
|
|
6231
|
-
// here too or that snapshot resumes from before the failed turn.
|
|
6232
|
-
lastSnapshotOutEventId =
|
|
6233
|
-
errorTurnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
|
|
6234
5628
|
}
|
|
6235
5629
|
catch {
|
|
6236
5630
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
@@ -6275,47 +5669,15 @@ function chatAgent(options) {
|
|
|
6275
5669
|
: partialIdx === -1
|
|
6276
5670
|
? [...erroredUIMessages, partialResponse]
|
|
6277
5671
|
: erroredUIMessages.map((m, i) => i === partialIdx ? partialResponse : m);
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
* instruction whenever the turn it steered went on to fail.
|
|
6283
|
-
*/
|
|
6284
|
-
const buildErroredNew = () => {
|
|
6285
|
-
const out = [];
|
|
6286
|
-
const addUnique = (m) => {
|
|
6287
|
-
if (m && !out.some((existing) => existing.id === m.id))
|
|
6288
|
-
out.push(m);
|
|
6289
|
-
};
|
|
6290
|
-
addUnique(erroredWireMessage);
|
|
6291
|
-
for (const m of (locals_js_1.locals.get(chatTurnNewUIMessagesKey) ?? [])) {
|
|
6292
|
-
addUnique(m);
|
|
6293
|
-
}
|
|
6294
|
-
if (includePartial)
|
|
6295
|
-
addUnique(partialResponse);
|
|
6296
|
-
return out;
|
|
6297
|
-
};
|
|
6298
|
-
let erroredNewUIMessages = buildErroredNew();
|
|
5672
|
+
let erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
|
|
5673
|
+
if (includePartial) {
|
|
5674
|
+
erroredNewUIMessages.push(partialResponse);
|
|
5675
|
+
}
|
|
6299
5676
|
let erroredNewModelMessages = [];
|
|
6300
|
-
const reconciledSteer = reconcilePendingSteer();
|
|
6301
|
-
const backgroundTailThisTurn = reconcilePendingBackground();
|
|
6302
5677
|
if (!responseCommitted) {
|
|
6303
5678
|
try {
|
|
6304
5679
|
if (erroredNewUIMessages.length > 0) {
|
|
6305
|
-
|
|
6306
|
-
* Built in order from the recorded forms rather than by
|
|
6307
|
-
* converting the UI list, so a steer appears in the delta as
|
|
6308
|
-
* the model received it (what `prepare` produced), matching the
|
|
6309
|
-
* lane. The wire message and partial are converted as before.
|
|
6310
|
-
*/
|
|
6311
|
-
const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
|
|
6312
|
-
for (const m of erroredNewUIMessages) {
|
|
6313
|
-
const recorded = steerModelById.get(m.id);
|
|
6314
|
-
if (recorded)
|
|
6315
|
-
erroredNewModelMessages.push(...recorded);
|
|
6316
|
-
else
|
|
6317
|
-
erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
|
|
6318
|
-
}
|
|
5680
|
+
erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map((m) => stripProviderMetadata(m)));
|
|
6319
5681
|
}
|
|
6320
5682
|
if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
|
|
6321
5683
|
if (partialIdx === -1) {
|
|
@@ -6323,14 +5685,7 @@ function chatAgent(options) {
|
|
|
6323
5685
|
accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
|
|
6324
5686
|
}
|
|
6325
5687
|
else {
|
|
6326
|
-
|
|
6327
|
-
backgroundTailThisTurn);
|
|
6328
|
-
if (!ok) {
|
|
6329
|
-
v3_1.logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
|
|
6330
|
-
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6331
|
-
laneCompacted = false;
|
|
6332
|
-
laneInjections = [];
|
|
6333
|
-
}
|
|
5688
|
+
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6334
5689
|
}
|
|
6335
5690
|
accumulatedUIMessages = erroredUIMessagesWithPartial;
|
|
6336
5691
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -6339,7 +5694,7 @@ function chatAgent(options) {
|
|
|
6339
5694
|
catch {
|
|
6340
5695
|
erroredNewModelMessages = [];
|
|
6341
5696
|
erroredUIMessagesWithPartial = erroredUIMessages;
|
|
6342
|
-
erroredNewUIMessages =
|
|
5697
|
+
erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
|
|
6343
5698
|
}
|
|
6344
5699
|
}
|
|
6345
5700
|
if (onTurnComplete) {
|
|
@@ -6393,14 +5748,13 @@ function chatAgent(options) {
|
|
|
6393
5748
|
// neither the snapshot nor the replayable `.in` tail.
|
|
6394
5749
|
if (!hydrateMessages) {
|
|
6395
5750
|
try {
|
|
6396
|
-
|
|
6397
|
-
|
|
5751
|
+
const errorSnapshotInCursor = chatInputRouter().resumeFloor();
|
|
5752
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
5753
|
+
version: 1,
|
|
5754
|
+
savedAt: Date.now(),
|
|
6398
5755
|
messages: erroredUIMessagesWithPartial,
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
clientData: turnClientData,
|
|
6402
|
-
lastOutEventId: lastSnapshotOutEventId,
|
|
6403
|
-
nonFinalIds: includePartial && partialResponse ? new Set([partialResponse.id]) : undefined,
|
|
5756
|
+
lastOutEventId: errorTurnCompleteResult?.lastEventId,
|
|
5757
|
+
lastInEventId: errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined,
|
|
6404
5758
|
});
|
|
6405
5759
|
}
|
|
6406
5760
|
catch (error) {
|
|
@@ -6410,15 +5764,8 @@ function chatAgent(options) {
|
|
|
6410
5764
|
});
|
|
6411
5765
|
}
|
|
6412
5766
|
}
|
|
6413
|
-
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
6414
|
-
await performChatClose();
|
|
6415
|
-
return;
|
|
6416
|
-
}
|
|
6417
5767
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
6418
5768
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
6419
|
-
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
6420
|
-
await persistUpgradeHandoff();
|
|
6421
|
-
}
|
|
6422
5769
|
return;
|
|
6423
5770
|
}
|
|
6424
5771
|
// Drain remaining recovered turns before idling — a thrown
|
|
@@ -6441,12 +5788,6 @@ function chatAgent(options) {
|
|
|
6441
5788
|
return; // Timed out — end run gracefully
|
|
6442
5789
|
}
|
|
6443
5790
|
currentWirePayload = next.output;
|
|
6444
|
-
// Same close check the success path makes. Without it a close
|
|
6445
|
-
// record that lands after a failed turn is consumed as if it were
|
|
6446
|
-
// a turn payload, and the loop runs on against a closed session.
|
|
6447
|
-
if (currentWirePayload.trigger === "close") {
|
|
6448
|
-
return;
|
|
6449
|
-
}
|
|
6450
5791
|
// Continue to next iteration of the for loop
|
|
6451
5792
|
}
|
|
6452
5793
|
finally {
|
|
@@ -6455,11 +5796,6 @@ function chatAgent(options) {
|
|
|
6455
5796
|
}
|
|
6456
5797
|
}
|
|
6457
5798
|
finally {
|
|
6458
|
-
// Safety net for a close requested on a path that exits without
|
|
6459
|
-
// reaching one of the loop's close checks (a turn timeout, an OOM
|
|
6460
|
-
// re-throw). `performChatClose` is idempotent, so the ordinary path
|
|
6461
|
-
// having already run it costs nothing here.
|
|
6462
|
-
await performChatClose();
|
|
6463
5799
|
// `stopSub` is registered post-preload so the close-during-preload
|
|
6464
5800
|
// early-return path may exit before it ever attached. Guard the
|
|
6465
5801
|
// cleanup so a missing subscription doesn't throw.
|
|
@@ -6797,22 +6133,15 @@ function isStopped() {
|
|
|
6797
6133
|
// Version upgrade
|
|
6798
6134
|
// ---------------------------------------------------------------------------
|
|
6799
6135
|
/**
|
|
6800
|
-
*
|
|
6801
|
-
*
|
|
6802
|
-
* The handover happens immediately and server-side: a successor run is created
|
|
6803
|
-
* and picks the conversation up from `session.in`. The transport keeps reading
|
|
6804
|
-
* the same session output, so no client action is needed and nothing waits for
|
|
6805
|
-
* the next message.
|
|
6806
|
-
*
|
|
6807
|
-
* Without a target the session's pin is cleared, so the successor lands on the
|
|
6808
|
-
* latest deployed version; with `externalDeploymentId` the session is re-pinned
|
|
6809
|
-
* to that deployment.
|
|
6136
|
+
* Request that the current run exits so the next message starts on the latest
|
|
6137
|
+
* deployed version (via the standard continuation mechanism).
|
|
6810
6138
|
*
|
|
6811
6139
|
* When called from `onTurnStart` or `onValidateMessages`, `run()` is skipped
|
|
6812
|
-
* entirely
|
|
6140
|
+
* entirely — the run exits immediately and the transport re-triggers the
|
|
6141
|
+
* same message on the new version.
|
|
6813
6142
|
*
|
|
6814
6143
|
* When called from `run()` or `chat.defer()`, the current turn completes
|
|
6815
|
-
* normally and the
|
|
6144
|
+
* normally and the run exits afterward instead of waiting for the next message.
|
|
6816
6145
|
*
|
|
6817
6146
|
* Call from `onTurnStart`, `onValidateMessages`, `onChatResume`, `run()`,
|
|
6818
6147
|
* or inside `chat.defer()`.
|
|
@@ -6832,37 +6161,8 @@ function isStopped() {
|
|
|
6832
6161
|
* });
|
|
6833
6162
|
* ```
|
|
6834
6163
|
*/
|
|
6835
|
-
function requestUpgrade(
|
|
6164
|
+
function requestUpgrade() {
|
|
6836
6165
|
locals_js_1.locals.set(chatUpgradeRequestedKey, true);
|
|
6837
|
-
// Without a target the handoff clears the session's pin; with one it re-pins to that deployment.
|
|
6838
|
-
const target = options?.externalDeploymentId?.trim();
|
|
6839
|
-
if (target)
|
|
6840
|
-
locals_js_1.locals.set(chatUpgradeExternalDeploymentIdKey, target);
|
|
6841
|
-
}
|
|
6842
|
-
/** @internal Requests a handoff when the session's pin no longer names this deployment. */
|
|
6843
|
-
async function followSessionPin(chatId, policy) {
|
|
6844
|
-
if (!chatId) {
|
|
6845
|
-
return;
|
|
6846
|
-
}
|
|
6847
|
-
const deployedExternalId = locals_js_1.locals.get(chatAgentRunContextKey)?.deployment?.externalId;
|
|
6848
|
-
if (policy !== "hold" && !deployedExternalId) {
|
|
6849
|
-
v3_1.logger.debug("chat.versionSkew: cannot follow the session pin", {
|
|
6850
|
-
chatId,
|
|
6851
|
-
reason: "the run context carries no deployment.externalId",
|
|
6852
|
-
});
|
|
6853
|
-
}
|
|
6854
|
-
const target = await (0, chatVersionSkew_js_1.resolvePinToFollow)({
|
|
6855
|
-
policy,
|
|
6856
|
-
deployedExternalId,
|
|
6857
|
-
upgradeAlreadyRequested: locals_js_1.locals.get(chatUpgradeRequestedKey) === true,
|
|
6858
|
-
readPin: async () => (await sessions_js_1.sessions.retrieve(chatId, { retry: { maxAttempts: 2, randomize: true } }))
|
|
6859
|
-
.triggerConfig,
|
|
6860
|
-
});
|
|
6861
|
-
if (!target) {
|
|
6862
|
-
return;
|
|
6863
|
-
}
|
|
6864
|
-
v3_1.logger.info("chat.versionSkew: following the session pin", { chatId, target });
|
|
6865
|
-
requestUpgrade({ externalDeploymentId: target });
|
|
6866
6166
|
}
|
|
6867
6167
|
/**
|
|
6868
6168
|
* Hand off the current custom agent Session to a fresh run.
|
|
@@ -6901,31 +6201,20 @@ async function endAndContinue() {
|
|
|
6901
6201
|
if ((locals_js_1.locals.get(chatActiveSessionIteratorsKey) ?? 0) > 0) {
|
|
6902
6202
|
throw new Error("chat.endAndContinue() cannot be called while a chat.createSession() iterator is active. Close the iterator, then call chat.endAndContinue().");
|
|
6903
6203
|
}
|
|
6904
|
-
await performEndAndContinue(
|
|
6204
|
+
await performEndAndContinue();
|
|
6905
6205
|
}
|
|
6906
6206
|
/** @internal Shared server handoff used by managed and custom agent loops. */
|
|
6907
|
-
async function performEndAndContinue(
|
|
6207
|
+
async function performEndAndContinue() {
|
|
6908
6208
|
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
6909
6209
|
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
6910
6210
|
if (!chatId || !callingRunId) {
|
|
6911
6211
|
throw new Error("Cannot end and continue without an active chat agent run");
|
|
6912
6212
|
}
|
|
6913
|
-
const externalDeploymentId = options.externalDeploymentId;
|
|
6914
6213
|
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
6915
|
-
|
|
6214
|
+
await apiClient.endAndContinueSession(chatId, {
|
|
6916
6215
|
callingRunId,
|
|
6917
|
-
reason:
|
|
6918
|
-
...(externalDeploymentId ? { externalDeploymentId } : {}),
|
|
6216
|
+
reason: "upgrade",
|
|
6919
6217
|
});
|
|
6920
|
-
if (result?.pendingVersion !== true) {
|
|
6921
|
-
return;
|
|
6922
|
-
}
|
|
6923
|
-
// The successor parked. Say so on `.out` while this run still can — the transport's
|
|
6924
|
-
// subscription survives the swap, so the client learns without waiting for its next send.
|
|
6925
|
-
const [error] = await (0, v3_1.tryCatch)(getChatSession().out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION));
|
|
6926
|
-
if (error) {
|
|
6927
|
-
v3_1.logger.warn("could not signal a parked handoff", { chatId, error });
|
|
6928
|
-
}
|
|
6929
6218
|
}
|
|
6930
6219
|
/**
|
|
6931
6220
|
* Exit the run after the current turn completes, without waiting for the
|
|
@@ -6956,124 +6245,6 @@ async function performEndAndContinue(options) {
|
|
|
6956
6245
|
function endRun() {
|
|
6957
6246
|
locals_js_1.locals.set(chatEndRunRequestedKey, true);
|
|
6958
6247
|
}
|
|
6959
|
-
/**
|
|
6960
|
-
* End the whole conversation, permanently. The session row is closed, further
|
|
6961
|
-
* appends are refused, and the run exits without scheduling a continuation.
|
|
6962
|
-
*
|
|
6963
|
-
* This is the session-level stop. {@link endRun} ends the current run and lets
|
|
6964
|
-
* the next message start a fresh one; `chat.close()` ends the session itself,
|
|
6965
|
-
* so there is no next message. Use it for a budget cap, a completed goal,
|
|
6966
|
-
* abuse detection, or a user signing out.
|
|
6967
|
-
*
|
|
6968
|
-
* In a `chat.agent`, call it from `run()`, `prepareStep`, or
|
|
6969
|
-
* `onBeforeTurnComplete`. Called mid-step, it aborts the in-flight
|
|
6970
|
-
* `streamText` the same way the stop signal does, so the partial response is
|
|
6971
|
-
* still captured and streamed. The turn then completes normally, a terminal
|
|
6972
|
-
* `session-closed` record carrying `reason` is written to the response stream,
|
|
6973
|
-
* and the loop exits.
|
|
6974
|
-
*
|
|
6975
|
-
* Prefer `onBeforeTurnComplete` over `onTurnComplete`. It carries the same
|
|
6976
|
-
* fields but still runs while the stream is open, so the closed state rides
|
|
6977
|
-
* out on the turn's final record. `onTurnComplete` runs after that record, so
|
|
6978
|
-
* a close decided there does not reach a reader that has already finished the
|
|
6979
|
-
* turn, and the user only finds out when their next message is refused.
|
|
6980
|
-
*
|
|
6981
|
-
* In a `chat.customAgent`, call it anywhere in your own loop. The close is
|
|
6982
|
-
* performed when `run()` returns, so it lands whether you break out of a
|
|
6983
|
-
* `chat.createSession` loop, return early, or hand-roll the loop entirely.
|
|
6984
|
-
*
|
|
6985
|
-
* Closing is one-way: a closed session cannot be reopened. Its transcript
|
|
6986
|
-
* stays readable.
|
|
6987
|
-
*
|
|
6988
|
-
* @example
|
|
6989
|
-
* ```ts
|
|
6990
|
-
* chat.agent({
|
|
6991
|
-
* id: "budgeted-agent",
|
|
6992
|
-
* onBeforeTurnComplete: async ({ usage }) => {
|
|
6993
|
-
* if (await overBudget(usage)) {
|
|
6994
|
-
* chat.close({ reason: "Monthly budget reached" });
|
|
6995
|
-
* }
|
|
6996
|
-
* },
|
|
6997
|
-
* });
|
|
6998
|
-
* ```
|
|
6999
|
-
*/
|
|
7000
|
-
function close(options) {
|
|
7001
|
-
if (!locals_js_1.locals.get(chatExternalIdKey)) {
|
|
7002
|
-
throw new Error("chat.close() can only be called from inside a chat.agent() or chat.customAgent() run");
|
|
7003
|
-
}
|
|
7004
|
-
// Bound the reason once, here. It goes out on S2 record headers as well as
|
|
7005
|
-
// the close API, and an oversized value would fail the turn-complete write
|
|
7006
|
-
// that carries the turn boundary, costing the client far more than the
|
|
7007
|
-
// reason text.
|
|
7008
|
-
// Trailing high surrogate: the cut landed between the two halves of an
|
|
7009
|
-
// astral character, and encoding the orphan to UTF-8 for a record header
|
|
7010
|
-
// yields a replacement character. Drop it rather than ship mojibake.
|
|
7011
|
-
const reason = options?.reason
|
|
7012
|
-
?.slice(0, CHAT_CLOSE_REASON_MAX_LENGTH)
|
|
7013
|
-
.replace(/[\uD800-\uDBFF]$/, "");
|
|
7014
|
-
locals_js_1.locals.set(chatCloseRequestedKey, reason ? { reason } : {});
|
|
7015
|
-
// Mid-step call: unblock the in-flight streamText exactly like the stop
|
|
7016
|
-
// signal, so the turn can reach its turn boundary instead of running the
|
|
7017
|
-
// model out to completion after the decision to close has been made.
|
|
7018
|
-
locals_js_1.locals.get(chatStopControllerKey)?.abort(reason ?? "closed");
|
|
7019
|
-
}
|
|
7020
|
-
/**
|
|
7021
|
-
* @internal Terminal close sequence, run once at whichever exit site observes
|
|
7022
|
-
* the close request. Writes the standalone `session-closed` record, then closes
|
|
7023
|
-
* the session row.
|
|
7024
|
-
*
|
|
7025
|
-
* The record lands after the turn's `turn-complete`, so a client reading that
|
|
7026
|
-
* turn's stream has already terminated on it and will not see this one. It is
|
|
7027
|
-
* there for a reconnect and for replay. What a live client reads is the
|
|
7028
|
-
* `session-closed` header stamped onto `turn-complete` itself by
|
|
7029
|
-
* `writeTurnCompleteChunk`, which fires whenever the close was decided before
|
|
7030
|
-
* the turn ended. A close decided from `onTurnComplete` is past that point, so
|
|
7031
|
-
* the client learns from the 409 on its next send.
|
|
7032
|
-
*/
|
|
7033
|
-
async function performChatClose() {
|
|
7034
|
-
const request = locals_js_1.locals.get(chatCloseRequestedKey);
|
|
7035
|
-
if (!request || locals_js_1.locals.get(chatClosePerformedKey))
|
|
7036
|
-
return;
|
|
7037
|
-
const reason = request.reason;
|
|
7038
|
-
// Two flags, not one. The record is a client-visible event and must not be
|
|
7039
|
-
// written twice, but the row close is the part that actually ends the
|
|
7040
|
-
// conversation: flagging it as done before it succeeds would let a transient
|
|
7041
|
-
// failure leave the session open with no later call willing to retry.
|
|
7042
|
-
if (!locals_js_1.locals.get(chatCloseRecordWrittenKey)) {
|
|
7043
|
-
locals_js_1.locals.set(chatCloseRecordWrittenKey, true);
|
|
7044
|
-
try {
|
|
7045
|
-
const session = getChatSession();
|
|
7046
|
-
await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED, reason ? [[v3_1.SESSION_CLOSED_REASON_HEADER, reason]] : undefined);
|
|
7047
|
-
}
|
|
7048
|
-
catch (error) {
|
|
7049
|
-
v3_1.logger.warn("chat.close: failed to write the session-closed record", {
|
|
7050
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7051
|
-
});
|
|
7052
|
-
}
|
|
7053
|
-
}
|
|
7054
|
-
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
7055
|
-
if (!chatId)
|
|
7056
|
-
return;
|
|
7057
|
-
try {
|
|
7058
|
-
await sessions_js_1.sessions.close(chatId, {
|
|
7059
|
-
...(reason ? { reason } : {}),
|
|
7060
|
-
...(locals_js_1.locals.get(chatAgentRunContextKey)?.run.id
|
|
7061
|
-
? { callingRunId: locals_js_1.locals.get(chatAgentRunContextKey).run.id }
|
|
7062
|
-
: {}),
|
|
7063
|
-
});
|
|
7064
|
-
locals_js_1.locals.set(chatClosePerformedKey, true);
|
|
7065
|
-
}
|
|
7066
|
-
catch (error) {
|
|
7067
|
-
// Deliberately NOT flagged as performed: the close API is idempotent, so a
|
|
7068
|
-
// later exit site on this run gets to retry it. Losing every retry to a
|
|
7069
|
-
// transient failure would leave the row open and the conversation alive.
|
|
7070
|
-
// Non-fatal either way — the run still exits.
|
|
7071
|
-
v3_1.logger.error("chat.close: failed to close the session", {
|
|
7072
|
-
chatId,
|
|
7073
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7074
|
-
});
|
|
7075
|
-
}
|
|
7076
|
-
}
|
|
7077
6248
|
// ---------------------------------------------------------------------------
|
|
7078
6249
|
// Per-turn deferred work
|
|
7079
6250
|
// ---------------------------------------------------------------------------
|
|
@@ -7135,18 +6306,9 @@ function chatDefer(promiseOrFn) {
|
|
|
7135
6306
|
* ```
|
|
7136
6307
|
*/
|
|
7137
6308
|
function injectBackgroundContext(messages) {
|
|
7138
|
-
const
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
const instructions = locals_js_1.locals.get(chatInjectedInstructionsKey) ?? [];
|
|
7142
|
-
instructions.push(...systemBlocks);
|
|
7143
|
-
locals_js_1.locals.set(chatInjectedInstructionsKey, instructions);
|
|
7144
|
-
}
|
|
7145
|
-
if (conversational.length > 0) {
|
|
7146
|
-
const queue = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
7147
|
-
queue.push(...conversational);
|
|
7148
|
-
locals_js_1.locals.set(chatBackgroundQueueKey, queue);
|
|
7149
|
-
}
|
|
6309
|
+
const queue = locals_js_1.locals.get(chatBackgroundQueueKey) ?? [];
|
|
6310
|
+
queue.push(...messages);
|
|
6311
|
+
locals_js_1.locals.set(chatBackgroundQueueKey, queue);
|
|
7150
6312
|
}
|
|
7151
6313
|
// ---------------------------------------------------------------------------
|
|
7152
6314
|
// Aborted message cleanup
|
|
@@ -7559,12 +6721,10 @@ class ChatMessageAccumulator {
|
|
|
7559
6721
|
// a duplicate, mirroring the chat.agent accumulator.
|
|
7560
6722
|
const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
|
|
7561
6723
|
if (existingIdx !== -1) {
|
|
7562
|
-
const previous = this.uiMessages[existingIdx];
|
|
7563
6724
|
this.uiMessages[existingIdx] = response;
|
|
7564
6725
|
try {
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
}
|
|
6726
|
+
// Reconvert all model messages since we replaced rather than appended.
|
|
6727
|
+
this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
|
|
7568
6728
|
}
|
|
7569
6729
|
catch {
|
|
7570
6730
|
// Conversion failed — leave the existing model messages in place
|
|
@@ -7600,28 +6760,6 @@ class ChatMessageAccumulator {
|
|
|
7600
6760
|
const modelMsgs = await toModelMessages([message]);
|
|
7601
6761
|
this._steeringQueue.push({ uiMessage: message, modelMessages: modelMsgs });
|
|
7602
6762
|
}
|
|
7603
|
-
/**
|
|
7604
|
-
* Record the messages a steering drain consumed.
|
|
7605
|
-
*
|
|
7606
|
-
* The drain only puts them in this step's prompt, so without this they
|
|
7607
|
-
* shape one answer and then exist in neither lane: not in `uiMessages`,
|
|
7608
|
-
* which is what an app persists from, and not in `modelMessages`, which is
|
|
7609
|
-
* what every later turn sends.
|
|
7610
|
-
*
|
|
7611
|
-
* Both lanes are appended to. The model lane is never reconverted from the
|
|
7612
|
-
* UI lane, because `compactIfNeeded` replaces it with a summary and leaves
|
|
7613
|
-
* the UI lane whole: a reconversion would restore everything the summary
|
|
7614
|
-
* replaced.
|
|
7615
|
-
*/
|
|
7616
|
-
async absorbSteering(claimed, injected) {
|
|
7617
|
-
const fresh = claimed.filter((m) => !this.uiMessages.some((e) => e.id === m.id));
|
|
7618
|
-
if (fresh.length === 0)
|
|
7619
|
-
return;
|
|
7620
|
-
this.uiMessages.push(...fresh);
|
|
7621
|
-
// Record what the model received. Only when the whole batch is new is
|
|
7622
|
-
// `injected` known to describe exactly these messages.
|
|
7623
|
-
this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
|
|
7624
|
-
}
|
|
7625
6763
|
/**
|
|
7626
6764
|
* Get and clear unconsumed steering messages.
|
|
7627
6765
|
*/
|
|
@@ -7654,8 +6792,7 @@ class ChatMessageAccumulator {
|
|
|
7654
6792
|
}
|
|
7655
6793
|
// 2. Pending message injection
|
|
7656
6794
|
if (pm && queue.length > 0) {
|
|
7657
|
-
const
|
|
7658
|
-
await this.absorbSteering(claimed, injected);
|
|
6795
|
+
const injected = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
|
|
7659
6796
|
if (injected.length > 0) {
|
|
7660
6797
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
7661
6798
|
}
|
|
@@ -7844,7 +6981,7 @@ function trackActiveChatSessionIterator(iterator) {
|
|
|
7844
6981
|
* ```
|
|
7845
6982
|
*/
|
|
7846
6983
|
function createChatSession(payload, options) {
|
|
7847
|
-
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages,
|
|
6984
|
+
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, } = options;
|
|
7848
6985
|
const idleTimeoutInSeconds = sessionIdleTimeoutOpt ?? 30;
|
|
7849
6986
|
return {
|
|
7850
6987
|
[Symbol.asyncIterator]() {
|
|
@@ -7933,16 +7070,8 @@ function createChatSession(payload, options) {
|
|
|
7933
7070
|
* without suspending.
|
|
7934
7071
|
*/
|
|
7935
7072
|
if (turn > 0) {
|
|
7936
|
-
if (locals_js_1.locals.get(chatCloseRequestedKey)) {
|
|
7937
|
-
await performChatClose();
|
|
7938
|
-
stop.cleanup();
|
|
7939
|
-
return { done: true, value: undefined };
|
|
7940
|
-
}
|
|
7941
7073
|
// chat.requestUpgrade() / chat.endRun() — exit before waiting
|
|
7942
7074
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
7943
|
-
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
7944
|
-
await persistUpgradeHandoff();
|
|
7945
|
-
}
|
|
7946
7075
|
stop.cleanup();
|
|
7947
7076
|
return { done: true, value: undefined };
|
|
7948
7077
|
}
|
|
@@ -8045,7 +7174,6 @@ function createChatSession(payload, options) {
|
|
|
8045
7174
|
}
|
|
8046
7175
|
accumulator.applyHandover(pendingHandoverSignal);
|
|
8047
7176
|
}
|
|
8048
|
-
await followSessionPin(currentPayload.chatId, sessionVersionSkew);
|
|
8049
7177
|
// chat.requestUpgrade() called before this turn — signal transport and exit
|
|
8050
7178
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
8051
7179
|
await writeUpgradeRequiredChunk();
|
|
@@ -8262,8 +7390,7 @@ function createChatSession(payload, options) {
|
|
|
8262
7390
|
}
|
|
8263
7391
|
}
|
|
8264
7392
|
if (sessionPendingMessages) {
|
|
8265
|
-
const
|
|
8266
|
-
await accumulator.absorbSteering(claimed, injected);
|
|
7393
|
+
const injected = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
|
|
8267
7394
|
if (injected.length > 0) {
|
|
8268
7395
|
resultMessages = [...(resultMessages ?? stepMsgs), ...injected];
|
|
8269
7396
|
}
|
|
@@ -8277,11 +7404,6 @@ function createChatSession(payload, options) {
|
|
|
8277
7404
|
async return() {
|
|
8278
7405
|
activeMsgSub?.off();
|
|
8279
7406
|
activeMsgSub = undefined;
|
|
8280
|
-
// Reached when the consumer leaves the `for await` early (`break`,
|
|
8281
|
-
// `return`, a throw). A `chat.close()` from the loop body would
|
|
8282
|
-
// otherwise be dropped: the exit that performs it lives in `next()`,
|
|
8283
|
-
// and `next()` is never called again.
|
|
8284
|
-
await performChatClose();
|
|
8285
7407
|
// `stop` only exists once next() has booted the iterator.
|
|
8286
7408
|
stop?.cleanup();
|
|
8287
7409
|
return { done: true, value: undefined };
|
|
@@ -8538,11 +7660,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8538
7660
|
const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
|
|
8539
7661
|
const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
|
|
8540
7662
|
const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
|
|
8541
|
-
// Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
|
|
8542
|
-
// action default, which neither truthiness nor `??` would allow.
|
|
8543
|
-
const externalDeploymentId = params.triggerConfig?.externalDeploymentId !== undefined
|
|
8544
|
-
? params.triggerConfig.externalDeploymentId
|
|
8545
|
-
: options?.triggerConfig?.externalDeploymentId;
|
|
8546
7663
|
const triggerConfig = {
|
|
8547
7664
|
basePayload: {
|
|
8548
7665
|
messages: [],
|
|
@@ -8569,10 +7686,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8569
7686
|
lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
|
|
8570
7687
|
}
|
|
8571
7688
|
: {}),
|
|
8572
|
-
...(params.triggerConfig?.ttl !== undefined || options?.triggerConfig?.ttl !== undefined
|
|
8573
|
-
? { ttl: params.triggerConfig?.ttl ?? options?.triggerConfig?.ttl }
|
|
8574
|
-
: {}),
|
|
8575
|
-
...(externalDeploymentId !== undefined ? { externalDeploymentId } : {}),
|
|
8576
7689
|
...(idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds } : {}),
|
|
8577
7690
|
};
|
|
8578
7691
|
const startBody = {
|
|
@@ -8617,7 +7730,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8617
7730
|
publicAccessToken,
|
|
8618
7731
|
runId: created.runId,
|
|
8619
7732
|
sessionId: created.id,
|
|
8620
|
-
...(created.pendingVersion ? { pendingVersion: true } : {}),
|
|
8621
7733
|
};
|
|
8622
7734
|
};
|
|
8623
7735
|
}
|
|
@@ -8651,8 +7763,7 @@ async function callSessionsCreateWithOverride(args) {
|
|
|
8651
7763
|
const init = {
|
|
8652
7764
|
method: "POST",
|
|
8653
7765
|
headers: overrideRequestHeaders(accessToken),
|
|
8654
|
-
|
|
8655
|
-
body: JSON.stringify((0, externalDeploymentId_js_1.withResolvedExternalDeploymentId)(args.body)),
|
|
7766
|
+
body: JSON.stringify(args.body),
|
|
8656
7767
|
};
|
|
8657
7768
|
const response = args.fetchOverride
|
|
8658
7769
|
? await args.fetchOverride(url, init, ctx)
|
|
@@ -8717,41 +7828,6 @@ async function mintPublicTokenWithOverride(args) {
|
|
|
8717
7828
|
expirationTime: args.expirationTime,
|
|
8718
7829
|
});
|
|
8719
7830
|
}
|
|
8720
|
-
/**
|
|
8721
|
-
* Creates a server-side helper that reads a conversation from a transcript
|
|
8722
|
-
* storage, for rendering history before the chat connects. Works the same
|
|
8723
|
-
* for every storage, the platform default included, so the browser never
|
|
8724
|
-
* reads a store directly and the secret key stays on the server.
|
|
8725
|
-
*
|
|
8726
|
-
* Wrap it in a Next.js server action (or any server-side handler), scope it
|
|
8727
|
-
* to the authenticated user through `clientData`, and pass the result to
|
|
8728
|
-
* `useLoadTranscript` in the browser.
|
|
8729
|
-
*
|
|
8730
|
-
* @example
|
|
8731
|
-
* ```ts
|
|
8732
|
-
* // actions.ts
|
|
8733
|
-
* "use server";
|
|
8734
|
-
* import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
|
|
8735
|
-
*
|
|
8736
|
-
* export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
|
|
8737
|
-
* ```
|
|
8738
|
-
*/
|
|
8739
|
-
function createChatLoadTranscriptAction(storage, options) {
|
|
8740
|
-
return async (params) => {
|
|
8741
|
-
if (!params.chatId) {
|
|
8742
|
-
throw new Error("chat.createLoadTranscriptAction: params.chatId is required.");
|
|
8743
|
-
}
|
|
8744
|
-
if (options?.apiClient) {
|
|
8745
|
-
const { apiClient, ...rest } = options;
|
|
8746
|
-
return v3_1.apiClientManager.runWithConfig(apiClient, () => createChatLoadTranscriptAction(storage, rest)(params));
|
|
8747
|
-
}
|
|
8748
|
-
const limit = params.limit ?? options?.limit;
|
|
8749
|
-
return storage.load({ chatId: params.chatId, clientData: params.clientData }, {
|
|
8750
|
-
...(limit !== undefined ? { limit } : {}),
|
|
8751
|
-
...(params.before !== undefined ? { before: params.before } : {}),
|
|
8752
|
-
});
|
|
8753
|
-
};
|
|
8754
|
-
}
|
|
8755
7831
|
exports.chat = {
|
|
8756
7832
|
/** Create a chat agent. See {@link chatAgent}. */
|
|
8757
7833
|
agent: chatAgent,
|
|
@@ -8763,12 +7839,8 @@ exports.chat = {
|
|
|
8763
7839
|
withClientData,
|
|
8764
7840
|
/** Create a server-side helper for starting (or resuming) a Session for a chatId. See {@link createChatStartSessionAction}. */
|
|
8765
7841
|
createStartSessionAction: createChatStartSessionAction,
|
|
8766
|
-
/** Returns a server-side helper that reads a conversation from a transcript storage. */
|
|
8767
|
-
createLoadTranscriptAction: createChatLoadTranscriptAction,
|
|
8768
7842
|
/** Pipe a stream to the chat transport. See {@link pipeChat}. */
|
|
8769
7843
|
pipe: pipeChat,
|
|
8770
|
-
/** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */
|
|
8771
|
-
turn: chatTurn,
|
|
8772
7844
|
/** Create a per-run typed local. See {@link chatLocal}. */
|
|
8773
7845
|
local: chatLocal,
|
|
8774
7846
|
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
|
|
@@ -8789,8 +7861,6 @@ exports.chat = {
|
|
|
8789
7861
|
endAndContinue,
|
|
8790
7862
|
/** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */
|
|
8791
7863
|
endRun,
|
|
8792
|
-
/** End the conversation permanently: close the session and exit the run. See {@link close}. */
|
|
8793
|
-
close,
|
|
8794
7864
|
/** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
|
|
8795
7865
|
cleanupAbortedParts,
|
|
8796
7866
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
@@ -8938,16 +8008,6 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
8938
8008
|
if (consumedCursor !== undefined) {
|
|
8939
8009
|
extraHeaders.push([v3_1.SESSION_IN_CONSUMED_ID_HEADER, String(consumedCursor)]);
|
|
8940
8010
|
}
|
|
8941
|
-
// A close decided before this turn ended rides out on turn-complete. Readers
|
|
8942
|
-
// terminate their stream on turn-complete, so a standalone record written
|
|
8943
|
-
// after it only reaches a reconnect — this header is what a live client sees.
|
|
8944
|
-
const pendingClose = locals_js_1.locals.get(chatCloseRequestedKey);
|
|
8945
|
-
if (pendingClose) {
|
|
8946
|
-
extraHeaders.push([v3_1.SESSION_CLOSED_HEADER, "true"]);
|
|
8947
|
-
if (pendingClose.reason) {
|
|
8948
|
-
extraHeaders.push([v3_1.SESSION_CLOSED_REASON_HEADER, pendingClose.reason]);
|
|
8949
|
-
}
|
|
8950
|
-
}
|
|
8951
8011
|
const result = await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE, extraHeaders);
|
|
8952
8012
|
const T_N = result.lastEventId ? Number.parseInt(result.lastEventId, 10) : undefined;
|
|
8953
8013
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
@@ -9005,47 +8065,12 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
9005
8065
|
*
|
|
9006
8066
|
* @internal
|
|
9007
8067
|
*/
|
|
9008
|
-
/**
|
|
9009
|
-
* Persists an upgrade requested after the turn has already run.
|
|
9010
|
-
*
|
|
9011
|
-
* The pre-turn sites reach {@link performEndAndContinue} through
|
|
9012
|
-
* {@link writeUpgradeRequiredChunk}, which is what clears (or re-points) the
|
|
9013
|
-
* session's stored `externalDeploymentId`. The post-turn exits had no such path,
|
|
9014
|
-
* so a `chat.requestUpgrade()` from `run()` or `chat.defer()` left the pin intact
|
|
9015
|
-
* and every continuation re-pinned to the deployment the agent asked to leave.
|
|
9016
|
-
*
|
|
9017
|
-
* No `upgrade-required` chunk is written here: the turn already produced its
|
|
9018
|
-
* answer, so there is nothing for a client to be told about.
|
|
9019
|
-
*/
|
|
9020
|
-
async function persistUpgradeHandoff() {
|
|
9021
|
-
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
9022
|
-
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
9023
|
-
if (!chatId || !callingRunId) {
|
|
9024
|
-
return;
|
|
9025
|
-
}
|
|
9026
|
-
try {
|
|
9027
|
-
await performEndAndContinue({
|
|
9028
|
-
reason: "upgrade",
|
|
9029
|
-
externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
9030
|
-
});
|
|
9031
|
-
}
|
|
9032
|
-
catch (error) {
|
|
9033
|
-
v3_1.logger.warn("upgrade handoff failed; session keeps its current version pin", {
|
|
9034
|
-
chatId,
|
|
9035
|
-
callingRunId,
|
|
9036
|
-
error,
|
|
9037
|
-
});
|
|
9038
|
-
}
|
|
9039
|
-
}
|
|
9040
8068
|
async function writeUpgradeRequiredChunk() {
|
|
9041
8069
|
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
9042
8070
|
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
9043
8071
|
if (chatId && callingRunId) {
|
|
9044
8072
|
try {
|
|
9045
|
-
await performEndAndContinue(
|
|
9046
|
-
reason: "upgrade",
|
|
9047
|
-
externalDeploymentId: locals_js_1.locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
9048
|
-
});
|
|
8073
|
+
await performEndAndContinue();
|
|
9049
8074
|
}
|
|
9050
8075
|
catch (error) {
|
|
9051
8076
|
// Non-fatal: the next `.in/append` re-triggers via the probe.
|