@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/esm/v3/ai.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
import { accessoryAttributes, apiClientManager, controlSubtype, generateJWT, getSchemaParseFn, headerValue, InputStreamOncePromise, isAdditionalApiKey, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, resourceCatalog, SemanticInternalAttributes, SESSION_IN_CONSUMED_ID_HEADER, SESSION_IN_EVENT_ID_HEADER, sessionStreams, SessionChannelRouter, InputStreamTimeoutError, taskContext, TRIGGER_CONTROL_SUBTYPE,
|
|
1
|
+
import { accessoryAttributes, apiClientManager, controlSubtype, generateJWT, getSchemaParseFn, headerValue, InputStreamOncePromise, isAdditionalApiKey, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, resourceCatalog, SemanticInternalAttributes, SESSION_IN_CONSUMED_ID_HEADER, SESSION_IN_EVENT_ID_HEADER, sessionStreams, SessionChannelRouter, InputStreamTimeoutError, taskContext, TRIGGER_CONTROL_SUBTYPE, } from "@trigger.dev/core/v3";
|
|
2
2
|
// Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
|
|
3
3
|
// ESM-only `ai@7` (see ../imports/ai-runtime.ts).
|
|
4
4
|
import { trace } from "@opentelemetry/api";
|
|
5
|
-
import { tool as aiTool, convertToModelMessages, dynamicTool, generateId as generateMessageId, getToolName, isToolUIPart, jsonSchema, readUIMessageStream,
|
|
6
|
-
import { createTranscriptShadow, defaultStorage, diffTranscript, parseTranscriptRuntimeState, prefixFingerprint, restoreModelLane, } from "./transcriptStorage.js";
|
|
7
|
-
let transcriptStorageOverride;
|
|
8
|
-
/**
|
|
9
|
-
* Test-only override for the storage `chat.agent` persists through, so a
|
|
10
|
-
* test can capture the exact changesets the runtime produces.
|
|
11
|
-
* @internal
|
|
12
|
-
*/
|
|
13
|
-
export function __setTranscriptStorageForTests(storage) {
|
|
14
|
-
transcriptStorageOverride = storage;
|
|
15
|
-
}
|
|
5
|
+
import { tool as aiTool, convertToModelMessages, dynamicTool, generateId as generateMessageId, getToolName, isToolUIPart, jsonSchema, readUIMessageStream, zodSchema, } from "../imports/ai-runtime.js";
|
|
16
6
|
import { PENDING_MESSAGE_INJECTED_TYPE, upsertIncomingMessage, chatRunTags, } from "./ai-shared.js";
|
|
17
7
|
import { auth } from "./auth.js";
|
|
18
8
|
import { locals } from "./locals.js";
|
|
@@ -27,8 +17,6 @@ import { metadata } from "./metadata.js";
|
|
|
27
17
|
// pulled in transitively here never reach a client chunk.
|
|
28
18
|
import { readFileInSkill, runBashInSkill } from "./agentSkillsRuntime.js";
|
|
29
19
|
import { ensureAiSdkTelemetry } from "./aiAutoTelemetry.js";
|
|
30
|
-
import { withResolvedExternalDeploymentId } from "./externalDeploymentId.js";
|
|
31
|
-
import { resolvePinToFollow } from "./chatVersionSkew.js";
|
|
32
20
|
import { sessions, } from "./sessions.js";
|
|
33
21
|
import { createTask } from "./shared.js";
|
|
34
22
|
import { markChatAgentRunForStreamsWarning } from "./streams.js";
|
|
@@ -115,8 +103,182 @@ async function findLatestSessionInCursor(chatId) {
|
|
|
115
103
|
}
|
|
116
104
|
return latestCursor;
|
|
117
105
|
}
|
|
118
|
-
|
|
119
|
-
export
|
|
106
|
+
let readChatSnapshotImpl;
|
|
107
|
+
export function __setReadChatSnapshotImplForTests(impl) {
|
|
108
|
+
readChatSnapshotImpl = impl;
|
|
109
|
+
}
|
|
110
|
+
let writeChatSnapshotImpl;
|
|
111
|
+
export function __setWriteChatSnapshotImplForTests(impl) {
|
|
112
|
+
writeChatSnapshotImpl = impl;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Read the persisted snapshot for a session. Returns `undefined` on:
|
|
116
|
+
* - missing object (404 from the presigned GET — fresh session, never
|
|
117
|
+
* persisted)
|
|
118
|
+
* - presign failure (network/auth issue)
|
|
119
|
+
* - malformed JSON
|
|
120
|
+
* - version mismatch (forward-compat — older runtimes ignore newer blobs)
|
|
121
|
+
*
|
|
122
|
+
* Always swallows errors via `logger.warn`. The agent boot loop must stay
|
|
123
|
+
* available even if S3 hiccups; the worst case is replaying more of
|
|
124
|
+
* `session.out` than strictly necessary.
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
async function readChatSnapshot(sessionId) {
|
|
128
|
+
if (readChatSnapshotImpl) {
|
|
129
|
+
return (await readChatSnapshotImpl(sessionId)) ?? undefined;
|
|
130
|
+
}
|
|
131
|
+
const apiClient = apiClientManager.clientOrThrow();
|
|
132
|
+
let presignedUrl;
|
|
133
|
+
try {
|
|
134
|
+
const resp = await apiClient.getChatSnapshotUrl(sessionId);
|
|
135
|
+
presignedUrl = resp.presignedUrl;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
logger.warn("chat.agent: snapshot presign (read) failed; continuing without snapshot", {
|
|
139
|
+
error: error instanceof Error ? error.message : String(error),
|
|
140
|
+
sessionId,
|
|
141
|
+
});
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
let response;
|
|
145
|
+
try {
|
|
146
|
+
response = await fetch(presignedUrl, { method: "GET" });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
logger.warn("chat.agent: snapshot fetch failed; continuing without snapshot", {
|
|
150
|
+
error: error instanceof Error ? error.message : String(error),
|
|
151
|
+
sessionId,
|
|
152
|
+
});
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
if (response.status === 404) {
|
|
156
|
+
// First-ever boot for this session — no snapshot yet. Caller falls
|
|
157
|
+
// through to replay-only.
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
logger.warn("chat.agent: snapshot fetch returned non-OK; continuing without snapshot", {
|
|
162
|
+
status: response.status,
|
|
163
|
+
sessionId,
|
|
164
|
+
});
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = await response.json();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
logger.warn("chat.agent: snapshot JSON parse failed; continuing without snapshot", {
|
|
173
|
+
error: error instanceof Error ? error.message : String(error),
|
|
174
|
+
sessionId,
|
|
175
|
+
});
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (!parsed || typeof parsed !== "object")
|
|
179
|
+
return undefined;
|
|
180
|
+
const candidate = parsed;
|
|
181
|
+
if (candidate.version !== 1 || !Array.isArray(candidate.messages)) {
|
|
182
|
+
logger.warn("chat.agent: snapshot version/shape mismatch; ignoring", {
|
|
183
|
+
version: candidate.version,
|
|
184
|
+
sessionId,
|
|
185
|
+
});
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return candidate;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Persist the snapshot for a session. Awaited by callers immediately after
|
|
192
|
+
* `onTurnComplete` — the agent may suspend right after this point, and
|
|
193
|
+
* fire-and-forget promises don't reliably complete on suspend.
|
|
194
|
+
*
|
|
195
|
+
* Errors are swallowed via `logger.warn`. A failed write means the next
|
|
196
|
+
* boot replays slightly more of `session.out` (back to the previous
|
|
197
|
+
* snapshot's cursor) instead of failing — the conversation stays
|
|
198
|
+
* coherent, only the boot path does marginally more work.
|
|
199
|
+
* @internal
|
|
200
|
+
*/
|
|
201
|
+
async function writeChatSnapshot(sessionId, snapshot) {
|
|
202
|
+
if (writeChatSnapshotImpl) {
|
|
203
|
+
await writeChatSnapshotImpl(sessionId, snapshot);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const apiClient = apiClientManager.clientOrThrow();
|
|
207
|
+
let presignedUrl;
|
|
208
|
+
try {
|
|
209
|
+
const resp = await apiClient.createChatSnapshotUploadUrl(sessionId);
|
|
210
|
+
presignedUrl = resp.presignedUrl;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
logger.warn("chat.agent: snapshot presign (write) failed; next run will replay further", {
|
|
214
|
+
error: error instanceof Error ? error.message : String(error),
|
|
215
|
+
sessionId,
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let response;
|
|
220
|
+
try {
|
|
221
|
+
response = await fetch(presignedUrl, {
|
|
222
|
+
method: "PUT",
|
|
223
|
+
headers: { "content-type": "application/json" },
|
|
224
|
+
body: JSON.stringify(snapshot),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
logger.warn("chat.agent: snapshot upload failed; next run will replay further", {
|
|
229
|
+
error: error instanceof Error ? error.message : String(error),
|
|
230
|
+
sessionId,
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
logger.warn("chat.agent: snapshot upload returned non-OK; next run will replay further", {
|
|
236
|
+
status: response.status,
|
|
237
|
+
sessionId,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Test-only entry point that bypasses `__setReadChatSnapshotImplForTests`
|
|
243
|
+
* and reaches the real `apiClient.getPayloadUrl` + `fetch` + JSON-parse path.
|
|
244
|
+
* Used by `chat-snapshot.test.ts` to verify 404 / 500 / malformed JSON /
|
|
245
|
+
* version-mismatch / network-error behavior end-to-end. Tests mock global
|
|
246
|
+
* `fetch` and the api-client config; this wrapper lets them drive the
|
|
247
|
+
* production code without the override hook short-circuiting.
|
|
248
|
+
*
|
|
249
|
+
* Not part of the public API. The `__` prefix and `ForTests` suffix mirror
|
|
250
|
+
* the override-hook setters above.
|
|
251
|
+
* @internal
|
|
252
|
+
*/
|
|
253
|
+
export async function __readChatSnapshotProductionPathForTests(sessionId) {
|
|
254
|
+
const saved = readChatSnapshotImpl;
|
|
255
|
+
readChatSnapshotImpl = undefined;
|
|
256
|
+
try {
|
|
257
|
+
return await readChatSnapshot(sessionId);
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
readChatSnapshotImpl = saved;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Test-only entry point that bypasses `__setWriteChatSnapshotImplForTests`
|
|
265
|
+
* and reaches the real `apiClient.createUploadPayloadUrl` + `fetch` PUT
|
|
266
|
+
* path. Pairs with `__readChatSnapshotProductionPathForTests` — see that
|
|
267
|
+
* function's note for the rationale.
|
|
268
|
+
*
|
|
269
|
+
* Not part of the public API.
|
|
270
|
+
* @internal
|
|
271
|
+
*/
|
|
272
|
+
export async function __writeChatSnapshotProductionPathForTests(sessionId, snapshot) {
|
|
273
|
+
const saved = writeChatSnapshotImpl;
|
|
274
|
+
writeChatSnapshotImpl = undefined;
|
|
275
|
+
try {
|
|
276
|
+
await writeChatSnapshot(sessionId, snapshot);
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
writeChatSnapshotImpl = saved;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
120
282
|
/**
|
|
121
283
|
* Merge two `UIMessage[]` lists by `id`, with the second list winning on
|
|
122
284
|
* collision. Used at run boot to combine the snapshot's persisted history
|
|
@@ -1592,13 +1754,6 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1592
1754
|
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? replayWindowEnd, replayWindowEnd);
|
|
1593
1755
|
}
|
|
1594
1756
|
}
|
|
1595
|
-
// A boot that replayed `.in` itself has already answered everything up to
|
|
1596
|
-
// `recoveredThrough`, so the floor has to cover it before the tail opens.
|
|
1597
|
-
if (options?.recoveredThrough !== undefined) {
|
|
1598
|
-
const recovered = options.recoveredThrough;
|
|
1599
|
-
checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
|
|
1600
|
-
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? checkpoint.resumeFrom, checkpoint.resumeFrom);
|
|
1601
|
-
}
|
|
1602
1757
|
const router = entry.router;
|
|
1603
1758
|
router.restore(checkpoint);
|
|
1604
1759
|
const floor = router.resumeFrom();
|
|
@@ -1607,10 +1762,6 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1607
1762
|
sessionStreams.setLastDispatchedSeqNum(chatId, "in", floor);
|
|
1608
1763
|
}
|
|
1609
1764
|
sessionStreams.onRecord(chatId, "in", (record) => {
|
|
1610
|
-
// The floor is the tail's `Last-Event-ID`, but a reconnect can still
|
|
1611
|
-
// re-deliver below it and a replayable route would re-queue it.
|
|
1612
|
-
if (floor !== undefined && record.seqNum <= floor)
|
|
1613
|
-
return true;
|
|
1614
1765
|
router.ingest(record);
|
|
1615
1766
|
return true;
|
|
1616
1767
|
});
|
|
@@ -1782,35 +1933,6 @@ function spliceHandoverPartial(modelMessages, uiMessages, signal) {
|
|
|
1782
1933
|
* @internal
|
|
1783
1934
|
*/
|
|
1784
1935
|
const chatBackgroundQueueKey = locals.create("chat.backgroundQueue");
|
|
1785
|
-
/**
|
|
1786
|
-
* Background injections a step-boundary drain handed to the model this turn,
|
|
1787
|
-
* with the transcript message they followed. Reconciled into the model lane
|
|
1788
|
-
* and the persisted injections once the turn's response is in.
|
|
1789
|
-
*/
|
|
1790
|
-
const chatPendingBackgroundKey = locals.create("chat.pendingBackground");
|
|
1791
|
-
/**
|
|
1792
|
-
* System-role context injected mid-conversation, held for the instructions lane.
|
|
1793
|
-
*
|
|
1794
|
-
* Kept apart from the message queue because ai@7 rejects a system message inside
|
|
1795
|
-
* `messages` for every provider — `standardizePrompt` throws upstream of any
|
|
1796
|
-
* provider call, and its own advice is to use the instructions option. Instructions
|
|
1797
|
-
* accept `Array<SystemModelMessage>`, so a system-role injection has a correct
|
|
1798
|
-
* home: appended as another system block rather than smuggled into the transcript.
|
|
1799
|
-
*
|
|
1800
|
-
* This is also the only way to inject *trusted* context. A message injected as
|
|
1801
|
-
* `user` is untrusted by construction, and a well-aligned model treats it that
|
|
1802
|
-
* way — it will say so, and re-derive the answer from tools instead.
|
|
1803
|
-
*/
|
|
1804
|
-
const chatInjectedInstructionsKey = locals.create("chat.injectedInstructions");
|
|
1805
|
-
/**
|
|
1806
|
-
* What a turn already consumed from the instructions lane, so a second
|
|
1807
|
-
* `toStreamTextOptions()` call in the same turn sees the same blocks.
|
|
1808
|
-
*
|
|
1809
|
-
* Consumed blocks are moved here rather than left in the pending lane: leaving
|
|
1810
|
-
* them there means an injection made during the consumed turn sits behind them,
|
|
1811
|
-
* and clearing the lane on the next turn destroys both.
|
|
1812
|
-
*/
|
|
1813
|
-
const chatInstructionsConsumedKey = locals.create("chat.injectedInstructionsConsumed");
|
|
1814
1936
|
/**
|
|
1815
1937
|
* Run-scoped pipe counter. Stored in locals so concurrent runs in the
|
|
1816
1938
|
* same worker don't share state.
|
|
@@ -2321,31 +2443,12 @@ const chatToolsOptionKey = locals.create("chat.toolsOption");
|
|
|
2321
2443
|
const chatResolvedToolsKey = locals.create("chat.resolvedTools");
|
|
2322
2444
|
/** @internal Flag set by `chat.requestUpgrade()` to exit the loop after the current turn. */
|
|
2323
2445
|
const chatUpgradeRequestedKey = locals.create("chat.upgradeRequested");
|
|
2324
|
-
/** @internal Target for the upgrade handoff, set by `chat.requestUpgrade({ externalDeploymentId })`. */
|
|
2325
|
-
const chatUpgradeExternalDeploymentIdKey = locals.create("chat.upgradeExternalDeploymentId");
|
|
2326
2446
|
/**
|
|
2327
2447
|
* @internal Flag set by `chat.endRun()` to exit the loop after the current
|
|
2328
2448
|
* turn completes, without any upgrade semantics. Checked at the same
|
|
2329
2449
|
* post-turn / pre-wait sites as `chatUpgradeRequestedKey`.
|
|
2330
2450
|
*/
|
|
2331
2451
|
const chatEndRunRequestedKey = locals.create("chat.endRunRequested");
|
|
2332
|
-
/**
|
|
2333
|
-
* @internal Set by `chat.close()`. Holds the close request (and its reason)
|
|
2334
|
-
* for the rest of the run: the loop writes the terminal `session-closed`
|
|
2335
|
-
* record, closes the session row, and exits at the same post-turn /
|
|
2336
|
-
* pre-wait sites as `chatEndRunRequestedKey`.
|
|
2337
|
-
*/
|
|
2338
|
-
const chatCloseRequestedKey = locals.create("chat.closeRequested");
|
|
2339
|
-
/** @internal Matches the server's `CloseSessionRequestBody.reason` cap. */
|
|
2340
|
-
const CHAT_CLOSE_REASON_MAX_LENGTH = 256;
|
|
2341
|
-
/** @internal Set once the session row is closed, so the close happens once. */
|
|
2342
|
-
const chatClosePerformedKey = locals.create("chat.closePerformed");
|
|
2343
|
-
/**
|
|
2344
|
-
* @internal Set once the terminal `.out` record is written. Tracked apart from
|
|
2345
|
-
* {@link chatClosePerformedKey} so a retried close does not emit a second
|
|
2346
|
-
* client-visible event.
|
|
2347
|
-
*/
|
|
2348
|
-
const chatCloseRecordWrittenKey = locals.create("chat.closeRecordWritten");
|
|
2349
2452
|
/** @internal */
|
|
2350
2453
|
const chatAgentCompactionKey = locals.create("chat.agentCompaction");
|
|
2351
2454
|
/**
|
|
@@ -2362,32 +2465,6 @@ export { PENDING_MESSAGE_INJECTED_TYPE, upsertIncomingMessage };
|
|
|
2362
2465
|
const chatPendingMessagesKey = locals.create("chat.pendingMessages");
|
|
2363
2466
|
/** @internal */
|
|
2364
2467
|
const chatSteeringQueueKey = locals.create("chat.steeringQueue");
|
|
2365
|
-
/**
|
|
2366
|
-
* This turn's new messages, as `onTurnComplete.newUIMessages` will see them.
|
|
2367
|
-
*
|
|
2368
|
-
* Held in locals because `drainSteeringQueue` runs outside the turn closure and
|
|
2369
|
-
* has to append the messages it injects. Without that, an injected message
|
|
2370
|
-
* reaches the model and the browser but no hook, so an app persisting from
|
|
2371
|
-
* `onTurnComplete` never learns it existed.
|
|
2372
|
-
*/
|
|
2373
|
-
const chatTurnNewUIMessagesKey = locals.create("chat.turnNewUIMessages");
|
|
2374
|
-
/**
|
|
2375
|
-
* Steering messages a drain consumed that the model accumulator has not been
|
|
2376
|
-
* given yet.
|
|
2377
|
-
*
|
|
2378
|
-
* The two accumulators are maintained separately, and the model one is
|
|
2379
|
-
* normally advanced by appending each turn's delta. A drained message is
|
|
2380
|
-
* appended to the UI one but reaches the model only through the `prepareStep`
|
|
2381
|
-
* return value, which is per-step: without this the model lane never learns
|
|
2382
|
-
* the message exists and every later turn of the run answers without it,
|
|
2383
|
-
* while the browser, the snapshot and `chat.history.*` all still show it.
|
|
2384
|
-
*
|
|
2385
|
-
* Held as the messages rather than a "rebuild me" flag because the model lane
|
|
2386
|
-
* can only be appended to, never reconstructed. Compaction replaces it with a
|
|
2387
|
-
* summary and deliberately leaves the UI lane whole, so reconverting the UI
|
|
2388
|
-
* lane restores every message the summary replaced.
|
|
2389
|
-
*/
|
|
2390
|
-
const chatPendingSteerKey = locals.create("chat.pendingSteer");
|
|
2391
2468
|
/** @internal — IDs of messages that were successfully injected via prepareStep */
|
|
2392
2469
|
const chatInjectedMessageIdsKey = locals.create("chat.injectedMessageIds");
|
|
2393
2470
|
/** @internal — non-transient data parts queued via chat.response or writer.write() for accumulation into the response message */
|
|
@@ -2740,32 +2817,20 @@ function chatCompactionStep(options) {
|
|
|
2740
2817
|
return result.type === "skipped" ? undefined : result;
|
|
2741
2818
|
};
|
|
2742
2819
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
* each entry's own conversion is used. With it, `prepare` returned one list
|
|
2747
|
-
* for the whole batch, so the first claimed message carries all of it and the
|
|
2748
|
-
* rest carry none, which keeps the total exactly what the model received.
|
|
2749
|
-
*/
|
|
2750
|
-
function modelFormOf(m, batch, injected) {
|
|
2751
|
-
return batch[0] === m ? injected : [];
|
|
2752
|
-
}
|
|
2820
|
+
// ---------------------------------------------------------------------------
|
|
2821
|
+
// Steering queue drain — shared by toStreamTextOptions, session, accumulator
|
|
2822
|
+
// ---------------------------------------------------------------------------
|
|
2753
2823
|
/**
|
|
2754
2824
|
* Drain the steering queue as a batch. Calls `shouldInject` once with all
|
|
2755
2825
|
* pending messages. If it returns true, calls `prepareMessages` once to
|
|
2756
2826
|
* transform the batch, then clears the queue.
|
|
2757
|
-
* Returns the model messages to inject
|
|
2758
|
-
*
|
|
2759
|
-
* `claimed` is returned rather than only published to locals because each
|
|
2760
|
-
* surface files it somewhere different: `chat.agent` has an accumulator in
|
|
2761
|
-
* locals, while `chat.createSession` keeps its own. Publishing to locals alone
|
|
2762
|
-
* is silently a no-op for any surface that never set the key.
|
|
2827
|
+
* Returns the model messages to inject (empty if none).
|
|
2763
2828
|
* @internal
|
|
2764
2829
|
*/
|
|
2765
2830
|
async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
2766
2831
|
const queue = queueOverride ?? locals.get(chatSteeringQueueKey);
|
|
2767
2832
|
if (!queue || queue.length === 0)
|
|
2768
|
-
return
|
|
2833
|
+
return [];
|
|
2769
2834
|
const ctx = locals.get(chatTurnContextKey);
|
|
2770
2835
|
const stepNumber = steps.length - 1;
|
|
2771
2836
|
/**
|
|
@@ -2788,7 +2853,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2788
2853
|
// Call shouldInject once for the whole batch
|
|
2789
2854
|
const shouldInject = config.shouldInject ? await config.shouldInject(batchEvent) : false;
|
|
2790
2855
|
if (!shouldInject)
|
|
2791
|
-
return
|
|
2856
|
+
return [];
|
|
2792
2857
|
const textOfUIMessage = (m) => (m.parts ?? [])
|
|
2793
2858
|
.filter((p) => p.type === "text")
|
|
2794
2859
|
.map((p) => p.text)
|
|
@@ -2834,7 +2899,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2834
2899
|
queue.splice(at, 1);
|
|
2835
2900
|
}
|
|
2836
2901
|
if (claimed.length === 0)
|
|
2837
|
-
return
|
|
2902
|
+
return [];
|
|
2838
2903
|
/**
|
|
2839
2904
|
* Give the claim back if the transform fails. `prepare` is caller code and
|
|
2840
2905
|
* can throw; the records have already left the router by this point, so
|
|
@@ -2864,37 +2929,6 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2864
2929
|
for (const m of claimedUIMessages)
|
|
2865
2930
|
injectedIds.add(m.id);
|
|
2866
2931
|
}
|
|
2867
|
-
// Record them as part of the conversation.
|
|
2868
|
-
//
|
|
2869
|
-
// The model has them and the browser has them; without this the
|
|
2870
|
-
// accumulator does not, so they reach neither `uiMessages` nor
|
|
2871
|
-
// `newUIMessages` on `onTurnComplete` and an app that persists from there
|
|
2872
|
-
// silently loses the instruction the answer was shaped by. Appending here
|
|
2873
|
-
// rather than at turn end keeps them in the order they happened: after the
|
|
2874
|
-
// message that started the turn, before the response that answers it.
|
|
2875
|
-
//
|
|
2876
|
-
// De-duplicated by id because a step boundary can drain more than once per
|
|
2877
|
-
// turn, and because a message that failed to inject falls back to becoming
|
|
2878
|
-
// its own turn, where it is accumulated the normal way.
|
|
2879
|
-
const currentUIMessages = locals.get(chatCurrentUIMessagesKey);
|
|
2880
|
-
const turnNew = locals.get(chatTurnNewUIMessagesKey);
|
|
2881
|
-
for (const m of claimedUIMessages) {
|
|
2882
|
-
if (currentUIMessages && !currentUIMessages.some((existing) => existing.id === m.id)) {
|
|
2883
|
-
currentUIMessages.push(m);
|
|
2884
|
-
}
|
|
2885
|
-
if (turnNew && !turnNew.some((existing) => existing.id === m.id)) {
|
|
2886
|
-
turnNew.push(m);
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
if (claimedUIMessages.length > 0 && currentUIMessages) {
|
|
2890
|
-
const pendingSteer = locals.get(chatPendingSteerKey) ?? [];
|
|
2891
|
-
for (const m of claimedUIMessages) {
|
|
2892
|
-
if (!pendingSteer.some((existing) => existing.ui.id === m.id)) {
|
|
2893
|
-
pendingSteer.push({ ui: m, model: modelFormOf(m, claimedUIMessages, injected) });
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
locals.set(chatPendingSteerKey, pendingSteer);
|
|
2897
|
-
}
|
|
2898
2932
|
// Write injection confirmation chunk to the stream so the frontend
|
|
2899
2933
|
// knows which messages were injected and where in the response.
|
|
2900
2934
|
if (injected.length > 0) {
|
|
@@ -2936,7 +2970,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2936
2970
|
/* non-fatal */
|
|
2937
2971
|
}
|
|
2938
2972
|
}
|
|
2939
|
-
return
|
|
2973
|
+
return injected;
|
|
2940
2974
|
}, {
|
|
2941
2975
|
attributes: {
|
|
2942
2976
|
[SemanticInternalAttributes.STYLE_ICON]: "tabler-message-forward",
|
|
@@ -3162,149 +3196,32 @@ export function buildSkillTools(skills) {
|
|
|
3162
3196
|
return { loadSkill, readFile, bash };
|
|
3163
3197
|
}
|
|
3164
3198
|
/**
|
|
3165
|
-
*
|
|
3166
|
-
*
|
|
3167
|
-
*
|
|
3168
|
-
*
|
|
3169
|
-
*
|
|
3170
|
-
*
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3173
|
-
* Caller options win for everything the caller owns (model, messages, signal,
|
|
3174
|
-
* stopWhen). The three that would otherwise clobber managed behaviour are
|
|
3175
|
-
* merged instead of replaced:
|
|
3176
|
-
*
|
|
3177
|
-
* - `tools` are passed into the helper, so skill tools survive.
|
|
3178
|
-
* - `prepareStep` is composed after the managed one, so a caller's per-step
|
|
3179
|
-
* overrides apply on top of steering and compaction instead of disabling them.
|
|
3180
|
-
*
|
|
3181
|
-
* `system` may be set at the call site, on `chat.agent({ system })`, or
|
|
3182
|
-
* through `chat.prompt.set()`, but only in one of them. Setting it in two
|
|
3183
|
-
* places throws: no shape merges two system values on every supported
|
|
3184
|
-
* version, and dropping one silently is the failure this seam exists to
|
|
3185
|
-
* prevent. Injected instructions append to whichever one is in play.
|
|
3186
|
-
*/
|
|
3187
|
-
/**
|
|
3188
|
-
* The agent-level managed options (`registry`, `system`, `cacheControl`,
|
|
3189
|
-
* `systemProviderOptions`), published for the run so that
|
|
3190
|
-
* `chat.toStreamTextOptions()` applies them too. Without this only the bound
|
|
3191
|
-
* `streamText` saw them, and the documented spread form silently ran without
|
|
3192
|
-
* the agent's system prompt or model.
|
|
3193
|
-
*/
|
|
3194
|
-
const chatAgentManagedConfigKey = locals.create("chat.agentManagedConfig");
|
|
3195
|
-
/**
|
|
3196
|
-
* The caller's `streamText` options merged with the agent's managed ones.
|
|
3199
|
+
* Returns an options object ready to spread into `streamText()`.
|
|
3200
|
+
*
|
|
3201
|
+
* Includes `system`, `experimental_telemetry`, and any config fields
|
|
3202
|
+
* (temperature, maxTokens, etc.) from the stored prompt.
|
|
3203
|
+
*
|
|
3204
|
+
* When a `registry` is provided and the prompt has a `model` string,
|
|
3205
|
+
* the resolved `LanguageModel` is included as `model`.
|
|
3197
3206
|
*
|
|
3198
|
-
*
|
|
3199
|
-
* the caller did not name has to survive the merge, and the way to be sure of
|
|
3200
|
-
* that is to look at the merged object rather than at what the model received.
|
|
3207
|
+
* If no prompt has been set, returns `{}` (no-op spread).
|
|
3201
3208
|
*/
|
|
3202
|
-
function buildManagedStreamTextOptions(options, config) {
|
|
3203
|
-
const { registry, system: agentSystem, cacheControl, systemProviderOptions, tools: agentTools, } = config;
|
|
3204
|
-
/**
|
|
3205
|
-
* Only the three keys that collide are intercepted. Everything else, telemetry
|
|
3206
|
-
* included, stays in `rest` and reaches `streamText` untouched, with the
|
|
3207
|
-
* caller's value winning because `rest` is spread after `managed`. Pulling a
|
|
3208
|
-
* key out to "handle" it is how a caller's option gets silently dropped.
|
|
3209
|
-
*/
|
|
3210
|
-
const { tools, system: callerSystem, prepareStep: callerPrepareStep, ...rest } = options;
|
|
3211
|
-
const managed = toStreamTextOptions({
|
|
3212
|
-
registry,
|
|
3213
|
-
system: callerSystem ?? agentSystem,
|
|
3214
|
-
cacheControl,
|
|
3215
|
-
systemProviderOptions,
|
|
3216
|
-
/**
|
|
3217
|
-
* A call site that names `tools` replaces the agent's set rather than
|
|
3218
|
-
* adding to it, so narrowing the tools for one call still works. Omitting
|
|
3219
|
-
* `tools` falls back to the agent's, which is what an `onAction`
|
|
3220
|
-
* regenerate needs: without it a regenerated answer can call nothing.
|
|
3221
|
-
*/
|
|
3222
|
-
tools: (tools ?? agentTools),
|
|
3223
|
-
});
|
|
3224
|
-
const promptSystem = locals.get(chatPromptKey)?.text;
|
|
3225
|
-
/**
|
|
3226
|
-
* Two managed sources conflict too, not only a caller against a managed one.
|
|
3227
|
-
* `toStreamTextOptions` resolves `prompt?.text || agentSystem`, so the prompt
|
|
3228
|
-
* would win and `chat.agent({ system })` would go nowhere.
|
|
3229
|
-
*/
|
|
3230
|
-
if (promptSystem && agentSystem) {
|
|
3231
|
-
throw new Error("chat.agent: `system` is set both on chat.agent({ system }) and by chat.prompt.set(), and only one " +
|
|
3232
|
-
"of them can apply. The prompt would win and the agent's `system` would go nowhere. Keep it in one " +
|
|
3233
|
-
"place, and add per-turn context with chat.inject({ role: 'system' }).");
|
|
3234
|
-
}
|
|
3235
|
-
const managedSystem = promptSystem || agentSystem;
|
|
3236
|
-
if (callerSystem !== undefined && managedSystem) {
|
|
3237
|
-
throw new Error("chat.agent: `system` is already set " +
|
|
3238
|
-
(promptSystem ? "by chat.prompt.set()" : "on chat.agent({ system })") +
|
|
3239
|
-
", so it cannot also be passed to the `streamText` given to run(). Set it in one place, and add " +
|
|
3240
|
-
"per-turn context with chat.inject({ role: 'system' }) rather than a second system value.");
|
|
3241
|
-
}
|
|
3242
|
-
const managedPrepareStep = managed.prepareStep;
|
|
3243
|
-
if (typeof callerPrepareStep === "function") {
|
|
3244
|
-
managed.prepareStep = async (arg) => {
|
|
3245
|
-
const first = managedPrepareStep ? await managedPrepareStep(arg) : undefined;
|
|
3246
|
-
const second = await callerPrepareStep({ ...arg, ...(first ?? {}) });
|
|
3247
|
-
return { ...(first ?? {}), ...(second ?? {}) };
|
|
3248
|
-
};
|
|
3249
|
-
}
|
|
3250
|
-
return { ...managed, ...rest };
|
|
3251
|
-
}
|
|
3252
|
-
/** @internal Test hook for {@link buildManagedStreamTextOptions}. */
|
|
3253
|
-
export const __buildManagedStreamTextOptionsForTests = buildManagedStreamTextOptions;
|
|
3254
|
-
function createBoundStreamText(registry, agentSystem, agentCacheControl, agentSystemProviderOptions) {
|
|
3255
|
-
const bound = (options = {}) => aiStreamText(buildManagedStreamTextOptions(options, {
|
|
3256
|
-
registry,
|
|
3257
|
-
system: agentSystem,
|
|
3258
|
-
cacheControl: agentCacheControl,
|
|
3259
|
-
systemProviderOptions: agentSystemProviderOptions,
|
|
3260
|
-
/** Read per call, so per-turn tools resolved after binding are included. */
|
|
3261
|
-
tools: locals.get(chatResolvedToolsKey),
|
|
3262
|
-
}));
|
|
3263
|
-
return bound;
|
|
3264
|
-
}
|
|
3265
3209
|
function toStreamTextOptions(options) {
|
|
3266
|
-
const agentDefaults = locals.get(chatAgentManagedConfigKey);
|
|
3267
|
-
if (agentDefaults) {
|
|
3268
|
-
options = {
|
|
3269
|
-
registry: agentDefaults.registry,
|
|
3270
|
-
system: agentDefaults.system,
|
|
3271
|
-
cacheControl: agentDefaults.cacheControl,
|
|
3272
|
-
systemProviderOptions: agentDefaults.systemProviderOptions,
|
|
3273
|
-
...options,
|
|
3274
|
-
};
|
|
3275
|
-
}
|
|
3276
3210
|
const prompt = locals.get(chatPromptKey);
|
|
3277
3211
|
const skills = locals.get(chatSkillsKey);
|
|
3278
3212
|
const result = {};
|
|
3279
3213
|
// Build the combined system prompt: stored prompt + skills preamble.
|
|
3280
|
-
const
|
|
3281
|
-
const baseSystemText = typeof baseSystem === "string"
|
|
3282
|
-
? baseSystem
|
|
3283
|
-
: typeof baseSystem?.content === "string"
|
|
3284
|
-
? baseSystem.content
|
|
3285
|
-
: "";
|
|
3286
|
-
const promptText = prompt?.text || baseSystemText;
|
|
3214
|
+
const promptText = prompt?.text ?? "";
|
|
3287
3215
|
const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
|
|
3288
3216
|
if (promptText || skillsText) {
|
|
3289
3217
|
const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
* `cacheControl` sugar, the ones carried on a structured `system` message,
|
|
3294
|
-
* then whatever `chat.prompt.set()` stored.
|
|
3295
|
-
*
|
|
3296
|
-
* A structured `system` counts only when its own text is the one being
|
|
3297
|
-
* sent. When `chat.prompt.set()` supplied the text, its provider options
|
|
3298
|
-
* are the ones that describe it.
|
|
3299
|
-
*/
|
|
3300
|
-
const baseSystemProviderOptions = !prompt?.text && baseSystem && typeof baseSystem !== "string"
|
|
3301
|
-
? baseSystem.providerOptions
|
|
3302
|
-
: undefined;
|
|
3218
|
+
// Resolve system-prompt provider options for caching. Precedence (most
|
|
3219
|
+
// specific wins, no deep merge): explicit `systemProviderOptions` →
|
|
3220
|
+
// `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
|
|
3303
3221
|
const systemProviderOptions = options?.systemProviderOptions ??
|
|
3304
3222
|
(options?.cacheControl
|
|
3305
3223
|
? { anthropic: { cacheControl: options.cacheControl } }
|
|
3306
3224
|
: undefined) ??
|
|
3307
|
-
baseSystemProviderOptions ??
|
|
3308
3225
|
locals.get(chatPromptProviderOptionsKey);
|
|
3309
3226
|
// A bare string stays a bare string (the unchanged default). With provider
|
|
3310
3227
|
// options, emit a structured `SystemModelMessage` so the provider can cache
|
|
@@ -3313,88 +3230,6 @@ function toStreamTextOptions(options) {
|
|
|
3313
3230
|
? { role: "system", content: systemText, providerOptions: systemProviderOptions }
|
|
3314
3231
|
: systemText;
|
|
3315
3232
|
}
|
|
3316
|
-
/**
|
|
3317
|
-
* Append anything injected as system context, in whichever shape the installed
|
|
3318
|
-
* AI SDK accepts.
|
|
3319
|
-
*
|
|
3320
|
-
* `system` widened over time: on ai@5 it is `string` only, and from ai@6 it is
|
|
3321
|
-
* `string | SystemModelMessage | Array<SystemModelMessage>`. This package's peer
|
|
3322
|
-
* range still spans all three, so emitting an array unconditionally would break
|
|
3323
|
-
* v5 consumers — for whom a system-role injection used to work, since v5 accepted
|
|
3324
|
-
* a system message inside `messages` that v7 rejects.
|
|
3325
|
-
*
|
|
3326
|
-
* So: concatenate into one string when the base is a plain string, which every
|
|
3327
|
-
* version accepts and which loses nothing (separate blocks only matter for
|
|
3328
|
-
* per-block `providerOptions`). Use the array form only when the base is already
|
|
3329
|
-
* a structured message — that path requires v6+ regardless, because it is how
|
|
3330
|
-
* prompt caching marks the system block, and flattening it would silently throw
|
|
3331
|
-
* the cache away.
|
|
3332
|
-
*
|
|
3333
|
-
* Either way the injected text goes last: the base prompt keeps its position for
|
|
3334
|
-
* caching, and the addition reads as a later amendment. A changed prefix does
|
|
3335
|
-
* cost the first call its cache hit, on turns that actually injected.
|
|
3336
|
-
*/
|
|
3337
|
-
/**
|
|
3338
|
-
* Consumed once per turn, not once per read, and moved out of the lane rather
|
|
3339
|
-
* than marked read in place.
|
|
3340
|
-
*
|
|
3341
|
-
* Per turn, because a `run()` that builds options twice (a cheap classifier
|
|
3342
|
-
* pass and then the answer) has to see the injection in both, and draining on
|
|
3343
|
-
* read hands it to whichever call ran first. Moved out, because blocks left in
|
|
3344
|
-
* the lane sit in front of anything injected during the same turn, and
|
|
3345
|
-
* clearing the lane on the next turn then destroys both. Outside a turn there
|
|
3346
|
-
* is no turn to scope the stash to, so the lane drains on read there.
|
|
3347
|
-
*/
|
|
3348
|
-
const injectedInstructions = locals.get(chatInjectedInstructionsKey);
|
|
3349
|
-
const currentTurn = locals.get(chatTurnContextKey)?.turn;
|
|
3350
|
-
const consumedThisTurn = currentTurn === undefined ? undefined : locals.get(chatInstructionsConsumedKey);
|
|
3351
|
-
let injectedBlocks = [];
|
|
3352
|
-
if (consumedThisTurn && consumedThisTurn.turn === currentTurn) {
|
|
3353
|
-
injectedBlocks = consumedThisTurn.blocks;
|
|
3354
|
-
// Anything injected since the stash was taken joins it, so an instruction
|
|
3355
|
-
// added after an action read the lane still reaches the real turn that
|
|
3356
|
-
// shares the action's turn number, rather than the one after.
|
|
3357
|
-
if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3358
|
-
injectedBlocks.push(...injectedInstructions.splice(0));
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
else if (injectedInstructions && injectedInstructions.length > 0) {
|
|
3362
|
-
injectedBlocks = injectedInstructions.splice(0);
|
|
3363
|
-
if (currentTurn !== undefined) {
|
|
3364
|
-
locals.set(chatInstructionsConsumedKey, { turn: currentTurn, blocks: injectedBlocks });
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
if (injectedBlocks.length > 0) {
|
|
3368
|
-
const blocks = injectedBlocks;
|
|
3369
|
-
const injectedText = blocks
|
|
3370
|
-
.map((block) => (typeof block.content === "string" ? block.content : ""))
|
|
3371
|
-
.filter(Boolean)
|
|
3372
|
-
.join("\n\n");
|
|
3373
|
-
const base = result.system;
|
|
3374
|
-
if (base === undefined) {
|
|
3375
|
-
result.system = injectedText;
|
|
3376
|
-
}
|
|
3377
|
-
else if (typeof base === "string") {
|
|
3378
|
-
result.system = [base, injectedText].filter(Boolean).join("\n\n");
|
|
3379
|
-
}
|
|
3380
|
-
else {
|
|
3381
|
-
// Merged into the existing block rather than added as a second one. An array
|
|
3382
|
-
// of system blocks would keep the base block's cache entry, but ai@5 rejects
|
|
3383
|
-
// it outright ("Invalid prompt: system must be a string") while accepting a
|
|
3384
|
-
// single structured block, and this package's peer range still spans v5.
|
|
3385
|
-
// Choosing per version would mean resolving the installed version at runtime,
|
|
3386
|
-
// which is not something to build on: `import.meta.url` is illegal in this
|
|
3387
|
-
// package's CommonJS output, and a bundled task may have no resolvable `ai`
|
|
3388
|
-
// to read. One shape that works everywhere beats a cache hit.
|
|
3389
|
-
const baseBlock = base;
|
|
3390
|
-
result.system = {
|
|
3391
|
-
...baseBlock,
|
|
3392
|
-
content: [typeof baseBlock.content === "string" ? baseBlock.content : "", injectedText]
|
|
3393
|
-
.filter(Boolean)
|
|
3394
|
-
.join("\n\n"),
|
|
3395
|
-
};
|
|
3396
|
-
}
|
|
3397
|
-
}
|
|
3398
3233
|
// Prompt-related options (only if chat.prompt.set() was called)
|
|
3399
3234
|
if (prompt) {
|
|
3400
3235
|
// Resolve model via registry if both are present
|
|
@@ -3449,7 +3284,7 @@ function toStreamTextOptions(options) {
|
|
|
3449
3284
|
}
|
|
3450
3285
|
// 2. Pending message injection (steering)
|
|
3451
3286
|
if (taskPendingMessages) {
|
|
3452
|
-
const
|
|
3287
|
+
const injected = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
|
|
3453
3288
|
if (injected.length > 0) {
|
|
3454
3289
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
3455
3290
|
}
|
|
@@ -3459,89 +3294,18 @@ function toStreamTextOptions(options) {
|
|
|
3459
3294
|
if (bgQueue && bgQueue.length > 0) {
|
|
3460
3295
|
const injected = bgQueue.splice(0); // drain
|
|
3461
3296
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
3462
|
-
const pendingBackground = locals.get(chatPendingBackgroundKey) ?? [];
|
|
3463
|
-
pendingBackground.push({
|
|
3464
|
-
afterId: locals.get(chatCurrentUIMessagesKey)?.at(-1)?.id ?? "",
|
|
3465
|
-
messages: injected,
|
|
3466
|
-
});
|
|
3467
|
-
locals.set(chatPendingBackgroundKey, pendingBackground);
|
|
3468
3297
|
}
|
|
3469
3298
|
return resultMessages ? { messages: resultMessages } : undefined;
|
|
3470
3299
|
};
|
|
3471
3300
|
}
|
|
3472
3301
|
return result;
|
|
3473
3302
|
}
|
|
3474
|
-
const actionTurnBrand = Symbol.for("trigger.dev/chat/actionTurn");
|
|
3475
|
-
/**
|
|
3476
|
-
* Turn the current action into a turn.
|
|
3477
|
-
*
|
|
3478
|
-
* Return it from `onAction` after editing history. The action's own work is
|
|
3479
|
-
* finished first (the edit is applied and snapshotted), then a turn runs on the
|
|
3480
|
-
* result exactly as a message turn does: `onTurnStart`, `run()` with the edited
|
|
3481
|
-
* history, `onBeforeTurnComplete`, `onTurnComplete`, and the turn counter
|
|
3482
|
-
* advances. That gives the answer everything a turn has, the system prompt,
|
|
3483
|
-
* tools, steering, compaction, injected instructions and persistence, with no
|
|
3484
|
-
* action-specific handling.
|
|
3485
|
-
*
|
|
3486
|
-
* @example
|
|
3487
|
-
* ```ts
|
|
3488
|
-
* onAction: async ({ action }) => {
|
|
3489
|
-
* if (action.type === "regenerate") {
|
|
3490
|
-
* chat.history.slice(0, -1);
|
|
3491
|
-
* return chat.turn();
|
|
3492
|
-
* }
|
|
3493
|
-
* if (action.type === "undo") chat.history.slice(0, -2); // no turn
|
|
3494
|
-
* },
|
|
3495
|
-
* ```
|
|
3496
|
-
*/
|
|
3497
|
-
function chatTurn() {
|
|
3498
|
-
return { [actionTurnBrand]: true };
|
|
3499
|
-
}
|
|
3500
|
-
function isActionTurn(value) {
|
|
3501
|
-
return typeof value === "object" && value !== null && value[actionTurnBrand] === true;
|
|
3502
|
-
}
|
|
3503
|
-
/**
|
|
3504
|
-
* Replace, in a model lane, the run of messages one UI message contributed.
|
|
3505
|
-
*
|
|
3506
|
-
* Used when a UI message is replaced in place (a tool-approval continuation
|
|
3507
|
-
* merging onto the trailing assistant, a captured response reusing an existing
|
|
3508
|
-
* id, a partial replacing an existing message). Reconverting the whole lane
|
|
3509
|
-
* from the UI lane would also replace a compaction summary with the full
|
|
3510
|
-
* transcript and drop the model forms `pendingMessages.prepare` produced.
|
|
3511
|
-
*
|
|
3512
|
-
* The replaced message is the trailing one, so its run is the lane's tail,
|
|
3513
|
-
* before any steer forms appended after it this turn (`tailAfter`). If the
|
|
3514
|
-
* tail does not match the old message's conversion, nothing is changed and
|
|
3515
|
-
* `false` is returned so the caller can fall back to a full reconversion.
|
|
3516
|
-
*/
|
|
3517
|
-
async function replaceModelRun(lane, oldUi, newUi, tailAfter) {
|
|
3518
|
-
const oldRun = await toModelMessages([stripProviderMetadata(oldUi)]);
|
|
3519
|
-
const newRun = await toModelMessages([stripProviderMetadata(newUi)]);
|
|
3520
|
-
const end = lane.length - tailAfter;
|
|
3521
|
-
const start = end - oldRun.length;
|
|
3522
|
-
if (start < 0 || end > lane.length)
|
|
3523
|
-
return false;
|
|
3524
|
-
if (JSON.stringify(lane.slice(start, end)) !== JSON.stringify(oldRun))
|
|
3525
|
-
return false;
|
|
3526
|
-
lane.splice(start, oldRun.length, ...newRun);
|
|
3527
|
-
return true;
|
|
3528
|
-
}
|
|
3529
3303
|
function isUIMessageStreamable(value) {
|
|
3530
3304
|
return (typeof value === "object" &&
|
|
3531
3305
|
value !== null &&
|
|
3532
3306
|
"toUIMessageStream" in value &&
|
|
3533
3307
|
typeof value.toUIMessageStream === "function");
|
|
3534
3308
|
}
|
|
3535
|
-
const warnedHydrateMessagesDeprecated = new Set();
|
|
3536
|
-
function warnHydrateMessagesDeprecatedOnce(agentId) {
|
|
3537
|
-
if (warnedHydrateMessagesDeprecated.has(agentId))
|
|
3538
|
-
return;
|
|
3539
|
-
warnedHydrateMessagesDeprecated.add(agentId);
|
|
3540
|
-
console.warn(`[chat.agent] \`hydrateMessages\` on "${agentId}" is deprecated. Give the agent a transcript ` +
|
|
3541
|
-
"storage instead: `save` receives every change to the conversation and `loadContext` " +
|
|
3542
|
-
"lets the application own the model's context, with crash recovery and durable " +
|
|
3543
|
-
"compaction that `hydrateMessages` never had.");
|
|
3544
|
-
}
|
|
3545
3309
|
let warnedMissingOnAction = false;
|
|
3546
3310
|
function warnMissingOnActionOnce() {
|
|
3547
3311
|
if (warnedMissingOnAction)
|
|
@@ -3674,22 +3438,10 @@ function chatCustomAgent(options) {
|
|
|
3674
3438
|
await installChatInputRouter(payload.chatId, {
|
|
3675
3439
|
resuming: Boolean(payload.continuation),
|
|
3676
3440
|
});
|
|
3677
|
-
// A custom agent's loop is the customer's, so there is no exit site the
|
|
3678
|
-
// SDK controls. Perform a requested close when `run()` returns, whatever
|
|
3679
|
-
// shape the loop had. Idempotent, so the createSession iterator having
|
|
3680
|
-
// already closed on its own exit costs nothing.
|
|
3681
|
-
const withClose = async (result) => {
|
|
3682
|
-
try {
|
|
3683
|
-
return await result;
|
|
3684
|
-
}
|
|
3685
|
-
finally {
|
|
3686
|
-
await performChatClose();
|
|
3687
|
-
}
|
|
3688
|
-
};
|
|
3689
3441
|
// Keep the schema-free path identical to the original custom-agent
|
|
3690
3442
|
// wrapper, including when userRun starts executing.
|
|
3691
3443
|
if (!parseClientData) {
|
|
3692
|
-
return
|
|
3444
|
+
return userRun(payload, runOptions);
|
|
3693
3445
|
}
|
|
3694
3446
|
const isHandoverBoot = payload.trigger === "handover-prepare";
|
|
3695
3447
|
const isMessagelessBoot = payload.trigger === "preload" ||
|
|
@@ -3705,7 +3457,7 @@ function chatCustomAgent(options) {
|
|
|
3705
3457
|
writeErrorToStream: !isMessagelessBoot && !isHandoverBoot,
|
|
3706
3458
|
});
|
|
3707
3459
|
if (validated.ok) {
|
|
3708
|
-
return
|
|
3460
|
+
return userRun(validated.payload, runOptions);
|
|
3709
3461
|
}
|
|
3710
3462
|
if (isHandoverBoot) {
|
|
3711
3463
|
const signal = await waitForHandover({
|
|
@@ -3741,7 +3493,7 @@ function chatCustomAgent(options) {
|
|
|
3741
3493
|
sessionId: next.output.sessionId ?? payload.sessionId,
|
|
3742
3494
|
idleTimeoutInSeconds: next.output.idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds,
|
|
3743
3495
|
};
|
|
3744
|
-
return
|
|
3496
|
+
return userRun(recoveredPayload, runOptions);
|
|
3745
3497
|
},
|
|
3746
3498
|
});
|
|
3747
3499
|
// Register clientDataSchema so the CLI converts it to JSONSchema
|
|
@@ -3754,19 +3506,7 @@ function chatCustomAgent(options) {
|
|
|
3754
3506
|
return task;
|
|
3755
3507
|
}
|
|
3756
3508
|
function chatAgent(options) {
|
|
3757
|
-
const { run: userRun, clientDataSchema, onBoot, onRecoveryBoot, onPreload, onChatStart, onValidateMessages, hydrateMessages,
|
|
3758
|
-
if (hydrateMessages) {
|
|
3759
|
-
if (storage) {
|
|
3760
|
-
throw new Error(`chat.agent: "${options.id}" sets both \`hydrateMessages\` and \`storage\`. ` +
|
|
3761
|
-
"`hydrateMessages` is deprecated and replaced by the storage: `save` receives every " +
|
|
3762
|
-
"change and `loadContext` on the storage owns the model's context. Remove `hydrateMessages`.");
|
|
3763
|
-
}
|
|
3764
|
-
if (typeof (transcriptStorageOverride ?? defaultStorage).loadContext === "function") {
|
|
3765
|
-
throw new Error(`chat.agent: "${options.id}" sets \`hydrateMessages\` and uses a transcript storage with ` +
|
|
3766
|
-
"`loadContext`. Both would own the model's context; keep one.");
|
|
3767
|
-
}
|
|
3768
|
-
warnHydrateMessagesDeprecatedOnce(options.id);
|
|
3769
|
-
}
|
|
3509
|
+
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;
|
|
3770
3510
|
const parseClientData = clientDataSchema ? getSchemaParseFn(clientDataSchema) : undefined;
|
|
3771
3511
|
const parseAction = actionSchema ? getSchemaParseFn(actionSchema) : undefined;
|
|
3772
3512
|
// chat.agent does not expose generic retry options (see docstring on
|
|
@@ -3849,42 +3589,6 @@ function chatAgent(options) {
|
|
|
3849
3589
|
// durable snapshot + `session.out` replay (or `hydrateMessages` if
|
|
3850
3590
|
// registered) — the wire is delta-only now, no longer a seed.
|
|
3851
3591
|
let accumulatedMessages = [];
|
|
3852
|
-
/**
|
|
3853
|
-
* Give the model accumulator the background injections a step-boundary
|
|
3854
|
-
* drain handed to the model this turn, and record them for persistence.
|
|
3855
|
-
* Returns how many model messages were appended.
|
|
3856
|
-
*/
|
|
3857
|
-
const reconcilePendingBackground = () => {
|
|
3858
|
-
const pending = locals.get(chatPendingBackgroundKey);
|
|
3859
|
-
if (!pending || pending.length === 0)
|
|
3860
|
-
return 0;
|
|
3861
|
-
locals.set(chatPendingBackgroundKey, []);
|
|
3862
|
-
let appended = 0;
|
|
3863
|
-
for (const entry of pending) {
|
|
3864
|
-
accumulatedMessages.push(...entry.messages);
|
|
3865
|
-
laneInjections.push(entry);
|
|
3866
|
-
appended += entry.messages.length;
|
|
3867
|
-
}
|
|
3868
|
-
return appended;
|
|
3869
|
-
};
|
|
3870
|
-
/**
|
|
3871
|
-
* Give the model accumulator the steering messages a drain consumed,
|
|
3872
|
-
* in the form the model actually received. Appended, never reconverted
|
|
3873
|
-
* from the UI lane, so a model-only compaction summary survives. Called
|
|
3874
|
-
* on both the success and the error path, before the response or the
|
|
3875
|
-
* partial joins the lane, so the order stays steer-then-answer.
|
|
3876
|
-
*/
|
|
3877
|
-
const reconcilePendingSteer = (options) => {
|
|
3878
|
-
const pending = locals.get(chatPendingSteerKey);
|
|
3879
|
-
if (!pending || pending.length === 0)
|
|
3880
|
-
return [];
|
|
3881
|
-
locals.set(chatPendingSteerKey, []);
|
|
3882
|
-
for (const entry of pending) {
|
|
3883
|
-
accumulatedMessages.push(...entry.model);
|
|
3884
|
-
options?.turnNew?.push(...entry.model);
|
|
3885
|
-
}
|
|
3886
|
-
return pending;
|
|
3887
|
-
};
|
|
3888
3592
|
// Accumulated UI messages for persistence. Mirrors the model accumulator
|
|
3889
3593
|
// but in frontend-friendly UIMessage format (with parts, id, etc.).
|
|
3890
3594
|
let accumulatedUIMessages = [];
|
|
@@ -3902,157 +3606,7 @@ function chatAgent(options) {
|
|
|
3902
3606
|
// collectively cost ~600ms on every first-message TTFC. Both reads
|
|
3903
3607
|
// swallow errors internally; the agent stays available either way.
|
|
3904
3608
|
const sessionIdForSnapshot = payload.sessionId ?? payload.chatId;
|
|
3905
|
-
const transcriptStorage = storage ??
|
|
3906
|
-
transcriptStorageOverride ??
|
|
3907
|
-
defaultStorage;
|
|
3908
|
-
const storageLoadContext = transcriptStorage.loadContext?.bind(transcriptStorage);
|
|
3909
|
-
/**
|
|
3910
|
-
* Who supplies the model's context each turn: the deprecated
|
|
3911
|
-
* `hydrateMessages` hook, the storage's `loadContext`, or (undefined)
|
|
3912
|
-
* the runtime's own transcript.
|
|
3913
|
-
*/
|
|
3914
|
-
const loadContextHook = hydrateMessages
|
|
3915
|
-
? (event) => hydrateMessages(event)
|
|
3916
|
-
: storageLoadContext
|
|
3917
|
-
? (event) => storageLoadContext({ chatId: event.chatId, clientData: event.clientData }, event)
|
|
3918
|
-
: undefined;
|
|
3919
|
-
let transcriptShadow = createTranscriptShadow([]);
|
|
3920
|
-
let bootTranscriptState = null;
|
|
3921
|
-
/**
|
|
3922
|
-
* True while the model lane holds a compaction summary, so it cannot be
|
|
3923
|
-
* rebuilt from the transcript and has to be persisted as state. Reset
|
|
3924
|
-
* wherever the lane is reconverted from the UI lane.
|
|
3925
|
-
*/
|
|
3926
|
-
let laneCompacted = false;
|
|
3927
|
-
/** Conversational `chat.inject` messages in the lane, anchored to the transcript. */
|
|
3928
|
-
let laneInjections = [];
|
|
3929
|
-
let persistedStateSet = false;
|
|
3930
3609
|
let bootSnapshot;
|
|
3931
|
-
let bootClientData = payload.metadata;
|
|
3932
|
-
if (parseClientData) {
|
|
3933
|
-
try {
|
|
3934
|
-
bootClientData = await parseClientData(payload.metadata);
|
|
3935
|
-
}
|
|
3936
|
-
catch {
|
|
3937
|
-
bootClientData = payload.metadata;
|
|
3938
|
-
}
|
|
3939
|
-
}
|
|
3940
|
-
/**
|
|
3941
|
-
* The `lastOutEventId` the most recent snapshot carried.
|
|
3942
|
-
*
|
|
3943
|
-
* A snapshot written outside a turn — after an action mutates history — has
|
|
3944
|
-
* no turn cursor of its own, and writing `undefined` there would drop the
|
|
3945
|
-
* resume point and make the next boot replay from further back. Retaining it
|
|
3946
|
-
* keeps an action's write cursor-neutral.
|
|
3947
|
-
*/
|
|
3948
|
-
let lastSnapshotOutEventId;
|
|
3949
|
-
const storageTrigger = (trigger) => trigger === "regenerate-message"
|
|
3950
|
-
? "regenerate-message"
|
|
3951
|
-
: trigger === "action" || trigger === "action-turn"
|
|
3952
|
-
? "action"
|
|
3953
|
-
: "submit-message";
|
|
3954
|
-
/**
|
|
3955
|
-
* Hand the runtime's view of the transcript to the storage as a
|
|
3956
|
-
* changeset: the diff against what was last saved, plus the cursors the
|
|
3957
|
-
* next boot resumes from. The shadow only advances when the save
|
|
3958
|
-
* succeeds, so a failed save is folded into the next changeset.
|
|
3959
|
-
*/
|
|
3960
|
-
/** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
|
|
3961
|
-
let transcriptState = null;
|
|
3962
|
-
const saveTranscript = async (opts) => {
|
|
3963
|
-
const { changes, shadow } = diffTranscript(transcriptShadow, opts.messages, {
|
|
3964
|
-
nonFinalIds: opts.nonFinalIds,
|
|
3965
|
-
});
|
|
3966
|
-
const throughId = opts.messages.at(-1)?.id ?? "";
|
|
3967
|
-
const queued = locals.get(chatBackgroundQueueKey) ?? [];
|
|
3968
|
-
const runtimeState = laneCompacted || laneInjections.length > 0 || queued.length > 0
|
|
3969
|
-
? {
|
|
3970
|
-
v: 1,
|
|
3971
|
-
...(laneCompacted
|
|
3972
|
-
? {
|
|
3973
|
-
compaction: {
|
|
3974
|
-
modelMessages: accumulatedMessages,
|
|
3975
|
-
throughId,
|
|
3976
|
-
fingerprint: prefixFingerprint(shadow, throughId),
|
|
3977
|
-
},
|
|
3978
|
-
}
|
|
3979
|
-
: {}),
|
|
3980
|
-
...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
|
|
3981
|
-
...(queued.length > 0 ? { queued: [...queued] } : {}),
|
|
3982
|
-
}
|
|
3983
|
-
: null;
|
|
3984
|
-
if (runtimeState !== null || persistedStateSet) {
|
|
3985
|
-
changes.push({ op: "state", value: runtimeState });
|
|
3986
|
-
}
|
|
3987
|
-
transcriptState = runtimeState;
|
|
3988
|
-
const inCursor = chatInputRouter().resumeFloor();
|
|
3989
|
-
await transcriptStorage.save({
|
|
3990
|
-
chatId: payload.chatId,
|
|
3991
|
-
clientData: opts.clientData,
|
|
3992
|
-
turn: opts.turn,
|
|
3993
|
-
trigger: opts.trigger,
|
|
3994
|
-
runId: ctx.run.id,
|
|
3995
|
-
ctx,
|
|
3996
|
-
}, {
|
|
3997
|
-
reason: opts.reason,
|
|
3998
|
-
changes,
|
|
3999
|
-
transcript: {
|
|
4000
|
-
entries: opts.messages.map((message) => ({
|
|
4001
|
-
id: message.id,
|
|
4002
|
-
final: !shadow.nonFinal.has(message.id),
|
|
4003
|
-
message,
|
|
4004
|
-
})),
|
|
4005
|
-
state: transcriptState,
|
|
4006
|
-
},
|
|
4007
|
-
cursors: {
|
|
4008
|
-
lastOutEventId: opts.lastOutEventId,
|
|
4009
|
-
lastInEventId: inCursor !== undefined ? String(inCursor) : undefined,
|
|
4010
|
-
},
|
|
4011
|
-
});
|
|
4012
|
-
transcriptShadow = shadow;
|
|
4013
|
-
persistedStateSet = runtimeState !== null;
|
|
4014
|
-
};
|
|
4015
|
-
/**
|
|
4016
|
-
* Persist the accumulator outside a turn.
|
|
4017
|
-
*
|
|
4018
|
-
* An action is not a turn, so it never reaches the turn-complete path where
|
|
4019
|
-
* the transcript is normally saved, but a `chat.history` mutation changes
|
|
4020
|
-
* the conversation and has to survive the run ending.
|
|
4021
|
-
*
|
|
4022
|
-
* Cursor-neutral: an action has no turn cursor of its own, and writing
|
|
4023
|
-
* `undefined` would drop the resume point the last turn established and make
|
|
4024
|
-
* the next boot replay from further back.
|
|
4025
|
-
*/
|
|
4026
|
-
const writeSnapshotOutsideTurn = async (reason, turnContext) => {
|
|
4027
|
-
if (hydrateMessages)
|
|
4028
|
-
return;
|
|
4029
|
-
try {
|
|
4030
|
-
await tracer.startActiveSpan("snapshot.write", async () => {
|
|
4031
|
-
await saveTranscript({
|
|
4032
|
-
reason: "action",
|
|
4033
|
-
messages: accumulatedUIMessages,
|
|
4034
|
-
turn: turnContext.turn,
|
|
4035
|
-
trigger: "action",
|
|
4036
|
-
clientData: turnContext.clientData,
|
|
4037
|
-
lastOutEventId: lastSnapshotOutEventId,
|
|
4038
|
-
});
|
|
4039
|
-
}, {
|
|
4040
|
-
attributes: {
|
|
4041
|
-
[SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
4042
|
-
[SemanticInternalAttributes.COLLAPSED]: true,
|
|
4043
|
-
"chat.snapshot.reason": reason,
|
|
4044
|
-
"chat.messages.count": accumulatedUIMessages.length,
|
|
4045
|
-
},
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
|
-
catch (error) {
|
|
4049
|
-
logger.warn("chat.agent: snapshot write outside a turn failed; the change may not survive a continuation", {
|
|
4050
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4051
|
-
sessionId: sessionIdForSnapshot,
|
|
4052
|
-
reason,
|
|
4053
|
-
});
|
|
4054
|
-
}
|
|
4055
|
-
};
|
|
4056
3610
|
let replayedSettled = [];
|
|
4057
3611
|
let replayedPartial;
|
|
4058
3612
|
let replayedPartialRaw;
|
|
@@ -4068,7 +3622,7 @@ function chatAgent(options) {
|
|
|
4068
3622
|
// resume-cursor block reuses it instead of re-scanning.
|
|
4069
3623
|
let bootInCursor;
|
|
4070
3624
|
let bootInCursorResolved = false;
|
|
4071
|
-
if (couldHavePriorState) {
|
|
3625
|
+
if (!hydrateMessages && couldHavePriorState) {
|
|
4072
3626
|
// Single parent span for the whole boot read phase — snapshot
|
|
4073
3627
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
4074
3628
|
// + result counts are attributes on the span.
|
|
@@ -4076,33 +3630,18 @@ function chatAgent(options) {
|
|
|
4076
3630
|
// snapshot read
|
|
4077
3631
|
const snapStart = Date.now();
|
|
4078
3632
|
try {
|
|
4079
|
-
|
|
4080
|
-
? undefined
|
|
4081
|
-
: await transcriptStorage.load({
|
|
4082
|
-
chatId: payload.chatId,
|
|
4083
|
-
clientData: bootClientData,
|
|
4084
|
-
});
|
|
4085
|
-
if (loaded) {
|
|
4086
|
-
transcriptShadow = createTranscriptShadow(loaded.messages, new Set(loaded.nonFinalIds ?? []));
|
|
4087
|
-
bootTranscriptState = loaded.state;
|
|
4088
|
-
transcriptState = loaded.state ?? null;
|
|
4089
|
-
persistedStateSet = loaded.state !== null && loaded.state !== undefined;
|
|
4090
|
-
bootSnapshot = {
|
|
4091
|
-
messages: loaded.messages,
|
|
4092
|
-
lastOutEventId: loaded.cursors?.lastOutEventId,
|
|
4093
|
-
lastInEventId: loaded.cursors?.lastInEventId,
|
|
4094
|
-
};
|
|
4095
|
-
}
|
|
3633
|
+
bootSnapshot = await readChatSnapshot(sessionIdForSnapshot);
|
|
4096
3634
|
}
|
|
4097
3635
|
catch (error) {
|
|
4098
|
-
|
|
3636
|
+
// `readChatSnapshot` already swallows + warns internally; this catch
|
|
3637
|
+
// is just belt-and-suspenders against tracer/span errors.
|
|
3638
|
+
logger.warn("chat.agent: snapshot read failed; continuing without snapshot", {
|
|
4099
3639
|
error: error instanceof Error ? error.message : String(error),
|
|
4100
3640
|
sessionId: sessionIdForSnapshot,
|
|
4101
3641
|
});
|
|
4102
3642
|
}
|
|
4103
3643
|
bootSpan.setAttribute("chat.boot.snapshot.durationMs", Date.now() - snapStart);
|
|
4104
|
-
bootSpan.setAttribute("chat.boot.snapshot.present", bootSnapshot
|
|
4105
|
-
(bootSnapshot.messages.length > 0 || bootSnapshot.lastOutEventId !== undefined));
|
|
3644
|
+
bootSpan.setAttribute("chat.boot.snapshot.present", !!bootSnapshot);
|
|
4106
3645
|
bootSpan.setAttribute("chat.boot.snapshot.messageCount", bootSnapshot?.messages?.length ?? 0);
|
|
4107
3646
|
// Seed the trim chain from the snapshot's `lastOutEventId` (the SSE
|
|
4108
3647
|
// id of the previous turn's `turn-complete` control record). The
|
|
@@ -4110,7 +3649,6 @@ function chatAgent(options) {
|
|
|
4110
3649
|
// Without seeding, the new worker would emit no trim on its first
|
|
4111
3650
|
// turn (chain self-bootstraps from turn 2), so this is purely an
|
|
4112
3651
|
// optimization to keep continuation runs bounded from the first turn.
|
|
4113
|
-
lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
|
|
4114
3652
|
if (bootSnapshot?.lastOutEventId !== undefined) {
|
|
4115
3653
|
const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
|
|
4116
3654
|
if (Number.isFinite(seeded)) {
|
|
@@ -4205,17 +3743,13 @@ function chatAgent(options) {
|
|
|
4205
3743
|
// Reads the turn boundary and subscribes in one call. `bootInCursor` is
|
|
4206
3744
|
// only a fallback: the boot block above may already have resolved a
|
|
4207
3745
|
// cursor from the snapshot, which is used when the boundary itself
|
|
4208
|
-
// carries none.
|
|
4209
|
-
// `bootInjectedQueue` below, so it goes into the floor here — folded in
|
|
4210
|
-
// after the subscription opens, the live tail re-delivers it as a turn.
|
|
4211
|
-
const lastRecoveredInSeq = replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1].seqNum : undefined;
|
|
3746
|
+
// carries none.
|
|
4212
3747
|
await installChatInputRouter(payload.chatId, {
|
|
4213
3748
|
fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
|
|
4214
|
-
recoveredThrough: lastRecoveredInSeq,
|
|
4215
3749
|
resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
|
|
4216
3750
|
});
|
|
4217
3751
|
// ── Recovery boot + chain reconstruction ────────────────────────
|
|
4218
|
-
{
|
|
3752
|
+
if (!hydrateMessages) {
|
|
4219
3753
|
const settledMessages = mergeByIdReplaceWins(bootSnapshot?.messages ?? [], replayedSettled);
|
|
4220
3754
|
const inFlightUsers = replayedInTail.map((r) => r.message);
|
|
4221
3755
|
const partialAssistant = replayedPartial;
|
|
@@ -4319,6 +3853,16 @@ function chatAgent(options) {
|
|
|
4319
3853
|
if (hookBeforeBoot) {
|
|
4320
3854
|
await hookBeforeBoot();
|
|
4321
3855
|
}
|
|
3856
|
+
// Advance the session.in cursor past every recovered user so
|
|
3857
|
+
// the live subscription doesn't re-deliver them.
|
|
3858
|
+
if (replayedInTail.length > 0) {
|
|
3859
|
+
const lastRecoveredSeq = replayedInTail[replayedInTail.length - 1].seqNum;
|
|
3860
|
+
const currentCursor = sessionStreams.lastSeqNum(payload.chatId, "in");
|
|
3861
|
+
if (currentCursor === undefined || lastRecoveredSeq > currentCursor) {
|
|
3862
|
+
sessionStreams.setLastSeqNum(payload.chatId, "in", lastRecoveredSeq);
|
|
3863
|
+
sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", lastRecoveredSeq);
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
4322
3866
|
// Synthesize wire payloads for each recoveredTurn. The turn-loop
|
|
4323
3867
|
// pops these ahead of `messagesInput.waitWithIdleTimeout` so they
|
|
4324
3868
|
// dispatch as normal turns with the existing hook stack.
|
|
@@ -4368,8 +3912,7 @@ function chatAgent(options) {
|
|
|
4368
3912
|
// path where wire-borne UIMessage[] still seeds the accumulator,
|
|
4369
3913
|
// and it's safe because the route handler isn't subject to the
|
|
4370
3914
|
// `/in/append` 512 KiB cap.
|
|
4371
|
-
if (
|
|
4372
|
-
accumulatedUIMessages.length === 0 &&
|
|
3915
|
+
if (accumulatedUIMessages.length === 0 &&
|
|
4373
3916
|
payload.trigger === "handover-prepare" &&
|
|
4374
3917
|
Array.isArray(payload.headStartMessages) &&
|
|
4375
3918
|
payload.headStartMessages.length > 0) {
|
|
@@ -4398,17 +3941,7 @@ function chatAgent(options) {
|
|
|
4398
3941
|
}
|
|
4399
3942
|
}
|
|
4400
3943
|
try {
|
|
4401
|
-
|
|
4402
|
-
const restored = await restoreModelLane(accumulatedUIMessages, bootRuntimeState, (messages) => toModelMessages(messages));
|
|
4403
|
-
accumulatedMessages = restored.messages;
|
|
4404
|
-
laneCompacted = restored.compacted;
|
|
4405
|
-
laneInjections = restored.injections;
|
|
4406
|
-
if (bootRuntimeState?.queued && bootRuntimeState.queued.length > 0) {
|
|
4407
|
-
locals.set(chatBackgroundQueueKey, [
|
|
4408
|
-
...(locals.get(chatBackgroundQueueKey) ?? []),
|
|
4409
|
-
...bootRuntimeState.queued,
|
|
4410
|
-
]);
|
|
4411
|
-
}
|
|
3944
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
4412
3945
|
}
|
|
4413
3946
|
catch (error) {
|
|
4414
3947
|
logger.warn("chat.agent: toModelMessages failed at boot; starting empty", {
|
|
@@ -4422,12 +3955,6 @@ function chatAgent(options) {
|
|
|
4422
3955
|
// before any hook (`onChatStart`, `onTurnStart`, etc.) fires.
|
|
4423
3956
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4424
3957
|
}
|
|
4425
|
-
locals.set(chatAgentManagedConfigKey, {
|
|
4426
|
-
registry: promptRegistry,
|
|
4427
|
-
system: agentSystem,
|
|
4428
|
-
cacheControl: agentCacheControl,
|
|
4429
|
-
systemProviderOptions: agentSystemProviderOptions,
|
|
4430
|
-
});
|
|
4431
3958
|
// Token usage tracking across turns
|
|
4432
3959
|
let previousTurnUsage;
|
|
4433
3960
|
let cumulativeUsage = emptyUsage();
|
|
@@ -4785,7 +4312,6 @@ function chatAgent(options) {
|
|
|
4785
4312
|
} // end else (no boot-injected first turn)
|
|
4786
4313
|
}
|
|
4787
4314
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
4788
|
-
let turnClientData = payload.metadata;
|
|
4789
4315
|
// Declared here so the finally can detach it — a handler leaked past
|
|
4790
4316
|
// its turn duplicates every mid-stream message into the shared buffer.
|
|
4791
4317
|
let turnMsgSub;
|
|
@@ -4807,7 +4333,6 @@ function chatAgent(options) {
|
|
|
4807
4333
|
// `cleanupAbortedParts` no longer has to be re-applied below.
|
|
4808
4334
|
const cleanedIncomingMessages = incomingMessages.map((msg) => msg.role === "assistant" ? cleanupAbortedParts(msg) : msg);
|
|
4809
4335
|
const clientData = (parseClientData ? await parseClientData(wireMetadata) : wireMetadata);
|
|
4810
|
-
turnClientData = clientData;
|
|
4811
4336
|
const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages);
|
|
4812
4337
|
// Actions are not turns. They use a different span name
|
|
4813
4338
|
// and don't carry a turn.number. Branched on at `isAction`.
|
|
@@ -4844,7 +4369,6 @@ function chatAgent(options) {
|
|
|
4844
4369
|
locals.set(chatDeferKey, new Set());
|
|
4845
4370
|
locals.set(chatCompactionStateKey, undefined);
|
|
4846
4371
|
locals.set(chatSteeringQueueKey, []);
|
|
4847
|
-
locals.set(chatPendingBackgroundKey, []);
|
|
4848
4372
|
locals.set(chatResponsePartsKey, []);
|
|
4849
4373
|
// NOTE: chatBackgroundQueueKey is NOT reset here — messages injected
|
|
4850
4374
|
// by deferred work from the previous turn's onTurnComplete need to
|
|
@@ -4926,10 +4450,6 @@ function chatAgent(options) {
|
|
|
4926
4450
|
// Track new messages for this turn (user input + assistant response).
|
|
4927
4451
|
const turnNewModelMessages = [];
|
|
4928
4452
|
const turnNewUIMessages = [];
|
|
4929
|
-
locals.set(chatTurnNewUIMessagesKey, turnNewUIMessages);
|
|
4930
|
-
// A head-start handover deliberately resumes from an assistant
|
|
4931
|
-
// message it spliced in, so it isn't a no-op turn.
|
|
4932
|
-
let splicedHandoverPartial = false;
|
|
4933
4453
|
// ── Action handling ──────────────────────────────────────
|
|
4934
4454
|
// Actions arrive on the same input stream but with
|
|
4935
4455
|
// trigger === "action". They are NOT turns — only
|
|
@@ -4940,25 +4460,16 @@ function chatAgent(options) {
|
|
|
4940
4460
|
// an action, return a `StreamTextResult` (auto-piped),
|
|
4941
4461
|
// string, or UIMessage from `onAction`. Turn counter
|
|
4942
4462
|
// does not advance.
|
|
4943
|
-
let
|
|
4944
|
-
/** Set when `onAction` returned `chat.turn()`: the turn block below runs. */
|
|
4945
|
-
let actionTurn = false;
|
|
4946
|
-
/**
|
|
4947
|
-
* Whether this action changed the conversation, by rolling history
|
|
4948
|
-
* back or by streaming a response. Drives the single snapshot write
|
|
4949
|
-
* at the end — an action never reaches the turn-complete path that
|
|
4950
|
-
* normally does it.
|
|
4951
|
-
*/
|
|
4952
|
-
let actionChangedHistory = false;
|
|
4463
|
+
let actionStreamResult = undefined;
|
|
4953
4464
|
if (isAction) {
|
|
4954
4465
|
// Parse and validate the action payload
|
|
4955
4466
|
const parsedAction = parseAction
|
|
4956
4467
|
? await parseAction(currentWirePayload.action)
|
|
4957
4468
|
: currentWirePayload.action;
|
|
4958
4469
|
// Hydrate messages from backend if configured
|
|
4959
|
-
if (
|
|
4470
|
+
if (hydrateMessages) {
|
|
4960
4471
|
const hydrated = await tracer.startActiveSpan("hydrateMessages()", async () => {
|
|
4961
|
-
return
|
|
4472
|
+
return hydrateMessages({
|
|
4962
4473
|
chatId: currentWirePayload.chatId,
|
|
4963
4474
|
turn,
|
|
4964
4475
|
trigger: "action",
|
|
@@ -4978,14 +4489,12 @@ function chatAgent(options) {
|
|
|
4978
4489
|
});
|
|
4979
4490
|
accumulatedUIMessages = [...hydrated];
|
|
4980
4491
|
accumulatedMessages = await toModelMessages(hydrated);
|
|
4981
|
-
laneCompacted = false;
|
|
4982
|
-
laneInjections = [];
|
|
4983
4492
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
4984
4493
|
}
|
|
4985
4494
|
// Fire onAction — handler may mutate state via
|
|
4986
4495
|
// `chat.history.*` and / or return a model response.
|
|
4987
4496
|
if (onAction) {
|
|
4988
|
-
|
|
4497
|
+
actionStreamResult = await tracer.startActiveSpan("onAction()", async () => {
|
|
4989
4498
|
return await onAction({
|
|
4990
4499
|
action: parsedAction,
|
|
4991
4500
|
chatId: currentWirePayload.chatId,
|
|
@@ -5010,10 +4519,7 @@ function chatAgent(options) {
|
|
|
5010
4519
|
locals.set(chatOverrideMessagesKey, undefined);
|
|
5011
4520
|
accumulatedUIMessages = [...actionOverride];
|
|
5012
4521
|
accumulatedMessages = await toModelMessages(actionOverride);
|
|
5013
|
-
laneCompacted = false;
|
|
5014
|
-
laneInjections = [];
|
|
5015
4522
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5016
|
-
actionChangedHistory = true;
|
|
5017
4523
|
}
|
|
5018
4524
|
}
|
|
5019
4525
|
else {
|
|
@@ -5034,7 +4540,7 @@ function chatAgent(options) {
|
|
|
5034
4540
|
// route handler's first-turn history to the hydrate hook as
|
|
5035
4541
|
// incoming messages instead (gated on the pending handover).
|
|
5036
4542
|
if (turn === 0 &&
|
|
5037
|
-
|
|
4543
|
+
hydrateMessages &&
|
|
5038
4544
|
cleanedUIMessages.length === 0 &&
|
|
5039
4545
|
(locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
|
|
5040
4546
|
Array.isArray(payload.headStartMessages) &&
|
|
@@ -5064,7 +4570,7 @@ function chatAgent(options) {
|
|
|
5064
4570
|
},
|
|
5065
4571
|
}));
|
|
5066
4572
|
}
|
|
5067
|
-
if (
|
|
4573
|
+
if (hydrateMessages) {
|
|
5068
4574
|
// Snapshot the ids the accumulator knew BEFORE this
|
|
5069
4575
|
// turn ran — used below to decide whether an
|
|
5070
4576
|
// incoming wire message is genuinely new or just a
|
|
@@ -5082,7 +4588,7 @@ function chatAgent(options) {
|
|
|
5082
4588
|
// was always true for `submit-message` is now true for every
|
|
5083
4589
|
// trigger.
|
|
5084
4590
|
const hydrated = await tracer.startActiveSpan("hydrateMessages()", async () => {
|
|
5085
|
-
return
|
|
4591
|
+
return hydrateMessages({
|
|
5086
4592
|
chatId: currentWirePayload.chatId,
|
|
5087
4593
|
turn,
|
|
5088
4594
|
trigger: currentWirePayload.trigger,
|
|
@@ -5121,8 +4627,6 @@ function chatAgent(options) {
|
|
|
5121
4627
|
}
|
|
5122
4628
|
accumulatedUIMessages = merged;
|
|
5123
4629
|
accumulatedMessages = await toModelMessages(merged);
|
|
5124
|
-
laneCompacted = false;
|
|
5125
|
-
laneInjections = [];
|
|
5126
4630
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5127
4631
|
// Track new messages for onTurnComplete.newUIMessages.
|
|
5128
4632
|
// Only push for genuinely new ids — HITL continuations
|
|
@@ -5168,8 +4672,6 @@ function chatAgent(options) {
|
|
|
5168
4672
|
accumulatedUIMessages.pop();
|
|
5169
4673
|
}
|
|
5170
4674
|
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5171
|
-
laneCompacted = false;
|
|
5172
|
-
laneInjections = [];
|
|
5173
4675
|
}
|
|
5174
4676
|
else if (cleanedUIMessages.length > 0) {
|
|
5175
4677
|
// Submit-message (and the special-cased
|
|
@@ -5186,7 +4688,6 @@ function chatAgent(options) {
|
|
|
5186
4688
|
// where AI SDK regenerates the id (TRI-9137) still
|
|
5187
4689
|
// applies via `rewriteIncomingIdViaToolCallMap`.
|
|
5188
4690
|
let replaced = false;
|
|
5189
|
-
const replacedPairs = [];
|
|
5190
4691
|
for (const raw of cleanedUIMessages) {
|
|
5191
4692
|
let incoming = raw;
|
|
5192
4693
|
let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
|
|
@@ -5198,9 +4699,7 @@ function chatAgent(options) {
|
|
|
5198
4699
|
}
|
|
5199
4700
|
}
|
|
5200
4701
|
if (idx !== -1) {
|
|
5201
|
-
|
|
5202
|
-
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(previous, incoming);
|
|
5203
|
-
replacedPairs.push({ previous, merged: accumulatedUIMessages[idx] });
|
|
4702
|
+
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(accumulatedUIMessages[idx], incoming);
|
|
5204
4703
|
replaced = true;
|
|
5205
4704
|
}
|
|
5206
4705
|
else {
|
|
@@ -5210,19 +4709,9 @@ function chatAgent(options) {
|
|
|
5210
4709
|
recordToolCallIdsFromMessage(incoming);
|
|
5211
4710
|
}
|
|
5212
4711
|
if (replaced) {
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
inPlace = false;
|
|
5217
|
-
break;
|
|
5218
|
-
}
|
|
5219
|
-
}
|
|
5220
|
-
if (!inPlace) {
|
|
5221
|
-
logger.warn("chat.agent: replaced message not found at the model lane tail; reconverting the lane");
|
|
5222
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5223
|
-
laneCompacted = false;
|
|
5224
|
-
laneInjections = [];
|
|
5225
|
-
}
|
|
4712
|
+
// Replacement changes structure — reconvert all model
|
|
4713
|
+
// messages instead of appending.
|
|
4714
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5226
4715
|
}
|
|
5227
4716
|
else {
|
|
5228
4717
|
const incomingModelMessages = await toModelMessages(cleanedUIMessages);
|
|
@@ -5265,33 +4754,10 @@ function chatAgent(options) {
|
|
|
5265
4754
|
messageId: locals.get(chatHandoverMessageIdKey),
|
|
5266
4755
|
});
|
|
5267
4756
|
locals.set(chatHandoverPartialKey, []); // consume once
|
|
5268
|
-
splicedHandoverPartial = true;
|
|
5269
4757
|
}
|
|
5270
4758
|
}
|
|
5271
4759
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5272
4760
|
} // end if (trigger !== "action")
|
|
5273
|
-
// ── No-op turn ──────────────────────────────────────────
|
|
5274
|
-
//
|
|
5275
|
-
// A submit that added no new user message and leaves the model
|
|
5276
|
-
// chain ending on an assistant message has nothing to answer —
|
|
5277
|
-
// calling the model would prefill its own last reply. Keyed on
|
|
5278
|
-
// the model tail, so a `tool`-terminated chain (a merged tool
|
|
5279
|
-
// approval) still runs.
|
|
5280
|
-
const isNoOpTurn = !isAction &&
|
|
5281
|
-
!splicedHandoverPartial &&
|
|
5282
|
-
currentWirePayload.trigger === "submit-message" &&
|
|
5283
|
-
turnNewUIMessages.length === 0 &&
|
|
5284
|
-
accumulatedMessages[accumulatedMessages.length - 1]?.role === "assistant";
|
|
5285
|
-
if (isNoOpTurn) {
|
|
5286
|
-
msgSub?.off();
|
|
5287
|
-
logger.warn("chat.agent: turn added no new user message; skipping the model", {
|
|
5288
|
-
chatId: currentWirePayload.chatId,
|
|
5289
|
-
messageId: currentWirePayload.messageId,
|
|
5290
|
-
});
|
|
5291
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5292
|
-
// Not a turn — don't consume an iteration.
|
|
5293
|
-
turn--;
|
|
5294
|
-
}
|
|
5295
4761
|
// ── Action result handling ──────────────────────────────
|
|
5296
4762
|
// For action turns, skip the turn machinery entirely.
|
|
5297
4763
|
// If `onAction` returned a stream / string / UIMessage,
|
|
@@ -5301,34 +4767,34 @@ function chatAgent(options) {
|
|
|
5301
4767
|
// The turn counter is decremented so the next iteration
|
|
5302
4768
|
// sees the same `turn` value — actions don't count.
|
|
5303
4769
|
if (isAction) {
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
4770
|
+
msgSub?.off();
|
|
4771
|
+
if ((locals.get(chatPipeCountKey) ?? 0) === 0 &&
|
|
4772
|
+
isUIMessageStreamable(actionStreamResult)) {
|
|
4773
|
+
try {
|
|
4774
|
+
const resolvedOptions = resolveUIMessageStreamOptions();
|
|
4775
|
+
const uiStream = actionStreamResult.toUIMessageStream({
|
|
4776
|
+
...resolvedOptions,
|
|
4777
|
+
generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId,
|
|
4778
|
+
});
|
|
4779
|
+
await pipeChat(uiStream, {
|
|
4780
|
+
signal: combinedSignal,
|
|
4781
|
+
spanName: "stream response",
|
|
4782
|
+
});
|
|
5311
4783
|
}
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
else {
|
|
5320
|
-
msgSub?.off();
|
|
5321
|
-
if (actionChangedHistory) {
|
|
5322
|
-
await writeSnapshotOutsideTurn("action", { turn, clientData });
|
|
4784
|
+
catch (error) {
|
|
4785
|
+
if (error instanceof Error &&
|
|
4786
|
+
error.name === "AbortError" &&
|
|
4787
|
+
runSignal.aborted) {
|
|
4788
|
+
return "exit";
|
|
4789
|
+
}
|
|
4790
|
+
throw error;
|
|
5323
4791
|
}
|
|
5324
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5325
|
-
// Don't consume a turn iteration — actions aren't turns.
|
|
5326
|
-
turn--;
|
|
5327
4792
|
}
|
|
4793
|
+
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4794
|
+
// Don't consume a turn iteration — actions aren't turns.
|
|
4795
|
+
turn--;
|
|
5328
4796
|
}
|
|
5329
|
-
|
|
5330
|
-
// there is nothing to answer, so nothing to hand over.
|
|
5331
|
-
if ((!isAction || actionTurn) && !isNoOpTurn) {
|
|
4797
|
+
if (!isAction) {
|
|
5332
4798
|
// Mint a scoped public access token once per turn, reused for
|
|
5333
4799
|
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
5334
4800
|
const currentRunId = ctx.run.id;
|
|
@@ -5417,8 +4883,6 @@ function chatAgent(options) {
|
|
|
5417
4883
|
locals.set(chatOverrideMessagesKey, undefined);
|
|
5418
4884
|
accumulatedUIMessages = [...turnStartOverride];
|
|
5419
4885
|
accumulatedMessages = await toModelMessages(turnStartOverride);
|
|
5420
|
-
laneCompacted = false;
|
|
5421
|
-
laneInjections = [];
|
|
5422
4886
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5423
4887
|
}
|
|
5424
4888
|
}, {
|
|
@@ -5435,11 +4899,9 @@ function chatAgent(options) {
|
|
|
5435
4899
|
},
|
|
5436
4900
|
});
|
|
5437
4901
|
}
|
|
5438
|
-
await followSessionPin(currentWirePayload.chatId, versionSkew);
|
|
5439
4902
|
// chat.requestUpgrade() called in onTurnStart (or onValidateMessages) —
|
|
5440
|
-
// skip run() and
|
|
5441
|
-
//
|
|
5442
|
-
// keeps reading.
|
|
4903
|
+
// skip run() and signal the transport to re-trigger the same message
|
|
4904
|
+
// on the new version.
|
|
5443
4905
|
if (locals.get(chatUpgradeRequestedKey)) {
|
|
5444
4906
|
await writeUpgradeRequiredChunk();
|
|
5445
4907
|
return "exit";
|
|
@@ -5479,12 +4941,7 @@ function chatAgent(options) {
|
|
|
5479
4941
|
const lastAccumulated = accumulatedMessages[accumulatedMessages.length - 1];
|
|
5480
4942
|
const bgQueue = locals.get(chatBackgroundQueueKey);
|
|
5481
4943
|
if (bgQueue && bgQueue.length > 0 && lastAccumulated?.role !== "tool") {
|
|
5482
|
-
|
|
5483
|
-
accumulatedMessages.push(...injected);
|
|
5484
|
-
laneInjections.push({
|
|
5485
|
-
afterId: accumulatedUIMessages.at(-1)?.id ?? "",
|
|
5486
|
-
messages: injected,
|
|
5487
|
-
});
|
|
4944
|
+
accumulatedMessages.push(...bgQueue.splice(0));
|
|
5488
4945
|
}
|
|
5489
4946
|
if (isHeadStartFinalTurn) {
|
|
5490
4947
|
// The synthesized partial UIMessage IS the response.
|
|
@@ -5503,9 +4960,6 @@ function chatAgent(options) {
|
|
|
5503
4960
|
const preparedMessages = await applyPrepareMessages(accumulatedMessages, "run");
|
|
5504
4961
|
runResult = await userRun({
|
|
5505
4962
|
...restWire,
|
|
5506
|
-
// A turn requested by chat.turn() is not the action itself:
|
|
5507
|
-
// a run() that short-circuits on "action" must still answer.
|
|
5508
|
-
...(actionTurn ? { trigger: "action-turn" } : {}),
|
|
5509
4963
|
messages: preparedMessages,
|
|
5510
4964
|
clientData,
|
|
5511
4965
|
continuation,
|
|
@@ -5518,7 +4972,6 @@ function chatAgent(options) {
|
|
|
5518
4972
|
signal: combinedSignal,
|
|
5519
4973
|
cancelSignal,
|
|
5520
4974
|
stopSignal,
|
|
5521
|
-
streamText: createBoundStreamText(promptRegistry, agentSystem, agentCacheControl, agentSystemProviderOptions),
|
|
5522
4975
|
});
|
|
5523
4976
|
}
|
|
5524
4977
|
// Auto-pipe if the run function returned a StreamTextResult or similar,
|
|
@@ -5632,21 +5085,7 @@ function chatAgent(options) {
|
|
|
5632
5085
|
if (runOverride) {
|
|
5633
5086
|
locals.set(chatOverrideMessagesKey, undefined);
|
|
5634
5087
|
accumulatedUIMessages = [...runOverride];
|
|
5635
|
-
|
|
5636
|
-
* Steers the drain consumed are left out of the rebuild and
|
|
5637
|
-
* appended by the reconciliation below instead, so the lane
|
|
5638
|
-
* gets the form the model actually received rather than a
|
|
5639
|
-
* reconversion of the UI message, and gets it once. A steer
|
|
5640
|
-
* the edit removed is dropped from the pending list too, so
|
|
5641
|
-
* the edit is honoured.
|
|
5642
|
-
*/
|
|
5643
|
-
const overrideIds = new Set(runOverride.map((m) => m.id));
|
|
5644
|
-
const pending = (locals.get(chatPendingSteerKey) ?? []).filter((e) => overrideIds.has(e.ui.id));
|
|
5645
|
-
locals.set(chatPendingSteerKey, pending);
|
|
5646
|
-
const pendingIds = new Set(pending.map((e) => e.ui.id));
|
|
5647
|
-
accumulatedMessages = await toModelMessages(runOverride.filter((m) => !pendingIds.has(m.id)));
|
|
5648
|
-
laneCompacted = false;
|
|
5649
|
-
laneInjections = [];
|
|
5088
|
+
accumulatedMessages = await toModelMessages(runOverride);
|
|
5650
5089
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5651
5090
|
}
|
|
5652
5091
|
// Check if compaction set a model-only override (preserves UI messages).
|
|
@@ -5669,8 +5108,6 @@ function chatAgent(options) {
|
|
|
5669
5108
|
accumulatedMessages = taskCompactionConfig?.compactModelMessages
|
|
5670
5109
|
? await taskCompactionConfig.compactModelMessages(compactEvent)
|
|
5671
5110
|
: modelOnlyOverride;
|
|
5672
|
-
laneCompacted = true;
|
|
5673
|
-
laneInjections = [];
|
|
5674
5111
|
// Apply UI messages: callback or default (preserve all)
|
|
5675
5112
|
if (taskCompactionConfig?.compactUIMessages) {
|
|
5676
5113
|
accumulatedUIMessages = (await taskCompactionConfig.compactUIMessages(compactEvent));
|
|
@@ -5678,15 +5115,6 @@ function chatAgent(options) {
|
|
|
5678
5115
|
}
|
|
5679
5116
|
// Determine if the user stopped generation this turn (not a full run cancel).
|
|
5680
5117
|
const wasStopped = stopController.signal.aborted && !runSignal.aborted;
|
|
5681
|
-
// Give the model accumulator the steering messages the drain
|
|
5682
|
-
// consumed. Appended, never reconverted from the UI lane, so a
|
|
5683
|
-
// model-only compaction summary set just above survives; and done
|
|
5684
|
-
// before the response is appended so the order stays
|
|
5685
|
-
// steer-then-answer. Outside the `capturedResponseMessage`
|
|
5686
|
-
// branches below, so a turn that captured no response is covered.
|
|
5687
|
-
const steerTailThisTurn = reconcilePendingSteer({
|
|
5688
|
-
turnNew: turnNewModelMessages,
|
|
5689
|
-
}).reduce((n, e) => n + e.model.length, 0) + reconcilePendingBackground();
|
|
5690
5118
|
// Append the assistant's response (partial or complete) to the accumulator.
|
|
5691
5119
|
// The onFinish callback fires even on abort/stop, so partial responses
|
|
5692
5120
|
// from stopped generation are captured correctly.
|
|
@@ -5724,7 +5152,6 @@ function chatAgent(options) {
|
|
|
5724
5152
|
const existingIdx = capturedResponseMessage.id
|
|
5725
5153
|
? accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage.id)
|
|
5726
5154
|
: -1;
|
|
5727
|
-
const previousAtIdx = existingIdx !== -1 ? accumulatedUIMessages[existingIdx] : undefined;
|
|
5728
5155
|
if (existingIdx !== -1) {
|
|
5729
5156
|
accumulatedUIMessages[existingIdx] = capturedResponseMessage;
|
|
5730
5157
|
}
|
|
@@ -5744,14 +5171,8 @@ function chatAgent(options) {
|
|
|
5744
5171
|
stripProviderMetadata(capturedResponseMessage),
|
|
5745
5172
|
]);
|
|
5746
5173
|
if (existingIdx !== -1) {
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
if (!ok) {
|
|
5750
|
-
logger.warn("chat.agent: replaced response not found at the model lane tail; reconverting the lane");
|
|
5751
|
-
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5752
|
-
laneCompacted = false;
|
|
5753
|
-
laneInjections = [];
|
|
5754
|
-
}
|
|
5174
|
+
// Reconvert all model messages since we replaced rather than appended
|
|
5175
|
+
accumulatedMessages = await toModelMessages(accumulatedUIMessages);
|
|
5755
5176
|
}
|
|
5756
5177
|
else {
|
|
5757
5178
|
accumulatedMessages.push(...responseModelMessages);
|
|
@@ -5859,8 +5280,6 @@ function chatAgent(options) {
|
|
|
5859
5280
|
],
|
|
5860
5281
|
},
|
|
5861
5282
|
];
|
|
5862
|
-
laneCompacted = true;
|
|
5863
|
-
laneInjections = [];
|
|
5864
5283
|
// UI messages: callback or default (preserve all)
|
|
5865
5284
|
if (outerCompaction.compactUIMessages) {
|
|
5866
5285
|
accumulatedUIMessages = (await outerCompaction.compactUIMessages(outerCompactEvent));
|
|
@@ -5950,8 +5369,6 @@ function chatAgent(options) {
|
|
|
5950
5369
|
locals.set(chatOverrideMessagesKey, undefined);
|
|
5951
5370
|
accumulatedUIMessages = [...override];
|
|
5952
5371
|
accumulatedMessages = await toModelMessages(override);
|
|
5953
|
-
laneCompacted = false;
|
|
5954
|
-
laneInjections = [];
|
|
5955
5372
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
5956
5373
|
// Update event so onTurnComplete sees compacted messages
|
|
5957
5374
|
turnCompleteEvent.messages = accumulatedMessages;
|
|
@@ -5998,8 +5415,6 @@ function chatAgent(options) {
|
|
|
5998
5415
|
locals.set(chatOverrideMessagesKey, undefined);
|
|
5999
5416
|
accumulatedUIMessages = [...turnCompleteOverride];
|
|
6000
5417
|
accumulatedMessages = await toModelMessages(turnCompleteOverride);
|
|
6001
|
-
laneCompacted = false;
|
|
6002
|
-
laneInjections = [];
|
|
6003
5418
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
6004
5419
|
}
|
|
6005
5420
|
}, {
|
|
@@ -6053,18 +5468,13 @@ function chatAgent(options) {
|
|
|
6053
5468
|
if (!hydrateMessages) {
|
|
6054
5469
|
try {
|
|
6055
5470
|
await tracer.startActiveSpan("snapshot.write", async () => {
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
5471
|
+
const snapshotInCursor = chatInputRouter().resumeFloor();
|
|
5472
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
5473
|
+
version: 1,
|
|
5474
|
+
savedAt: Date.now(),
|
|
6060
5475
|
messages: accumulatedUIMessages,
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
clientData,
|
|
6064
|
-
lastOutEventId: lastSnapshotOutEventId,
|
|
6065
|
-
nonFinalIds: wasStopped && capturedResponseMessage?.id
|
|
6066
|
-
? new Set([capturedResponseMessage.id])
|
|
6067
|
-
: undefined,
|
|
5476
|
+
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
5477
|
+
lastInEventId: snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined,
|
|
6068
5478
|
});
|
|
6069
5479
|
}, {
|
|
6070
5480
|
attributes: {
|
|
@@ -6097,17 +5507,10 @@ function chatAgent(options) {
|
|
|
6097
5507
|
currentWirePayload = bootInjectedQueue.shift();
|
|
6098
5508
|
return "continue";
|
|
6099
5509
|
}
|
|
6100
|
-
// chat.requestUpgrade() was called — exit the loop
|
|
6101
|
-
//
|
|
5510
|
+
// chat.requestUpgrade() was called — exit the loop so the
|
|
5511
|
+
// transport triggers a new run on the latest version.
|
|
6102
5512
|
// chat.endRun() — same exit, no upgrade semantics.
|
|
6103
|
-
if (locals.get(chatCloseRequestedKey)) {
|
|
6104
|
-
await performChatClose();
|
|
6105
|
-
return "exit";
|
|
6106
|
-
}
|
|
6107
5513
|
if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) {
|
|
6108
|
-
if (locals.get(chatUpgradeRequestedKey)) {
|
|
6109
|
-
await persistUpgradeHandoff();
|
|
6110
|
-
}
|
|
6111
5514
|
return "exit";
|
|
6112
5515
|
}
|
|
6113
5516
|
// Wait for the next message — stay idle briefly, then suspend
|
|
@@ -6214,10 +5617,6 @@ function chatAgent(options) {
|
|
|
6214
5617
|
});
|
|
6215
5618
|
// Signal turn complete so the client knows this turn is done
|
|
6216
5619
|
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
6217
|
-
// A later action's snapshot reuses this cursor, so it has to move
|
|
6218
|
-
// here too or that snapshot resumes from before the failed turn.
|
|
6219
|
-
lastSnapshotOutEventId =
|
|
6220
|
-
errorTurnCompleteResult?.lastEventId ?? lastSnapshotOutEventId;
|
|
6221
5620
|
}
|
|
6222
5621
|
catch {
|
|
6223
5622
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
@@ -6262,47 +5661,15 @@ function chatAgent(options) {
|
|
|
6262
5661
|
: partialIdx === -1
|
|
6263
5662
|
? [...erroredUIMessages, partialResponse]
|
|
6264
5663
|
: erroredUIMessages.map((m, i) => i === partialIdx ? partialResponse : m);
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
* instruction whenever the turn it steered went on to fail.
|
|
6270
|
-
*/
|
|
6271
|
-
const buildErroredNew = () => {
|
|
6272
|
-
const out = [];
|
|
6273
|
-
const addUnique = (m) => {
|
|
6274
|
-
if (m && !out.some((existing) => existing.id === m.id))
|
|
6275
|
-
out.push(m);
|
|
6276
|
-
};
|
|
6277
|
-
addUnique(erroredWireMessage);
|
|
6278
|
-
for (const m of (locals.get(chatTurnNewUIMessagesKey) ?? [])) {
|
|
6279
|
-
addUnique(m);
|
|
6280
|
-
}
|
|
6281
|
-
if (includePartial)
|
|
6282
|
-
addUnique(partialResponse);
|
|
6283
|
-
return out;
|
|
6284
|
-
};
|
|
6285
|
-
let erroredNewUIMessages = buildErroredNew();
|
|
5664
|
+
let erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
|
|
5665
|
+
if (includePartial) {
|
|
5666
|
+
erroredNewUIMessages.push(partialResponse);
|
|
5667
|
+
}
|
|
6286
5668
|
let erroredNewModelMessages = [];
|
|
6287
|
-
const reconciledSteer = reconcilePendingSteer();
|
|
6288
|
-
const backgroundTailThisTurn = reconcilePendingBackground();
|
|
6289
5669
|
if (!responseCommitted) {
|
|
6290
5670
|
try {
|
|
6291
5671
|
if (erroredNewUIMessages.length > 0) {
|
|
6292
|
-
|
|
6293
|
-
* Built in order from the recorded forms rather than by
|
|
6294
|
-
* converting the UI list, so a steer appears in the delta as
|
|
6295
|
-
* the model received it (what `prepare` produced), matching the
|
|
6296
|
-
* lane. The wire message and partial are converted as before.
|
|
6297
|
-
*/
|
|
6298
|
-
const steerModelById = new Map(reconciledSteer.map((e) => [e.ui.id, e.model]));
|
|
6299
|
-
for (const m of erroredNewUIMessages) {
|
|
6300
|
-
const recorded = steerModelById.get(m.id);
|
|
6301
|
-
if (recorded)
|
|
6302
|
-
erroredNewModelMessages.push(...recorded);
|
|
6303
|
-
else
|
|
6304
|
-
erroredNewModelMessages.push(...(await toModelMessages([stripProviderMetadata(m)])));
|
|
6305
|
-
}
|
|
5672
|
+
erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map((m) => stripProviderMetadata(m)));
|
|
6306
5673
|
}
|
|
6307
5674
|
if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
|
|
6308
5675
|
if (partialIdx === -1) {
|
|
@@ -6310,14 +5677,7 @@ function chatAgent(options) {
|
|
|
6310
5677
|
accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
|
|
6311
5678
|
}
|
|
6312
5679
|
else {
|
|
6313
|
-
|
|
6314
|
-
backgroundTailThisTurn);
|
|
6315
|
-
if (!ok) {
|
|
6316
|
-
logger.warn("chat.agent: replaced partial not found at the model lane tail; reconverting the lane");
|
|
6317
|
-
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6318
|
-
laneCompacted = false;
|
|
6319
|
-
laneInjections = [];
|
|
6320
|
-
}
|
|
5680
|
+
accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
|
|
6321
5681
|
}
|
|
6322
5682
|
accumulatedUIMessages = erroredUIMessagesWithPartial;
|
|
6323
5683
|
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
@@ -6326,7 +5686,7 @@ function chatAgent(options) {
|
|
|
6326
5686
|
catch {
|
|
6327
5687
|
erroredNewModelMessages = [];
|
|
6328
5688
|
erroredUIMessagesWithPartial = erroredUIMessages;
|
|
6329
|
-
erroredNewUIMessages =
|
|
5689
|
+
erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
|
|
6330
5690
|
}
|
|
6331
5691
|
}
|
|
6332
5692
|
if (onTurnComplete) {
|
|
@@ -6380,14 +5740,13 @@ function chatAgent(options) {
|
|
|
6380
5740
|
// neither the snapshot nor the replayable `.in` tail.
|
|
6381
5741
|
if (!hydrateMessages) {
|
|
6382
5742
|
try {
|
|
6383
|
-
|
|
6384
|
-
|
|
5743
|
+
const errorSnapshotInCursor = chatInputRouter().resumeFloor();
|
|
5744
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
5745
|
+
version: 1,
|
|
5746
|
+
savedAt: Date.now(),
|
|
6385
5747
|
messages: erroredUIMessagesWithPartial,
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
clientData: turnClientData,
|
|
6389
|
-
lastOutEventId: lastSnapshotOutEventId,
|
|
6390
|
-
nonFinalIds: includePartial && partialResponse ? new Set([partialResponse.id]) : undefined,
|
|
5748
|
+
lastOutEventId: errorTurnCompleteResult?.lastEventId,
|
|
5749
|
+
lastInEventId: errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined,
|
|
6391
5750
|
});
|
|
6392
5751
|
}
|
|
6393
5752
|
catch (error) {
|
|
@@ -6397,15 +5756,8 @@ function chatAgent(options) {
|
|
|
6397
5756
|
});
|
|
6398
5757
|
}
|
|
6399
5758
|
}
|
|
6400
|
-
if (locals.get(chatCloseRequestedKey)) {
|
|
6401
|
-
await performChatClose();
|
|
6402
|
-
return;
|
|
6403
|
-
}
|
|
6404
5759
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
6405
5760
|
if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) {
|
|
6406
|
-
if (locals.get(chatUpgradeRequestedKey)) {
|
|
6407
|
-
await persistUpgradeHandoff();
|
|
6408
|
-
}
|
|
6409
5761
|
return;
|
|
6410
5762
|
}
|
|
6411
5763
|
// Drain remaining recovered turns before idling — a thrown
|
|
@@ -6428,12 +5780,6 @@ function chatAgent(options) {
|
|
|
6428
5780
|
return; // Timed out — end run gracefully
|
|
6429
5781
|
}
|
|
6430
5782
|
currentWirePayload = next.output;
|
|
6431
|
-
// Same close check the success path makes. Without it a close
|
|
6432
|
-
// record that lands after a failed turn is consumed as if it were
|
|
6433
|
-
// a turn payload, and the loop runs on against a closed session.
|
|
6434
|
-
if (currentWirePayload.trigger === "close") {
|
|
6435
|
-
return;
|
|
6436
|
-
}
|
|
6437
5783
|
// Continue to next iteration of the for loop
|
|
6438
5784
|
}
|
|
6439
5785
|
finally {
|
|
@@ -6442,11 +5788,6 @@ function chatAgent(options) {
|
|
|
6442
5788
|
}
|
|
6443
5789
|
}
|
|
6444
5790
|
finally {
|
|
6445
|
-
// Safety net for a close requested on a path that exits without
|
|
6446
|
-
// reaching one of the loop's close checks (a turn timeout, an OOM
|
|
6447
|
-
// re-throw). `performChatClose` is idempotent, so the ordinary path
|
|
6448
|
-
// having already run it costs nothing here.
|
|
6449
|
-
await performChatClose();
|
|
6450
5791
|
// `stopSub` is registered post-preload so the close-during-preload
|
|
6451
5792
|
// early-return path may exit before it ever attached. Guard the
|
|
6452
5793
|
// cleanup so a missing subscription doesn't throw.
|
|
@@ -6784,22 +6125,15 @@ function isStopped() {
|
|
|
6784
6125
|
// Version upgrade
|
|
6785
6126
|
// ---------------------------------------------------------------------------
|
|
6786
6127
|
/**
|
|
6787
|
-
*
|
|
6788
|
-
*
|
|
6789
|
-
* The handover happens immediately and server-side: a successor run is created
|
|
6790
|
-
* and picks the conversation up from `session.in`. The transport keeps reading
|
|
6791
|
-
* the same session output, so no client action is needed and nothing waits for
|
|
6792
|
-
* the next message.
|
|
6793
|
-
*
|
|
6794
|
-
* Without a target the session's pin is cleared, so the successor lands on the
|
|
6795
|
-
* latest deployed version; with `externalDeploymentId` the session is re-pinned
|
|
6796
|
-
* to that deployment.
|
|
6128
|
+
* Request that the current run exits so the next message starts on the latest
|
|
6129
|
+
* deployed version (via the standard continuation mechanism).
|
|
6797
6130
|
*
|
|
6798
6131
|
* When called from `onTurnStart` or `onValidateMessages`, `run()` is skipped
|
|
6799
|
-
* entirely
|
|
6132
|
+
* entirely — the run exits immediately and the transport re-triggers the
|
|
6133
|
+
* same message on the new version.
|
|
6800
6134
|
*
|
|
6801
6135
|
* When called from `run()` or `chat.defer()`, the current turn completes
|
|
6802
|
-
* normally and the
|
|
6136
|
+
* normally and the run exits afterward instead of waiting for the next message.
|
|
6803
6137
|
*
|
|
6804
6138
|
* Call from `onTurnStart`, `onValidateMessages`, `onChatResume`, `run()`,
|
|
6805
6139
|
* or inside `chat.defer()`.
|
|
@@ -6819,37 +6153,8 @@ function isStopped() {
|
|
|
6819
6153
|
* });
|
|
6820
6154
|
* ```
|
|
6821
6155
|
*/
|
|
6822
|
-
function requestUpgrade(
|
|
6156
|
+
function requestUpgrade() {
|
|
6823
6157
|
locals.set(chatUpgradeRequestedKey, true);
|
|
6824
|
-
// Without a target the handoff clears the session's pin; with one it re-pins to that deployment.
|
|
6825
|
-
const target = options?.externalDeploymentId?.trim();
|
|
6826
|
-
if (target)
|
|
6827
|
-
locals.set(chatUpgradeExternalDeploymentIdKey, target);
|
|
6828
|
-
}
|
|
6829
|
-
/** @internal Requests a handoff when the session's pin no longer names this deployment. */
|
|
6830
|
-
async function followSessionPin(chatId, policy) {
|
|
6831
|
-
if (!chatId) {
|
|
6832
|
-
return;
|
|
6833
|
-
}
|
|
6834
|
-
const deployedExternalId = locals.get(chatAgentRunContextKey)?.deployment?.externalId;
|
|
6835
|
-
if (policy !== "hold" && !deployedExternalId) {
|
|
6836
|
-
logger.debug("chat.versionSkew: cannot follow the session pin", {
|
|
6837
|
-
chatId,
|
|
6838
|
-
reason: "the run context carries no deployment.externalId",
|
|
6839
|
-
});
|
|
6840
|
-
}
|
|
6841
|
-
const target = await resolvePinToFollow({
|
|
6842
|
-
policy,
|
|
6843
|
-
deployedExternalId,
|
|
6844
|
-
upgradeAlreadyRequested: locals.get(chatUpgradeRequestedKey) === true,
|
|
6845
|
-
readPin: async () => (await sessions.retrieve(chatId, { retry: { maxAttempts: 2, randomize: true } }))
|
|
6846
|
-
.triggerConfig,
|
|
6847
|
-
});
|
|
6848
|
-
if (!target) {
|
|
6849
|
-
return;
|
|
6850
|
-
}
|
|
6851
|
-
logger.info("chat.versionSkew: following the session pin", { chatId, target });
|
|
6852
|
-
requestUpgrade({ externalDeploymentId: target });
|
|
6853
6158
|
}
|
|
6854
6159
|
/**
|
|
6855
6160
|
* Hand off the current custom agent Session to a fresh run.
|
|
@@ -6888,31 +6193,20 @@ async function endAndContinue() {
|
|
|
6888
6193
|
if ((locals.get(chatActiveSessionIteratorsKey) ?? 0) > 0) {
|
|
6889
6194
|
throw new Error("chat.endAndContinue() cannot be called while a chat.createSession() iterator is active. Close the iterator, then call chat.endAndContinue().");
|
|
6890
6195
|
}
|
|
6891
|
-
await performEndAndContinue(
|
|
6196
|
+
await performEndAndContinue();
|
|
6892
6197
|
}
|
|
6893
6198
|
/** @internal Shared server handoff used by managed and custom agent loops. */
|
|
6894
|
-
async function performEndAndContinue(
|
|
6199
|
+
async function performEndAndContinue() {
|
|
6895
6200
|
const chatId = locals.get(chatExternalIdKey);
|
|
6896
6201
|
const callingRunId = locals.get(chatAgentRunContextKey)?.run.id;
|
|
6897
6202
|
if (!chatId || !callingRunId) {
|
|
6898
6203
|
throw new Error("Cannot end and continue without an active chat agent run");
|
|
6899
6204
|
}
|
|
6900
|
-
const externalDeploymentId = options.externalDeploymentId;
|
|
6901
6205
|
const apiClient = apiClientManager.clientOrThrow();
|
|
6902
|
-
|
|
6206
|
+
await apiClient.endAndContinueSession(chatId, {
|
|
6903
6207
|
callingRunId,
|
|
6904
|
-
reason:
|
|
6905
|
-
...(externalDeploymentId ? { externalDeploymentId } : {}),
|
|
6208
|
+
reason: "upgrade",
|
|
6906
6209
|
});
|
|
6907
|
-
if (result?.pendingVersion !== true) {
|
|
6908
|
-
return;
|
|
6909
|
-
}
|
|
6910
|
-
// The successor parked. Say so on `.out` while this run still can — the transport's
|
|
6911
|
-
// subscription survives the swap, so the client learns without waiting for its next send.
|
|
6912
|
-
const [error] = await tryCatch(getChatSession().out.writeControl(TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION));
|
|
6913
|
-
if (error) {
|
|
6914
|
-
logger.warn("could not signal a parked handoff", { chatId, error });
|
|
6915
|
-
}
|
|
6916
6210
|
}
|
|
6917
6211
|
/**
|
|
6918
6212
|
* Exit the run after the current turn completes, without waiting for the
|
|
@@ -6943,124 +6237,6 @@ async function performEndAndContinue(options) {
|
|
|
6943
6237
|
function endRun() {
|
|
6944
6238
|
locals.set(chatEndRunRequestedKey, true);
|
|
6945
6239
|
}
|
|
6946
|
-
/**
|
|
6947
|
-
* End the whole conversation, permanently. The session row is closed, further
|
|
6948
|
-
* appends are refused, and the run exits without scheduling a continuation.
|
|
6949
|
-
*
|
|
6950
|
-
* This is the session-level stop. {@link endRun} ends the current run and lets
|
|
6951
|
-
* the next message start a fresh one; `chat.close()` ends the session itself,
|
|
6952
|
-
* so there is no next message. Use it for a budget cap, a completed goal,
|
|
6953
|
-
* abuse detection, or a user signing out.
|
|
6954
|
-
*
|
|
6955
|
-
* In a `chat.agent`, call it from `run()`, `prepareStep`, or
|
|
6956
|
-
* `onBeforeTurnComplete`. Called mid-step, it aborts the in-flight
|
|
6957
|
-
* `streamText` the same way the stop signal does, so the partial response is
|
|
6958
|
-
* still captured and streamed. The turn then completes normally, a terminal
|
|
6959
|
-
* `session-closed` record carrying `reason` is written to the response stream,
|
|
6960
|
-
* and the loop exits.
|
|
6961
|
-
*
|
|
6962
|
-
* Prefer `onBeforeTurnComplete` over `onTurnComplete`. It carries the same
|
|
6963
|
-
* fields but still runs while the stream is open, so the closed state rides
|
|
6964
|
-
* out on the turn's final record. `onTurnComplete` runs after that record, so
|
|
6965
|
-
* a close decided there does not reach a reader that has already finished the
|
|
6966
|
-
* turn, and the user only finds out when their next message is refused.
|
|
6967
|
-
*
|
|
6968
|
-
* In a `chat.customAgent`, call it anywhere in your own loop. The close is
|
|
6969
|
-
* performed when `run()` returns, so it lands whether you break out of a
|
|
6970
|
-
* `chat.createSession` loop, return early, or hand-roll the loop entirely.
|
|
6971
|
-
*
|
|
6972
|
-
* Closing is one-way: a closed session cannot be reopened. Its transcript
|
|
6973
|
-
* stays readable.
|
|
6974
|
-
*
|
|
6975
|
-
* @example
|
|
6976
|
-
* ```ts
|
|
6977
|
-
* chat.agent({
|
|
6978
|
-
* id: "budgeted-agent",
|
|
6979
|
-
* onBeforeTurnComplete: async ({ usage }) => {
|
|
6980
|
-
* if (await overBudget(usage)) {
|
|
6981
|
-
* chat.close({ reason: "Monthly budget reached" });
|
|
6982
|
-
* }
|
|
6983
|
-
* },
|
|
6984
|
-
* });
|
|
6985
|
-
* ```
|
|
6986
|
-
*/
|
|
6987
|
-
function close(options) {
|
|
6988
|
-
if (!locals.get(chatExternalIdKey)) {
|
|
6989
|
-
throw new Error("chat.close() can only be called from inside a chat.agent() or chat.customAgent() run");
|
|
6990
|
-
}
|
|
6991
|
-
// Bound the reason once, here. It goes out on S2 record headers as well as
|
|
6992
|
-
// the close API, and an oversized value would fail the turn-complete write
|
|
6993
|
-
// that carries the turn boundary, costing the client far more than the
|
|
6994
|
-
// reason text.
|
|
6995
|
-
// Trailing high surrogate: the cut landed between the two halves of an
|
|
6996
|
-
// astral character, and encoding the orphan to UTF-8 for a record header
|
|
6997
|
-
// yields a replacement character. Drop it rather than ship mojibake.
|
|
6998
|
-
const reason = options?.reason
|
|
6999
|
-
?.slice(0, CHAT_CLOSE_REASON_MAX_LENGTH)
|
|
7000
|
-
.replace(/[\uD800-\uDBFF]$/, "");
|
|
7001
|
-
locals.set(chatCloseRequestedKey, reason ? { reason } : {});
|
|
7002
|
-
// Mid-step call: unblock the in-flight streamText exactly like the stop
|
|
7003
|
-
// signal, so the turn can reach its turn boundary instead of running the
|
|
7004
|
-
// model out to completion after the decision to close has been made.
|
|
7005
|
-
locals.get(chatStopControllerKey)?.abort(reason ?? "closed");
|
|
7006
|
-
}
|
|
7007
|
-
/**
|
|
7008
|
-
* @internal Terminal close sequence, run once at whichever exit site observes
|
|
7009
|
-
* the close request. Writes the standalone `session-closed` record, then closes
|
|
7010
|
-
* the session row.
|
|
7011
|
-
*
|
|
7012
|
-
* The record lands after the turn's `turn-complete`, so a client reading that
|
|
7013
|
-
* turn's stream has already terminated on it and will not see this one. It is
|
|
7014
|
-
* there for a reconnect and for replay. What a live client reads is the
|
|
7015
|
-
* `session-closed` header stamped onto `turn-complete` itself by
|
|
7016
|
-
* `writeTurnCompleteChunk`, which fires whenever the close was decided before
|
|
7017
|
-
* the turn ended. A close decided from `onTurnComplete` is past that point, so
|
|
7018
|
-
* the client learns from the 409 on its next send.
|
|
7019
|
-
*/
|
|
7020
|
-
async function performChatClose() {
|
|
7021
|
-
const request = locals.get(chatCloseRequestedKey);
|
|
7022
|
-
if (!request || locals.get(chatClosePerformedKey))
|
|
7023
|
-
return;
|
|
7024
|
-
const reason = request.reason;
|
|
7025
|
-
// Two flags, not one. The record is a client-visible event and must not be
|
|
7026
|
-
// written twice, but the row close is the part that actually ends the
|
|
7027
|
-
// conversation: flagging it as done before it succeeds would let a transient
|
|
7028
|
-
// failure leave the session open with no later call willing to retry.
|
|
7029
|
-
if (!locals.get(chatCloseRecordWrittenKey)) {
|
|
7030
|
-
locals.set(chatCloseRecordWrittenKey, true);
|
|
7031
|
-
try {
|
|
7032
|
-
const session = getChatSession();
|
|
7033
|
-
await session.out.writeControl(TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED, reason ? [[SESSION_CLOSED_REASON_HEADER, reason]] : undefined);
|
|
7034
|
-
}
|
|
7035
|
-
catch (error) {
|
|
7036
|
-
logger.warn("chat.close: failed to write the session-closed record", {
|
|
7037
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7038
|
-
});
|
|
7039
|
-
}
|
|
7040
|
-
}
|
|
7041
|
-
const chatId = locals.get(chatExternalIdKey);
|
|
7042
|
-
if (!chatId)
|
|
7043
|
-
return;
|
|
7044
|
-
try {
|
|
7045
|
-
await sessions.close(chatId, {
|
|
7046
|
-
...(reason ? { reason } : {}),
|
|
7047
|
-
...(locals.get(chatAgentRunContextKey)?.run.id
|
|
7048
|
-
? { callingRunId: locals.get(chatAgentRunContextKey).run.id }
|
|
7049
|
-
: {}),
|
|
7050
|
-
});
|
|
7051
|
-
locals.set(chatClosePerformedKey, true);
|
|
7052
|
-
}
|
|
7053
|
-
catch (error) {
|
|
7054
|
-
// Deliberately NOT flagged as performed: the close API is idempotent, so a
|
|
7055
|
-
// later exit site on this run gets to retry it. Losing every retry to a
|
|
7056
|
-
// transient failure would leave the row open and the conversation alive.
|
|
7057
|
-
// Non-fatal either way — the run still exits.
|
|
7058
|
-
logger.error("chat.close: failed to close the session", {
|
|
7059
|
-
chatId,
|
|
7060
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7061
|
-
});
|
|
7062
|
-
}
|
|
7063
|
-
}
|
|
7064
6240
|
// ---------------------------------------------------------------------------
|
|
7065
6241
|
// Per-turn deferred work
|
|
7066
6242
|
// ---------------------------------------------------------------------------
|
|
@@ -7122,18 +6298,9 @@ function chatDefer(promiseOrFn) {
|
|
|
7122
6298
|
* ```
|
|
7123
6299
|
*/
|
|
7124
6300
|
function injectBackgroundContext(messages) {
|
|
7125
|
-
const
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
const instructions = locals.get(chatInjectedInstructionsKey) ?? [];
|
|
7129
|
-
instructions.push(...systemBlocks);
|
|
7130
|
-
locals.set(chatInjectedInstructionsKey, instructions);
|
|
7131
|
-
}
|
|
7132
|
-
if (conversational.length > 0) {
|
|
7133
|
-
const queue = locals.get(chatBackgroundQueueKey) ?? [];
|
|
7134
|
-
queue.push(...conversational);
|
|
7135
|
-
locals.set(chatBackgroundQueueKey, queue);
|
|
7136
|
-
}
|
|
6301
|
+
const queue = locals.get(chatBackgroundQueueKey) ?? [];
|
|
6302
|
+
queue.push(...messages);
|
|
6303
|
+
locals.set(chatBackgroundQueueKey, queue);
|
|
7137
6304
|
}
|
|
7138
6305
|
// ---------------------------------------------------------------------------
|
|
7139
6306
|
// Aborted message cleanup
|
|
@@ -7546,12 +6713,10 @@ class ChatMessageAccumulator {
|
|
|
7546
6713
|
// a duplicate, mirroring the chat.agent accumulator.
|
|
7547
6714
|
const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
|
|
7548
6715
|
if (existingIdx !== -1) {
|
|
7549
|
-
const previous = this.uiMessages[existingIdx];
|
|
7550
6716
|
this.uiMessages[existingIdx] = response;
|
|
7551
6717
|
try {
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
}
|
|
6718
|
+
// Reconvert all model messages since we replaced rather than appended.
|
|
6719
|
+
this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
|
|
7555
6720
|
}
|
|
7556
6721
|
catch {
|
|
7557
6722
|
// Conversion failed — leave the existing model messages in place
|
|
@@ -7587,28 +6752,6 @@ class ChatMessageAccumulator {
|
|
|
7587
6752
|
const modelMsgs = await toModelMessages([message]);
|
|
7588
6753
|
this._steeringQueue.push({ uiMessage: message, modelMessages: modelMsgs });
|
|
7589
6754
|
}
|
|
7590
|
-
/**
|
|
7591
|
-
* Record the messages a steering drain consumed.
|
|
7592
|
-
*
|
|
7593
|
-
* The drain only puts them in this step's prompt, so without this they
|
|
7594
|
-
* shape one answer and then exist in neither lane: not in `uiMessages`,
|
|
7595
|
-
* which is what an app persists from, and not in `modelMessages`, which is
|
|
7596
|
-
* what every later turn sends.
|
|
7597
|
-
*
|
|
7598
|
-
* Both lanes are appended to. The model lane is never reconverted from the
|
|
7599
|
-
* UI lane, because `compactIfNeeded` replaces it with a summary and leaves
|
|
7600
|
-
* the UI lane whole: a reconversion would restore everything the summary
|
|
7601
|
-
* replaced.
|
|
7602
|
-
*/
|
|
7603
|
-
async absorbSteering(claimed, injected) {
|
|
7604
|
-
const fresh = claimed.filter((m) => !this.uiMessages.some((e) => e.id === m.id));
|
|
7605
|
-
if (fresh.length === 0)
|
|
7606
|
-
return;
|
|
7607
|
-
this.uiMessages.push(...fresh);
|
|
7608
|
-
// Record what the model received. Only when the whole batch is new is
|
|
7609
|
-
// `injected` known to describe exactly these messages.
|
|
7610
|
-
this.modelMessages.push(...(injected && fresh.length === claimed.length ? injected : await toModelMessages(fresh)));
|
|
7611
|
-
}
|
|
7612
6755
|
/**
|
|
7613
6756
|
* Get and clear unconsumed steering messages.
|
|
7614
6757
|
*/
|
|
@@ -7641,8 +6784,7 @@ class ChatMessageAccumulator {
|
|
|
7641
6784
|
}
|
|
7642
6785
|
// 2. Pending message injection
|
|
7643
6786
|
if (pm && queue.length > 0) {
|
|
7644
|
-
const
|
|
7645
|
-
await this.absorbSteering(claimed, injected);
|
|
6787
|
+
const injected = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
|
|
7646
6788
|
if (injected.length > 0) {
|
|
7647
6789
|
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
7648
6790
|
}
|
|
@@ -7831,7 +6973,7 @@ function trackActiveChatSessionIterator(iterator) {
|
|
|
7831
6973
|
* ```
|
|
7832
6974
|
*/
|
|
7833
6975
|
function createChatSession(payload, options) {
|
|
7834
|
-
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages,
|
|
6976
|
+
const { signal: runSignal, idleTimeoutInSeconds: sessionIdleTimeoutOpt, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, } = options;
|
|
7835
6977
|
const idleTimeoutInSeconds = sessionIdleTimeoutOpt ?? 30;
|
|
7836
6978
|
return {
|
|
7837
6979
|
[Symbol.asyncIterator]() {
|
|
@@ -7920,16 +7062,8 @@ function createChatSession(payload, options) {
|
|
|
7920
7062
|
* without suspending.
|
|
7921
7063
|
*/
|
|
7922
7064
|
if (turn > 0) {
|
|
7923
|
-
if (locals.get(chatCloseRequestedKey)) {
|
|
7924
|
-
await performChatClose();
|
|
7925
|
-
stop.cleanup();
|
|
7926
|
-
return { done: true, value: undefined };
|
|
7927
|
-
}
|
|
7928
7065
|
// chat.requestUpgrade() / chat.endRun() — exit before waiting
|
|
7929
7066
|
if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) {
|
|
7930
|
-
if (locals.get(chatUpgradeRequestedKey)) {
|
|
7931
|
-
await persistUpgradeHandoff();
|
|
7932
|
-
}
|
|
7933
7067
|
stop.cleanup();
|
|
7934
7068
|
return { done: true, value: undefined };
|
|
7935
7069
|
}
|
|
@@ -8032,7 +7166,6 @@ function createChatSession(payload, options) {
|
|
|
8032
7166
|
}
|
|
8033
7167
|
accumulator.applyHandover(pendingHandoverSignal);
|
|
8034
7168
|
}
|
|
8035
|
-
await followSessionPin(currentPayload.chatId, sessionVersionSkew);
|
|
8036
7169
|
// chat.requestUpgrade() called before this turn — signal transport and exit
|
|
8037
7170
|
if (locals.get(chatUpgradeRequestedKey)) {
|
|
8038
7171
|
await writeUpgradeRequiredChunk();
|
|
@@ -8249,8 +7382,7 @@ function createChatSession(payload, options) {
|
|
|
8249
7382
|
}
|
|
8250
7383
|
}
|
|
8251
7384
|
if (sessionPendingMessages) {
|
|
8252
|
-
const
|
|
8253
|
-
await accumulator.absorbSteering(claimed, injected);
|
|
7385
|
+
const injected = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
|
|
8254
7386
|
if (injected.length > 0) {
|
|
8255
7387
|
resultMessages = [...(resultMessages ?? stepMsgs), ...injected];
|
|
8256
7388
|
}
|
|
@@ -8264,11 +7396,6 @@ function createChatSession(payload, options) {
|
|
|
8264
7396
|
async return() {
|
|
8265
7397
|
activeMsgSub?.off();
|
|
8266
7398
|
activeMsgSub = undefined;
|
|
8267
|
-
// Reached when the consumer leaves the `for await` early (`break`,
|
|
8268
|
-
// `return`, a throw). A `chat.close()` from the loop body would
|
|
8269
|
-
// otherwise be dropped: the exit that performs it lives in `next()`,
|
|
8270
|
-
// and `next()` is never called again.
|
|
8271
|
-
await performChatClose();
|
|
8272
7399
|
// `stop` only exists once next() has booted the iterator.
|
|
8273
7400
|
stop?.cleanup();
|
|
8274
7401
|
return { done: true, value: undefined };
|
|
@@ -8525,11 +7652,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8525
7652
|
const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
|
|
8526
7653
|
const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
|
|
8527
7654
|
const idleTimeoutInSeconds = params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
|
|
8528
|
-
// Only `undefined` means "not supplied": a per-call `null` (opt out) has to beat a pinning
|
|
8529
|
-
// action default, which neither truthiness nor `??` would allow.
|
|
8530
|
-
const externalDeploymentId = params.triggerConfig?.externalDeploymentId !== undefined
|
|
8531
|
-
? params.triggerConfig.externalDeploymentId
|
|
8532
|
-
: options?.triggerConfig?.externalDeploymentId;
|
|
8533
7655
|
const triggerConfig = {
|
|
8534
7656
|
basePayload: {
|
|
8535
7657
|
messages: [],
|
|
@@ -8556,10 +7678,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8556
7678
|
lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
|
|
8557
7679
|
}
|
|
8558
7680
|
: {}),
|
|
8559
|
-
...(params.triggerConfig?.ttl !== undefined || options?.triggerConfig?.ttl !== undefined
|
|
8560
|
-
? { ttl: params.triggerConfig?.ttl ?? options?.triggerConfig?.ttl }
|
|
8561
|
-
: {}),
|
|
8562
|
-
...(externalDeploymentId !== undefined ? { externalDeploymentId } : {}),
|
|
8563
7681
|
...(idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds } : {}),
|
|
8564
7682
|
};
|
|
8565
7683
|
const startBody = {
|
|
@@ -8604,7 +7722,6 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
8604
7722
|
publicAccessToken,
|
|
8605
7723
|
runId: created.runId,
|
|
8606
7724
|
sessionId: created.id,
|
|
8607
|
-
...(created.pendingVersion ? { pendingVersion: true } : {}),
|
|
8608
7725
|
};
|
|
8609
7726
|
};
|
|
8610
7727
|
}
|
|
@@ -8638,8 +7755,7 @@ async function callSessionsCreateWithOverride(args) {
|
|
|
8638
7755
|
const init = {
|
|
8639
7756
|
method: "POST",
|
|
8640
7757
|
headers: overrideRequestHeaders(accessToken),
|
|
8641
|
-
|
|
8642
|
-
body: JSON.stringify(withResolvedExternalDeploymentId(args.body)),
|
|
7758
|
+
body: JSON.stringify(args.body),
|
|
8643
7759
|
};
|
|
8644
7760
|
const response = args.fetchOverride
|
|
8645
7761
|
? await args.fetchOverride(url, init, ctx)
|
|
@@ -8704,41 +7820,6 @@ async function mintPublicTokenWithOverride(args) {
|
|
|
8704
7820
|
expirationTime: args.expirationTime,
|
|
8705
7821
|
});
|
|
8706
7822
|
}
|
|
8707
|
-
/**
|
|
8708
|
-
* Creates a server-side helper that reads a conversation from a transcript
|
|
8709
|
-
* storage, for rendering history before the chat connects. Works the same
|
|
8710
|
-
* for every storage, the platform default included, so the browser never
|
|
8711
|
-
* reads a store directly and the secret key stays on the server.
|
|
8712
|
-
*
|
|
8713
|
-
* Wrap it in a Next.js server action (or any server-side handler), scope it
|
|
8714
|
-
* to the authenticated user through `clientData`, and pass the result to
|
|
8715
|
-
* `useLoadTranscript` in the browser.
|
|
8716
|
-
*
|
|
8717
|
-
* @example
|
|
8718
|
-
* ```ts
|
|
8719
|
-
* // actions.ts
|
|
8720
|
-
* "use server";
|
|
8721
|
-
* import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
|
|
8722
|
-
*
|
|
8723
|
-
* export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
|
|
8724
|
-
* ```
|
|
8725
|
-
*/
|
|
8726
|
-
function createChatLoadTranscriptAction(storage, options) {
|
|
8727
|
-
return async (params) => {
|
|
8728
|
-
if (!params.chatId) {
|
|
8729
|
-
throw new Error("chat.createLoadTranscriptAction: params.chatId is required.");
|
|
8730
|
-
}
|
|
8731
|
-
if (options?.apiClient) {
|
|
8732
|
-
const { apiClient, ...rest } = options;
|
|
8733
|
-
return apiClientManager.runWithConfig(apiClient, () => createChatLoadTranscriptAction(storage, rest)(params));
|
|
8734
|
-
}
|
|
8735
|
-
const limit = params.limit ?? options?.limit;
|
|
8736
|
-
return storage.load({ chatId: params.chatId, clientData: params.clientData }, {
|
|
8737
|
-
...(limit !== undefined ? { limit } : {}),
|
|
8738
|
-
...(params.before !== undefined ? { before: params.before } : {}),
|
|
8739
|
-
});
|
|
8740
|
-
};
|
|
8741
|
-
}
|
|
8742
7823
|
export const chat = {
|
|
8743
7824
|
/** Create a chat agent. See {@link chatAgent}. */
|
|
8744
7825
|
agent: chatAgent,
|
|
@@ -8750,12 +7831,8 @@ export const chat = {
|
|
|
8750
7831
|
withClientData,
|
|
8751
7832
|
/** Create a server-side helper for starting (or resuming) a Session for a chatId. See {@link createChatStartSessionAction}. */
|
|
8752
7833
|
createStartSessionAction: createChatStartSessionAction,
|
|
8753
|
-
/** Returns a server-side helper that reads a conversation from a transcript storage. */
|
|
8754
|
-
createLoadTranscriptAction: createChatLoadTranscriptAction,
|
|
8755
7834
|
/** Pipe a stream to the chat transport. See {@link pipeChat}. */
|
|
8756
7835
|
pipe: pipeChat,
|
|
8757
|
-
/** Return from `onAction` to run a turn on the edited history. See {@link chatTurn}. */
|
|
8758
|
-
turn: chatTurn,
|
|
8759
7836
|
/** Create a per-run typed local. See {@link chatLocal}. */
|
|
8760
7837
|
local: chatLocal,
|
|
8761
7838
|
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
|
|
@@ -8776,8 +7853,6 @@ export const chat = {
|
|
|
8776
7853
|
endAndContinue,
|
|
8777
7854
|
/** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */
|
|
8778
7855
|
endRun,
|
|
8779
|
-
/** End the conversation permanently: close the session and exit the run. See {@link close}. */
|
|
8780
|
-
close,
|
|
8781
7856
|
/** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
|
|
8782
7857
|
cleanupAbortedParts,
|
|
8783
7858
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
@@ -8925,16 +8000,6 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
8925
8000
|
if (consumedCursor !== undefined) {
|
|
8926
8001
|
extraHeaders.push([SESSION_IN_CONSUMED_ID_HEADER, String(consumedCursor)]);
|
|
8927
8002
|
}
|
|
8928
|
-
// A close decided before this turn ended rides out on turn-complete. Readers
|
|
8929
|
-
// terminate their stream on turn-complete, so a standalone record written
|
|
8930
|
-
// after it only reaches a reconnect — this header is what a live client sees.
|
|
8931
|
-
const pendingClose = locals.get(chatCloseRequestedKey);
|
|
8932
|
-
if (pendingClose) {
|
|
8933
|
-
extraHeaders.push([SESSION_CLOSED_HEADER, "true"]);
|
|
8934
|
-
if (pendingClose.reason) {
|
|
8935
|
-
extraHeaders.push([SESSION_CLOSED_REASON_HEADER, pendingClose.reason]);
|
|
8936
|
-
}
|
|
8937
|
-
}
|
|
8938
8003
|
const result = await session.out.writeControl(TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE, extraHeaders);
|
|
8939
8004
|
const T_N = result.lastEventId ? Number.parseInt(result.lastEventId, 10) : undefined;
|
|
8940
8005
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
@@ -8992,47 +8057,12 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
8992
8057
|
*
|
|
8993
8058
|
* @internal
|
|
8994
8059
|
*/
|
|
8995
|
-
/**
|
|
8996
|
-
* Persists an upgrade requested after the turn has already run.
|
|
8997
|
-
*
|
|
8998
|
-
* The pre-turn sites reach {@link performEndAndContinue} through
|
|
8999
|
-
* {@link writeUpgradeRequiredChunk}, which is what clears (or re-points) the
|
|
9000
|
-
* session's stored `externalDeploymentId`. The post-turn exits had no such path,
|
|
9001
|
-
* so a `chat.requestUpgrade()` from `run()` or `chat.defer()` left the pin intact
|
|
9002
|
-
* and every continuation re-pinned to the deployment the agent asked to leave.
|
|
9003
|
-
*
|
|
9004
|
-
* No `upgrade-required` chunk is written here: the turn already produced its
|
|
9005
|
-
* answer, so there is nothing for a client to be told about.
|
|
9006
|
-
*/
|
|
9007
|
-
async function persistUpgradeHandoff() {
|
|
9008
|
-
const chatId = locals.get(chatExternalIdKey);
|
|
9009
|
-
const callingRunId = locals.get(chatAgentRunContextKey)?.run.id;
|
|
9010
|
-
if (!chatId || !callingRunId) {
|
|
9011
|
-
return;
|
|
9012
|
-
}
|
|
9013
|
-
try {
|
|
9014
|
-
await performEndAndContinue({
|
|
9015
|
-
reason: "upgrade",
|
|
9016
|
-
externalDeploymentId: locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
9017
|
-
});
|
|
9018
|
-
}
|
|
9019
|
-
catch (error) {
|
|
9020
|
-
logger.warn("upgrade handoff failed; session keeps its current version pin", {
|
|
9021
|
-
chatId,
|
|
9022
|
-
callingRunId,
|
|
9023
|
-
error,
|
|
9024
|
-
});
|
|
9025
|
-
}
|
|
9026
|
-
}
|
|
9027
8060
|
async function writeUpgradeRequiredChunk() {
|
|
9028
8061
|
const chatId = locals.get(chatExternalIdKey);
|
|
9029
8062
|
const callingRunId = locals.get(chatAgentRunContextKey)?.run.id;
|
|
9030
8063
|
if (chatId && callingRunId) {
|
|
9031
8064
|
try {
|
|
9032
|
-
await performEndAndContinue(
|
|
9033
|
-
reason: "upgrade",
|
|
9034
|
-
externalDeploymentId: locals.get(chatUpgradeExternalDeploymentIdKey),
|
|
9035
|
-
});
|
|
8065
|
+
await performEndAndContinue();
|
|
9036
8066
|
}
|
|
9037
8067
|
catch (error) {
|
|
9038
8068
|
// Non-fatal: the next `.in/append` re-triggers via the probe.
|