@trigger.dev/sdk 4.5.0-rc.5 → 4.5.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +178 -5
- package/dist/commonjs/v3/ai.js +603 -119
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +3 -0
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.js +10 -7
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +8 -0
- package/dist/commonjs/v3/chat-server.js +32 -10
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat-server.test.js +51 -0
- package/dist/commonjs/v3/chat-server.test.js.map +1 -1
- package/dist/commonjs/v3/chat.js +34 -6
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +53 -0
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
- package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +11 -6
- package/dist/commonjs/v3/sessions.js +10 -5
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +178 -5
- package/dist/esm/v3/ai.js +603 -120
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.js +3 -0
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.js +10 -7
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +8 -0
- package/dist/esm/v3/chat-server.js +32 -10
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat-server.test.js +51 -0
- package/dist/esm/v3/chat-server.test.js.map +1 -1
- package/dist/esm/v3/chat.js +34 -6
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +53 -0
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/createStartSessionAction.test.js +30 -0
- package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +11 -6
- package/dist/esm/v3/sessions.js +10 -5
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/esm/v3/test/mock-chat-agent.js +1 -0
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/prompts.mdx +430 -0
- package/docs/ai-chat/actions.mdx +115 -0
- package/docs/ai-chat/anatomy.mdx +71 -0
- package/docs/ai-chat/backend.mdx +817 -0
- package/docs/ai-chat/background-injection.mdx +221 -0
- package/docs/ai-chat/changelog.mdx +850 -0
- package/docs/ai-chat/chat-local.mdx +174 -0
- package/docs/ai-chat/client-protocol.mdx +1081 -0
- package/docs/ai-chat/compaction.mdx +411 -0
- package/docs/ai-chat/custom-agents.mdx +364 -0
- package/docs/ai-chat/error-handling.mdx +415 -0
- package/docs/ai-chat/fast-starts.mdx +672 -0
- package/docs/ai-chat/frontend.mdx +580 -0
- package/docs/ai-chat/how-it-works.mdx +230 -0
- package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
- package/docs/ai-chat/mcp.mdx +101 -0
- package/docs/ai-chat/overview.mdx +90 -0
- package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
- package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
- package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
- package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
- package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
- package/docs/ai-chat/patterns/skills.mdx +221 -0
- package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
- package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
- package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
- package/docs/ai-chat/pending-messages.mdx +343 -0
- package/docs/ai-chat/prompt-caching.mdx +206 -0
- package/docs/ai-chat/quick-start.mdx +161 -0
- package/docs/ai-chat/reference.mdx +909 -0
- package/docs/ai-chat/server-chat.mdx +263 -0
- package/docs/ai-chat/sessions.mdx +333 -0
- package/docs/ai-chat/testing.mdx +682 -0
- package/docs/ai-chat/tools.mdx +191 -0
- package/docs/ai-chat/types.mdx +242 -0
- package/docs/ai-chat/upgrade-guide.mdx +515 -0
- package/docs/apikeys.mdx +54 -0
- package/docs/building-with-ai.mdx +261 -0
- package/docs/bulk-actions.mdx +49 -0
- package/docs/changelog.mdx +6 -0
- package/docs/cli-deploy-commands.mdx +9 -0
- package/docs/cli-dev-commands.mdx +9 -0
- package/docs/cli-dev.mdx +8 -0
- package/docs/cli-init-commands.mdx +58 -0
- package/docs/cli-introduction.mdx +25 -0
- package/docs/cli-list-profiles-commands.mdx +42 -0
- package/docs/cli-login-commands.mdx +33 -0
- package/docs/cli-logout-commands.mdx +33 -0
- package/docs/cli-preview-archive.mdx +59 -0
- package/docs/cli-promote-commands.mdx +9 -0
- package/docs/cli-switch.mdx +43 -0
- package/docs/cli-update-commands.mdx +42 -0
- package/docs/cli-whoami-commands.mdx +33 -0
- package/docs/community.mdx +6 -0
- package/docs/config/config-file.mdx +602 -0
- package/docs/config/extensions/additionalFiles.mdx +38 -0
- package/docs/config/extensions/additionalPackages.mdx +40 -0
- package/docs/config/extensions/aptGet.mdx +34 -0
- package/docs/config/extensions/audioWaveform.mdx +20 -0
- package/docs/config/extensions/custom.mdx +380 -0
- package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
- package/docs/config/extensions/esbuildPlugin.mdx +31 -0
- package/docs/config/extensions/ffmpeg.mdx +45 -0
- package/docs/config/extensions/lightpanda.mdx +56 -0
- package/docs/config/extensions/overview.mdx +67 -0
- package/docs/config/extensions/playwright.mdx +195 -0
- package/docs/config/extensions/prismaExtension.mdx +1014 -0
- package/docs/config/extensions/puppeteer.mdx +30 -0
- package/docs/config/extensions/pythonExtension.mdx +182 -0
- package/docs/config/extensions/syncEnvVars.mdx +291 -0
- package/docs/context.mdx +235 -0
- package/docs/database-connections.mdx +213 -0
- package/docs/deploy-environment-variables.mdx +435 -0
- package/docs/deployment/atomic-deployment.mdx +172 -0
- package/docs/deployment/overview.mdx +257 -0
- package/docs/deployment/preview-branches.mdx +224 -0
- package/docs/errors-retrying.mdx +379 -0
- package/docs/github-actions.mdx +222 -0
- package/docs/github-integration.mdx +136 -0
- package/docs/github-repo.mdx +8 -0
- package/docs/help-email.mdx +6 -0
- package/docs/help-slack.mdx +11 -0
- package/docs/hidden-tasks.mdx +56 -0
- package/docs/how-it-works.mdx +454 -0
- package/docs/how-to-reduce-your-spend.mdx +217 -0
- package/docs/idempotency.mdx +504 -0
- package/docs/introduction.mdx +223 -0
- package/docs/limits.mdx +241 -0
- package/docs/logging.mdx +195 -0
- package/docs/machines.mdx +952 -0
- package/docs/manual-setup.mdx +632 -0
- package/docs/mcp-agent-rules.mdx +41 -0
- package/docs/mcp-introduction.mdx +385 -0
- package/docs/mcp-tools.mdx +273 -0
- package/docs/migrating-from-v3.mdx +334 -0
- package/docs/observability/dashboards.mdx +102 -0
- package/docs/observability/query.mdx +585 -0
- package/docs/open-source-contributing.mdx +16 -0
- package/docs/open-source-self-hosting.mdx +541 -0
- package/docs/private-networking/aws-console-setup.mdx +304 -0
- package/docs/private-networking/overview.mdx +144 -0
- package/docs/private-networking/troubleshooting.mdx +78 -0
- package/docs/queue-concurrency.mdx +354 -0
- package/docs/quick-start.mdx +97 -0
- package/docs/realtime/auth.mdx +208 -0
- package/docs/realtime/backend/overview.mdx +45 -0
- package/docs/realtime/backend/streams.mdx +418 -0
- package/docs/realtime/backend/subscribe.mdx +225 -0
- package/docs/realtime/how-it-works.mdx +94 -0
- package/docs/realtime/overview.mdx +63 -0
- package/docs/realtime/react-hooks/overview.mdx +73 -0
- package/docs/realtime/react-hooks/streams.mdx +449 -0
- package/docs/realtime/react-hooks/subscribe.mdx +674 -0
- package/docs/realtime/react-hooks/swr.mdx +87 -0
- package/docs/realtime/react-hooks/triggering.mdx +194 -0
- package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
- package/docs/realtime/run-object.mdx +174 -0
- package/docs/replaying.mdx +72 -0
- package/docs/request-feature.mdx +6 -0
- package/docs/roadmap.mdx +6 -0
- package/docs/run-tests.mdx +20 -0
- package/docs/run-usage.mdx +113 -0
- package/docs/runs/heartbeats.mdx +38 -0
- package/docs/runs/max-duration.mdx +139 -0
- package/docs/runs/metadata.mdx +734 -0
- package/docs/runs/priority.mdx +31 -0
- package/docs/runs.mdx +396 -0
- package/docs/self-hosting/docker.mdx +458 -0
- package/docs/self-hosting/env/supervisor.mdx +74 -0
- package/docs/self-hosting/env/webapp.mdx +276 -0
- package/docs/self-hosting/kubernetes.mdx +601 -0
- package/docs/self-hosting/overview.mdx +108 -0
- package/docs/skills.mdx +85 -0
- package/docs/tags.mdx +120 -0
- package/docs/tasks/overview.mdx +697 -0
- package/docs/tasks/scheduled.mdx +382 -0
- package/docs/tasks/schemaTask.mdx +413 -0
- package/docs/tasks/streams.mdx +884 -0
- package/docs/triggering.mdx +1320 -0
- package/docs/troubleshooting-alerts.mdx +385 -0
- package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
- package/docs/troubleshooting-github-issues.mdx +6 -0
- package/docs/troubleshooting-uptime-status.mdx +6 -0
- package/docs/troubleshooting.mdx +398 -0
- package/docs/upgrading-packages.mdx +80 -0
- package/docs/vercel-integration.mdx +207 -0
- package/docs/versioning.mdx +56 -0
- package/docs/video-walkthrough.mdx +23 -0
- package/docs/wait-for-token.mdx +540 -0
- package/docs/wait-for.mdx +42 -0
- package/docs/wait-until.mdx +53 -0
- package/docs/wait.mdx +18 -0
- package/docs/writing-tasks-introduction.mdx +33 -0
- package/package.json +10 -6
- package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
- package/skills/trigger-authoring-tasks/SKILL.md +254 -0
- package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
- package/skills/trigger-cost-savings/SKILL.md +116 -0
- package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
package/dist/esm/v3/ai.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { accessoryAttributes, apiClientManager, getSchemaParseFn, headerValue, InputStreamOncePromise, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, sessionStreams, SemanticInternalAttributes, taskContext, SESSION_IN_EVENT_ID_HEADER, TRIGGER_CONTROL_SUBTYPE, generateJWT, } from "@trigger.dev/core/v3";
|
|
1
|
+
import { accessoryAttributes, apiClientManager, controlSubtype, getSchemaParseFn, headerValue, InputStreamOncePromise, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, sessionStreams, SemanticInternalAttributes, taskContext, SESSION_IN_EVENT_ID_HEADER, TRIGGER_CONTROL_SUBTYPE, generateJWT, } 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 { convertToModelMessages, dynamicTool, generateId as generateMessageId, getToolName, isToolUIPart, jsonSchema, readUIMessageStream, tool as aiTool, zodSchema, } from "../imports/ai-runtime.js";
|
|
@@ -49,6 +49,10 @@ const chatTurnContextKey = locals.create("chat.turnContext");
|
|
|
49
49
|
* @internal
|
|
50
50
|
*/
|
|
51
51
|
const chatSessionHandleKey = locals.create("chat.sessionHandle");
|
|
52
|
+
// The external `chatId` from the boot payload — the value `ToolCallExecutionOptions.chatId`
|
|
53
|
+
// is documented to carry. Custom-agent loops never set per-turn context, so subtask tool
|
|
54
|
+
// metadata reads this directly rather than the Session handle id.
|
|
55
|
+
const chatExternalIdKey = locals.create("chat.externalId");
|
|
52
56
|
/**
|
|
53
57
|
* S2 seq_num of the most recent `turn-complete` control record written by
|
|
54
58
|
* this worker. Read by `writeTurnCompleteChunk` to know what to trim back
|
|
@@ -68,51 +72,78 @@ const lastTurnCompleteSeqNumKey = locals.create("chat.lastTurnCompleteSeqNum");
|
|
|
68
72
|
* the `.in` subscription so already-processed user messages don't get
|
|
69
73
|
* replayed from S2.
|
|
70
74
|
*
|
|
71
|
-
* Implementation
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* Implementation is a non-blocking records read (`wait=0`) — the
|
|
76
|
+
* endpoint returns everything currently stored (including pre-trim
|
|
77
|
+
* records, since S2 trims are eventually consistent) in one shot, and
|
|
78
|
+
* we keep the LAST matching header. The previous SSE-based scan had to
|
|
79
|
+
* idle-wait a full 5s window to know it reached the tail, which put a
|
|
80
|
+
* constant ~6s tax on every continuation boot.
|
|
77
81
|
*
|
|
78
82
|
* Returns `undefined` if no `turn-complete` carrying the header has been
|
|
79
83
|
* written yet — first-turn-ever, first turn post-OOM-with-no-prior-runs,
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* messages.
|
|
84
|
+
* a `turn-complete` written before this header existed, or a server old
|
|
85
|
+
* enough that the records endpoint doesn't serialize headers. Callers
|
|
86
|
+
* fall back to subscribing `.in` from seq 0 in that case; the slim-wire
|
|
87
|
+
* merge handles any dedup against snapshot-restored messages.
|
|
84
88
|
* @internal
|
|
85
89
|
*/
|
|
86
90
|
async function findLatestSessionInCursor(chatId) {
|
|
87
91
|
const apiClient = apiClientManager.clientOrThrow();
|
|
92
|
+
const response = await apiClient.readSessionStreamRecords(chatId, "out");
|
|
88
93
|
let latestCursor;
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (event.subtype !== TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
99
|
-
return;
|
|
100
|
-
const raw = headerValue(event.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
101
|
-
if (!raw)
|
|
102
|
-
return;
|
|
103
|
-
const parsed = Number.parseInt(raw, 10);
|
|
104
|
-
if (Number.isFinite(parsed))
|
|
105
|
-
latestCursor = parsed;
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
// Drain the stream so the underlying SSE reader runs to completion. We
|
|
109
|
-
// don't accumulate chunks; `onControl` fires inline as turn-complete
|
|
110
|
-
// records arrive.
|
|
111
|
-
for await (const _ of stream) {
|
|
112
|
-
// intentionally empty
|
|
94
|
+
for (const record of response.records) {
|
|
95
|
+
if (controlSubtype(record.headers) !== TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
96
|
+
continue;
|
|
97
|
+
const raw = headerValue(record.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
98
|
+
if (!raw)
|
|
99
|
+
continue;
|
|
100
|
+
const parsed = Number.parseInt(raw, 10);
|
|
101
|
+
if (Number.isFinite(parsed))
|
|
102
|
+
latestCursor = parsed;
|
|
113
103
|
}
|
|
114
104
|
return latestCursor;
|
|
115
105
|
}
|
|
106
|
+
/** Test-only entry point for the records-based cursor scan. @internal */
|
|
107
|
+
export async function __findLatestSessionInCursorForTests(chatId) {
|
|
108
|
+
return findLatestSessionInCursor(chatId);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Seed the `.in` resume cursor for custom-agent loops (`chat.customAgent`
|
|
112
|
+
* raw loops and `chat.createSession`) the way `chat.agent`'s boot does.
|
|
113
|
+
*
|
|
114
|
+
* MUST run before anything attaches a `.in` listener (`createStopSignal`,
|
|
115
|
+
* `chat.messages.on`, the first wait): attaching opens the SSE tail with
|
|
116
|
+
* `Last-Event-ID` from the seeded cursor, so attach-then-seed replays
|
|
117
|
+
* every record from seq 0 — already-answered user messages get delivered
|
|
118
|
+
* into the new run's first wait and the loop re-answers them.
|
|
119
|
+
*
|
|
120
|
+
* Seeds both cursors: `setLastSeqNum` controls the SSE `Last-Event-ID`,
|
|
121
|
+
* `setLastDispatchedSeqNum` gates waiter dispatch — seeding only the
|
|
122
|
+
* former still re-delivers records the manager buffered before the seed.
|
|
123
|
+
*
|
|
124
|
+
* No-ops on fresh boots and when a cursor is already seeded (e.g. the
|
|
125
|
+
* `chatCustomAgent` wrapper ran before a nested `createChatSession`).
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
async function seedSessionInResumeCursorForCustomLoop(payload) {
|
|
129
|
+
if (sessionStreams.lastSeqNum(payload.chatId, "in") !== undefined)
|
|
130
|
+
return;
|
|
131
|
+
// No continuation/attempt gate: the wire may omit `continuation` on a
|
|
132
|
+
// run that still has prior turns (chat.agent covers that case via its
|
|
133
|
+
// snapshot). The scan doubles as the prior-state probe — a fresh
|
|
134
|
+
// session has no turn-complete on `.out`, returns no cursor, and
|
|
135
|
+
// seeds nothing. Cost on fresh boots is one non-blocking records read.
|
|
136
|
+
try {
|
|
137
|
+
const cursor = await findLatestSessionInCursor(payload.chatId);
|
|
138
|
+
if (cursor !== undefined) {
|
|
139
|
+
sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
140
|
+
sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", { error: error instanceof Error ? error.message : String(error) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
116
147
|
let readChatSnapshotImpl;
|
|
117
148
|
export function __setReadChatSnapshotImplForTests(impl) {
|
|
118
149
|
readChatSnapshotImpl = impl;
|
|
@@ -664,6 +695,16 @@ function createTaskToolExecuteHandler(task) {
|
|
|
664
695
|
toolMeta.continuation = chatCtx.continuation;
|
|
665
696
|
toolMeta.clientData = chatCtx.clientData;
|
|
666
697
|
}
|
|
698
|
+
else {
|
|
699
|
+
// Hand-rolled chat.customAgent loops never set per-turn context, but
|
|
700
|
+
// the wrapper records the boot payload's external chatId at run boot
|
|
701
|
+
// — thread it so subtask chat helpers (`chat.stream.writer` with
|
|
702
|
+
// target "root") can open the parent's session.
|
|
703
|
+
const chatExternalId = locals.get(chatExternalIdKey);
|
|
704
|
+
if (chatExternalId) {
|
|
705
|
+
toolMeta.chatId = chatExternalId;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
667
708
|
const chatLocals = {};
|
|
668
709
|
for (const entry of chatLocalRegistry) {
|
|
669
710
|
const value = locals.get(entry.key);
|
|
@@ -980,8 +1021,15 @@ const messagesInput = {
|
|
|
980
1021
|
on(handler) {
|
|
981
1022
|
return getChatSession().in.on((chunk) => {
|
|
982
1023
|
if (chunk.kind === "message") {
|
|
983
|
-
|
|
1024
|
+
// Returning `true` marks the record CONSUMED at the manager level:
|
|
1025
|
+
// it is neither buffered for a later `once()` nor re-delivered by
|
|
1026
|
+
// the buffer drain when the next turn re-attaches its handler.
|
|
1027
|
+
// Without this, a message arriving mid-stream was delivered twice
|
|
1028
|
+
// and ran a duplicate turn.
|
|
1029
|
+
void Promise.resolve(handler(chunk.payload)).catch(() => { });
|
|
1030
|
+
return true;
|
|
984
1031
|
}
|
|
1032
|
+
return undefined;
|
|
985
1033
|
});
|
|
986
1034
|
},
|
|
987
1035
|
once(options) {
|
|
@@ -1075,8 +1123,13 @@ const stopInput = {
|
|
|
1075
1123
|
on(handler) {
|
|
1076
1124
|
return getChatSession().in.on((chunk) => {
|
|
1077
1125
|
if (chunk.kind === "stop") {
|
|
1078
|
-
|
|
1126
|
+
// Consume stop records (see the messages facade above). A stop is
|
|
1127
|
+
// only meaningful to the turn it interrupts — buffering it would
|
|
1128
|
+
// let a stale stop abort a future turn.
|
|
1129
|
+
void Promise.resolve(handler({ stop: true, message: chunk.message })).catch(() => { });
|
|
1130
|
+
return true;
|
|
1079
1131
|
}
|
|
1132
|
+
return undefined;
|
|
1080
1133
|
});
|
|
1081
1134
|
},
|
|
1082
1135
|
once(options) {
|
|
@@ -1184,6 +1237,36 @@ const handoverInput = {
|
|
|
1184
1237
|
}
|
|
1185
1238
|
},
|
|
1186
1239
|
};
|
|
1240
|
+
/**
|
|
1241
|
+
* Wait for a `chat.headStart` handover signal inside a custom-agent loop or
|
|
1242
|
+
* `chat.createSession`. Returns:
|
|
1243
|
+
* - `null` — this run is not a `handover-prepare` boot, or the wait idled out /
|
|
1244
|
+
* the warm handler crashed before signaling. Treat as "no handover".
|
|
1245
|
+
* - `{ kind: "handover-skip" }` — the warm handler aborted; exit without a turn.
|
|
1246
|
+
* - `{ kind: "handover", partialAssistantMessage, messageId?, isFinal }` — splice
|
|
1247
|
+
* the partial (`chat.MessageAccumulator.applyHandover`) and, when `isFinal` is
|
|
1248
|
+
* false, fall through to `streamText` to run the handed-over tool round.
|
|
1249
|
+
*
|
|
1250
|
+
* For the common case prefer `accumulator.consumeHandover()`, which also seeds
|
|
1251
|
+
* `payload.headStartMessages` and applies the partial for you.
|
|
1252
|
+
*
|
|
1253
|
+
* Must be called at turn 0 before any `chat.messages.waitWithIdleTimeout` —
|
|
1254
|
+
* that facade consumes and discards non-message chunks, which would swallow the
|
|
1255
|
+
* handover signal.
|
|
1256
|
+
*/
|
|
1257
|
+
async function waitForHandover(options) {
|
|
1258
|
+
if (options.payload.trigger !== "handover-prepare")
|
|
1259
|
+
return null;
|
|
1260
|
+
const result = await handoverInput.waitWithIdleTimeout({
|
|
1261
|
+
idleTimeoutInSeconds: options.idleTimeoutInSeconds ?? options.payload.idleTimeoutInSeconds ?? 60,
|
|
1262
|
+
timeout: options.timeout,
|
|
1263
|
+
spanName: options.spanName ?? "waiting for handover signal",
|
|
1264
|
+
});
|
|
1265
|
+
// Non-ok = idle timeout or the warm handler crashed without signaling.
|
|
1266
|
+
if (!result.ok)
|
|
1267
|
+
return null;
|
|
1268
|
+
return result.output;
|
|
1269
|
+
}
|
|
1187
1270
|
/**
|
|
1188
1271
|
* Per-turn deferred promises. Registered via `chat.defer()`, awaited
|
|
1189
1272
|
* before `onTurnComplete` fires. Reset each turn.
|
|
@@ -1230,6 +1313,9 @@ const chatHandoverIsFinalKey = locals.create("chat.handoverIsFinal");
|
|
|
1230
1313
|
* `tool-approval-response` rows are AI-SDK-internal and don't need a
|
|
1231
1314
|
* UIMessage representation. We map:
|
|
1232
1315
|
* - `text` parts → `{ type: "text", text }`
|
|
1316
|
+
* - `reasoning` parts → `{ type: "reasoning", text, state: "done" }`
|
|
1317
|
+
* (provider metadata carried so an Anthropic thinking signature
|
|
1318
|
+
* survives a UIMessage → ModelMessage round trip)
|
|
1233
1319
|
* - `tool-call` parts → `{ type: "tool-${name}", toolCallId,
|
|
1234
1320
|
* state: "input-available", input }`
|
|
1235
1321
|
* - `tool-approval-request` parts → skipped (AI SDK derives the
|
|
@@ -1246,6 +1332,14 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1246
1332
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1247
1333
|
parts.push({ type: "text", text: part.text });
|
|
1248
1334
|
}
|
|
1335
|
+
else if (part.type === "reasoning" && typeof part.text === "string") {
|
|
1336
|
+
parts.push({
|
|
1337
|
+
type: "reasoning",
|
|
1338
|
+
text: part.text,
|
|
1339
|
+
state: "done",
|
|
1340
|
+
...(part.providerOptions ? { providerMetadata: part.providerOptions } : {}),
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1249
1343
|
else if (part.type === "tool-call" && part.toolCallId && part.toolName) {
|
|
1250
1344
|
parts.push({
|
|
1251
1345
|
type: `tool-${part.toolName}`,
|
|
@@ -1269,6 +1363,27 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1269
1363
|
parts,
|
|
1270
1364
|
};
|
|
1271
1365
|
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Splice a head-start handover partial into an accumulating message pair
|
|
1368
|
+
* (model + UI). Dedups by `messageId` against the UI chain (so a hydrated
|
|
1369
|
+
* history that already persisted the partial isn't doubled), then pushes the
|
|
1370
|
+
* partial into `modelMessages` and the synthesized UIMessage into `uiMessages`.
|
|
1371
|
+
* Shared by the `chat.agent` turn-0 splice and `ChatMessageAccumulator.applyHandover`.
|
|
1372
|
+
* @internal
|
|
1373
|
+
*/
|
|
1374
|
+
function spliceHandoverPartial(modelMessages, uiMessages, signal) {
|
|
1375
|
+
if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
// Skip if the hydrated chain already persisted the partial under this id.
|
|
1379
|
+
const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
|
|
1380
|
+
if (alreadyInChain)
|
|
1381
|
+
return;
|
|
1382
|
+
modelMessages.push(...signal.partialAssistantMessage);
|
|
1383
|
+
const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
|
|
1384
|
+
if (partialUI)
|
|
1385
|
+
uiMessages.push(partialUI);
|
|
1386
|
+
}
|
|
1272
1387
|
/**
|
|
1273
1388
|
* Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
|
|
1274
1389
|
* are drained at the next `prepareStep` boundary and appended to the model messages.
|
|
@@ -2221,11 +2336,18 @@ function isCompactionSafe(messages) {
|
|
|
2221
2336
|
}
|
|
2222
2337
|
/** @internal */
|
|
2223
2338
|
const chatPromptKey = locals.create("chat.prompt");
|
|
2339
|
+
/**
|
|
2340
|
+
* @internal Provider options attached to the system message that
|
|
2341
|
+
* `toStreamTextOptions()` builds from the stored prompt — lets a provider cache
|
|
2342
|
+
* the system block. Stored separately so it works for both the `ResolvedPrompt`
|
|
2343
|
+
* and plain-string forms without mutating the prompt object.
|
|
2344
|
+
*/
|
|
2345
|
+
const chatPromptProviderOptionsKey = locals.create("chat.prompt.providerOptions");
|
|
2224
2346
|
/**
|
|
2225
2347
|
* Store a resolved prompt (or plain string) for the current run.
|
|
2226
2348
|
* Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
|
|
2227
2349
|
*/
|
|
2228
|
-
function setChatPrompt(resolved) {
|
|
2350
|
+
function setChatPrompt(resolved, options) {
|
|
2229
2351
|
if (typeof resolved === "string") {
|
|
2230
2352
|
locals.set(chatPromptKey, {
|
|
2231
2353
|
text: resolved,
|
|
@@ -2242,6 +2364,9 @@ function setChatPrompt(resolved) {
|
|
|
2242
2364
|
else {
|
|
2243
2365
|
locals.set(chatPromptKey, resolved);
|
|
2244
2366
|
}
|
|
2367
|
+
// Always overwrite the slot (even with undefined) so a later prompt.set with
|
|
2368
|
+
// no options clears a previous prompt's cache opt-in rather than leaking it.
|
|
2369
|
+
locals.set(chatPromptProviderOptionsKey, options?.providerOptions);
|
|
2245
2370
|
}
|
|
2246
2371
|
/**
|
|
2247
2372
|
* Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
|
|
@@ -2407,7 +2532,21 @@ function toStreamTextOptions(options) {
|
|
|
2407
2532
|
const promptText = prompt?.text ?? "";
|
|
2408
2533
|
const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
|
|
2409
2534
|
if (promptText || skillsText) {
|
|
2410
|
-
|
|
2535
|
+
const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
|
|
2536
|
+
// Resolve system-prompt provider options for caching. Precedence (most
|
|
2537
|
+
// specific wins, no deep merge): explicit `systemProviderOptions` →
|
|
2538
|
+
// `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
|
|
2539
|
+
const systemProviderOptions = options?.systemProviderOptions ??
|
|
2540
|
+
(options?.cacheControl
|
|
2541
|
+
? { anthropic: { cacheControl: options.cacheControl } }
|
|
2542
|
+
: undefined) ??
|
|
2543
|
+
locals.get(chatPromptProviderOptionsKey);
|
|
2544
|
+
// A bare string stays a bare string (the unchanged default). With provider
|
|
2545
|
+
// options, emit a structured `SystemModelMessage` so the provider can cache
|
|
2546
|
+
// the system block — `streamText`'s `system` accepts string | message.
|
|
2547
|
+
result.system = systemProviderOptions
|
|
2548
|
+
? { role: "system", content: systemText, providerOptions: systemProviderOptions }
|
|
2549
|
+
: systemText;
|
|
2411
2550
|
}
|
|
2412
2551
|
// Prompt-related options (only if chat.prompt.set() was called)
|
|
2413
2552
|
if (prompt) {
|
|
@@ -2589,10 +2728,20 @@ function chatCustomAgent(options) {
|
|
|
2589
2728
|
// `chat.createStartSessionAction`) before this run is triggered.
|
|
2590
2729
|
// No client-side upsert needed.
|
|
2591
2730
|
locals.set(chatSessionHandleKey, sessions.open(payload.chatId));
|
|
2731
|
+
locals.set(chatExternalIdKey, payload.chatId);
|
|
2592
2732
|
locals.set(chatAgentRunContextKey, runOptions.ctx);
|
|
2733
|
+
// Initialize the turn-complete trim slot so `chat.writeTurnComplete`
|
|
2734
|
+
// trims `session.out` back to the previous turn boundary. Without
|
|
2735
|
+
// this the slot is undefined and the trim never runs, so `.out`
|
|
2736
|
+
// grows without bound for the whole custom-agent surface.
|
|
2737
|
+
locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
|
|
2593
2738
|
markChatAgentRunForStreamsWarning();
|
|
2594
2739
|
taskContext.setConversationId(payload.chatId);
|
|
2595
2740
|
stampConversationIdOnActiveSpan(payload.chatId);
|
|
2741
|
+
// Seed the `.in` resume cursor before user code attaches any `.in`
|
|
2742
|
+
// listener — otherwise a continuation boot replays already-answered
|
|
2743
|
+
// messages into the loop's first wait.
|
|
2744
|
+
await seedSessionInResumeCursorForCustomLoop(payload);
|
|
2596
2745
|
return userRun(payload, runOptions);
|
|
2597
2746
|
},
|
|
2598
2747
|
});
|
|
@@ -2639,6 +2788,7 @@ function chatAgent(options) {
|
|
|
2639
2788
|
// `chat.createStartSessionAction` or browser-direct) before this
|
|
2640
2789
|
// run is triggered — no client-side upsert needed here.
|
|
2641
2790
|
locals.set(chatSessionHandleKey, sessions.open(payload.chatId));
|
|
2791
|
+
locals.set(chatExternalIdKey, payload.chatId);
|
|
2642
2792
|
// Mutable holder; advances in `writeTurnCompleteChunk` after each turn
|
|
2643
2793
|
// and is the trim target for the NEXT turn's trim record.
|
|
2644
2794
|
locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
|
|
@@ -2715,6 +2865,11 @@ function chatAgent(options) {
|
|
|
2715
2865
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
2716
2866
|
const bootInjectedQueue = [];
|
|
2717
2867
|
const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
|
|
2868
|
+
// `.in` resume cursor, computed at most once per boot. The boot
|
|
2869
|
+
// block below resolves it (snapshot field or records scan) and the
|
|
2870
|
+
// resume-cursor block reuses it instead of re-scanning.
|
|
2871
|
+
let bootInCursor;
|
|
2872
|
+
let bootInCursorResolved = false;
|
|
2718
2873
|
if (!hydrateMessages && couldHavePriorState) {
|
|
2719
2874
|
// Single parent span for the whole boot read phase — snapshot
|
|
2720
2875
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
@@ -2750,23 +2905,28 @@ function chatAgent(options) {
|
|
|
2750
2905
|
slot.value = seeded;
|
|
2751
2906
|
}
|
|
2752
2907
|
}
|
|
2753
|
-
//
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2908
|
+
// The `.out` replay and the `.in` cursor + tail read are
|
|
2909
|
+
// independent (both depend only on the snapshot) — run them
|
|
2910
|
+
// concurrently. Each phase keeps its own catch + duration
|
|
2911
|
+
// attribute.
|
|
2912
|
+
const replayOutPhase = async () => {
|
|
2913
|
+
const replayOutStart = Date.now();
|
|
2914
|
+
try {
|
|
2915
|
+
const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
|
|
2916
|
+
replayedSettled = replayResult.settled;
|
|
2917
|
+
replayedPartial = replayResult.partial;
|
|
2918
|
+
replayedPartialRaw = replayResult.partialRaw;
|
|
2919
|
+
}
|
|
2920
|
+
catch (error) {
|
|
2921
|
+
logger.warn("chat.agent: session.out replay failed; using snapshot only", {
|
|
2922
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2923
|
+
sessionId: sessionIdForSnapshot,
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart);
|
|
2927
|
+
bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length);
|
|
2928
|
+
bootSpan.setAttribute("chat.boot.replay.out.partialPresent", replayedPartial !== undefined);
|
|
2929
|
+
};
|
|
2770
2930
|
// session.in tail read
|
|
2771
2931
|
//
|
|
2772
2932
|
// session.in carries the user-side of the conversation
|
|
@@ -2777,20 +2937,43 @@ function chatAgent(options) {
|
|
|
2777
2937
|
// visible via the live SSE subscription — by which point they
|
|
2778
2938
|
// would arrive AFTER the partial-assistant orphan and look like
|
|
2779
2939
|
// brand-new turns to the model, producing inverted chains.
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2940
|
+
//
|
|
2941
|
+
// The cursor comes from the snapshot when present (written
|
|
2942
|
+
// there since `lastInEventId` was added) — otherwise from a
|
|
2943
|
+
// records scan of `.out`'s latest turn-complete header.
|
|
2944
|
+
const replayInPhase = async () => {
|
|
2945
|
+
const replayInStart = Date.now();
|
|
2946
|
+
const snapshotInCursor = bootSnapshot?.lastInEventId !== undefined
|
|
2947
|
+
? Number.parseInt(bootSnapshot.lastInEventId, 10)
|
|
2948
|
+
: undefined;
|
|
2949
|
+
if (snapshotInCursor !== undefined && Number.isFinite(snapshotInCursor)) {
|
|
2950
|
+
bootInCursor = snapshotInCursor;
|
|
2951
|
+
bootInCursorResolved = true;
|
|
2952
|
+
}
|
|
2953
|
+
else {
|
|
2954
|
+
try {
|
|
2955
|
+
bootInCursor = await findLatestSessionInCursor(payload.chatId);
|
|
2956
|
+
bootInCursorResolved = true;
|
|
2957
|
+
}
|
|
2958
|
+
catch {
|
|
2959
|
+
// Transient scan failure: leave unresolved so the
|
|
2960
|
+
// resume-cursor block below retries the lookup.
|
|
2961
|
+
bootInCursor = undefined;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
bootSpan.setAttribute("chat.boot.replay.in.cursorFromSnapshot", snapshotInCursor !== undefined);
|
|
2965
|
+
try {
|
|
2966
|
+
replayedInTail = await replaySessionInTail(payload.chatId, {
|
|
2967
|
+
lastEventId: bootInCursor !== undefined ? String(bootInCursor) : undefined,
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
catch (error) {
|
|
2971
|
+
logger.warn("chat.agent: session.in replay failed; in-flight users may not be recovered", { error: error instanceof Error ? error.message : String(error) });
|
|
2972
|
+
}
|
|
2973
|
+
bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart);
|
|
2974
|
+
bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length);
|
|
2975
|
+
};
|
|
2976
|
+
await Promise.all([replayOutPhase(), replayInPhase()]);
|
|
2794
2977
|
}, {
|
|
2795
2978
|
attributes: {
|
|
2796
2979
|
[SemanticInternalAttributes.STYLE_ICON]: "tabler-rotate-clockwise",
|
|
@@ -2831,7 +3014,12 @@ function chatAgent(options) {
|
|
|
2831
3014
|
bootSnapshot !== undefined;
|
|
2832
3015
|
if (needsResumeCursor) {
|
|
2833
3016
|
try {
|
|
2834
|
-
|
|
3017
|
+
// Reuse the cursor the boot block already resolved (snapshot
|
|
3018
|
+
// field or records scan) — only scan here when the boot block
|
|
3019
|
+
// was skipped (hydrateMessages, or snapshot-only signals).
|
|
3020
|
+
const cursor = bootInCursorResolved
|
|
3021
|
+
? bootInCursor
|
|
3022
|
+
: await findLatestSessionInCursor(payload.chatId);
|
|
2835
3023
|
if (cursor !== undefined) {
|
|
2836
3024
|
sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
2837
3025
|
sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
@@ -3595,6 +3783,18 @@ function chatAgent(options) {
|
|
|
3595
3783
|
// therefore a delta merge, not a full-history reset.
|
|
3596
3784
|
if (currentWirePayload.trigger !== "action") {
|
|
3597
3785
|
let cleanedUIMessages = cleanedIncomingMessages;
|
|
3786
|
+
// Turn-0 head-start with hydrateMessages: the boot seeding from
|
|
3787
|
+
// `payload.headStartMessages` is non-hydrate-only, so ship the
|
|
3788
|
+
// route handler's first-turn history to the hydrate hook as
|
|
3789
|
+
// incoming messages instead (gated on the pending handover).
|
|
3790
|
+
if (turn === 0 &&
|
|
3791
|
+
hydrateMessages &&
|
|
3792
|
+
cleanedUIMessages.length === 0 &&
|
|
3793
|
+
(locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
|
|
3794
|
+
Array.isArray(payload.headStartMessages) &&
|
|
3795
|
+
payload.headStartMessages.length > 0) {
|
|
3796
|
+
cleanedUIMessages = payload.headStartMessages;
|
|
3797
|
+
}
|
|
3598
3798
|
// Validate/transform UIMessages before conversion — catches malformed
|
|
3599
3799
|
// messages from storage or untrusted input before they reach the model.
|
|
3600
3800
|
// Slim wire: triggers like `regenerate-message` carry no incoming
|
|
@@ -3773,40 +3973,39 @@ function chatAgent(options) {
|
|
|
3773
3973
|
// `preload` / `close` / `handover-prepare` and submits
|
|
3774
3974
|
// with no incoming message fall through with the boot-
|
|
3775
3975
|
// seeded accumulator unchanged.
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
}
|
|
3976
|
+
}
|
|
3977
|
+
if (turn === 0) {
|
|
3978
|
+
// Head-start handover splice (turn 0 only, BOTH
|
|
3979
|
+
// accumulation branches — hydrate and default): the
|
|
3980
|
+
// `chat.handover` route handler signalled a mid-turn
|
|
3981
|
+
// handover, so splice its partial assistant response
|
|
3982
|
+
// (text + pending tool-calls + the synthesized
|
|
3983
|
+
// tool-approval round) onto the accumulator.
|
|
3984
|
+
// `streamText` then hits AI SDK's initial-tool-
|
|
3985
|
+
// execution branch, runs the agent-side tool executes,
|
|
3986
|
+
// and resumes from step 2 — skipping the first model
|
|
3987
|
+
// call (already done by the handler).
|
|
3988
|
+
//
|
|
3989
|
+
// We also synthesize a UIMessage form of the partial
|
|
3990
|
+
// assistant and push it to `accumulatedUIMessages` so
|
|
3991
|
+
// AI SDK's `processUIMessageStream` (invoked when the
|
|
3992
|
+
// run loop calls `runResult.toUIMessageStream({
|
|
3993
|
+
// onFinish })`) can initialize `state.message` from
|
|
3994
|
+
// the trailing assistant in `originalMessages`. Without
|
|
3995
|
+
// that, the `tool-output-available` chunks emitted by
|
|
3996
|
+
// the initial-tool-execution branch can't find their
|
|
3997
|
+
// matching tool-call in state and AI SDK throws
|
|
3998
|
+
// `UIMessageStreamError: No tool invocation found`.
|
|
3999
|
+
const pendingHandoverPartial = locals.get(chatHandoverPartialKey);
|
|
4000
|
+
if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
|
|
4001
|
+
spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
|
|
4002
|
+
partialAssistantMessage: pendingHandoverPartial,
|
|
4003
|
+
messageId: locals.get(chatHandoverMessageIdKey),
|
|
4004
|
+
});
|
|
4005
|
+
locals.set(chatHandoverPartialKey, []); // consume once
|
|
3807
4006
|
}
|
|
3808
|
-
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3809
4007
|
}
|
|
4008
|
+
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3810
4009
|
} // end if (trigger !== "action")
|
|
3811
4010
|
// ── Action result handling ──────────────────────────────
|
|
3812
4011
|
// For action turns, skip the turn machinery entirely.
|
|
@@ -4503,11 +4702,15 @@ function chatAgent(options) {
|
|
|
4503
4702
|
if (!hydrateMessages) {
|
|
4504
4703
|
try {
|
|
4505
4704
|
await tracer.startActiveSpan("snapshot.write", async () => {
|
|
4705
|
+
const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4506
4706
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4507
4707
|
version: 1,
|
|
4508
4708
|
savedAt: Date.now(),
|
|
4509
4709
|
messages: accumulatedUIMessages,
|
|
4510
4710
|
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
4711
|
+
lastInEventId: snapshotInCursor !== undefined
|
|
4712
|
+
? String(snapshotInCursor)
|
|
4713
|
+
: undefined,
|
|
4511
4714
|
});
|
|
4512
4715
|
}, {
|
|
4513
4716
|
attributes: {
|
|
@@ -4644,17 +4847,100 @@ function chatAgent(options) {
|
|
|
4644
4847
|
if (turnError instanceof OutOfMemoryError) {
|
|
4645
4848
|
throw turnError;
|
|
4646
4849
|
}
|
|
4850
|
+
let errorTurnCompleteResult;
|
|
4647
4851
|
try {
|
|
4648
4852
|
await withChatWriter(async (writer) => {
|
|
4649
4853
|
const errorText = turnError instanceof Error ? turnError.message : "An unexpected error occurred";
|
|
4650
4854
|
writer.write({ type: "error", errorText });
|
|
4651
4855
|
});
|
|
4652
4856
|
// Signal turn complete so the client knows this turn is done
|
|
4653
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4857
|
+
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4654
4858
|
}
|
|
4655
4859
|
catch {
|
|
4656
4860
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
4657
4861
|
}
|
|
4862
|
+
// The submit-message merge into the accumulator may not have run
|
|
4863
|
+
// yet (a pre-run hook threw), so fold the wire message in for the
|
|
4864
|
+
// error event + snapshot — the cursor has already advanced past it,
|
|
4865
|
+
// so otherwise it survives in neither the snapshot nor the `.in` tail.
|
|
4866
|
+
const erroredWireMessage = currentWirePayload.message;
|
|
4867
|
+
const erroredUIMessages = erroredWireMessage &&
|
|
4868
|
+
!accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
|
|
4869
|
+
? [...accumulatedUIMessages, erroredWireMessage]
|
|
4870
|
+
: accumulatedUIMessages;
|
|
4871
|
+
// Fire onTurnComplete on the error path too — the docs promise it
|
|
4872
|
+
// runs "after every turn, successful or errored" so customers can
|
|
4873
|
+
// mark the turn failed. `responseMessage` is undefined/partial and
|
|
4874
|
+
// `error` carries the thrown value.
|
|
4875
|
+
if (onTurnComplete) {
|
|
4876
|
+
try {
|
|
4877
|
+
await tracer.startActiveSpan("onTurnComplete()", async () => {
|
|
4878
|
+
await onTurnComplete({
|
|
4879
|
+
ctx,
|
|
4880
|
+
chatId: currentWirePayload.chatId,
|
|
4881
|
+
messages: accumulatedMessages,
|
|
4882
|
+
uiMessages: erroredUIMessages,
|
|
4883
|
+
newMessages: [],
|
|
4884
|
+
newUIMessages: erroredWireMessage ? [erroredWireMessage] : [],
|
|
4885
|
+
responseMessage: undefined,
|
|
4886
|
+
rawResponseMessage: undefined,
|
|
4887
|
+
turn,
|
|
4888
|
+
runId: ctx.run.id,
|
|
4889
|
+
chatAccessToken: "",
|
|
4890
|
+
// Parsed `clientData` isn't reliably in scope here (parsing
|
|
4891
|
+
// may itself be the failure), and the raw metadata is the
|
|
4892
|
+
// wrong shape — leave it undefined on the error path.
|
|
4893
|
+
clientData: undefined,
|
|
4894
|
+
stopped: false,
|
|
4895
|
+
continuation,
|
|
4896
|
+
previousRunId,
|
|
4897
|
+
preloaded,
|
|
4898
|
+
totalUsage: cumulativeUsage,
|
|
4899
|
+
finishReason: "error",
|
|
4900
|
+
error: turnError,
|
|
4901
|
+
lastEventId: errorTurnCompleteResult?.lastEventId,
|
|
4902
|
+
});
|
|
4903
|
+
}, {
|
|
4904
|
+
attributes: {
|
|
4905
|
+
[SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
4906
|
+
[SemanticInternalAttributes.COLLAPSED]: true,
|
|
4907
|
+
"chat.id": currentWirePayload.chatId,
|
|
4908
|
+
"chat.turn": turn + 1,
|
|
4909
|
+
"chat.errored": true,
|
|
4910
|
+
},
|
|
4911
|
+
});
|
|
4912
|
+
}
|
|
4913
|
+
catch {
|
|
4914
|
+
// A throwing onTurnComplete on the error path must not crash
|
|
4915
|
+
// the run — keep the conversation alive for the next message.
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
// Persist a snapshot so the failed turn's user message isn't
|
|
4919
|
+
// stranded. `writeTurnCompleteChunk` already advanced the `.in`
|
|
4920
|
+
// cursor past it (via the session-in-event-id header), and the
|
|
4921
|
+
// success-path snapshot write is skipped on error — without this
|
|
4922
|
+
// the next boot would resume past a message that exists in
|
|
4923
|
+
// neither the snapshot nor the replayable `.in` tail.
|
|
4924
|
+
if (!hydrateMessages) {
|
|
4925
|
+
try {
|
|
4926
|
+
const errorSnapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4927
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4928
|
+
version: 1,
|
|
4929
|
+
savedAt: Date.now(),
|
|
4930
|
+
messages: erroredUIMessages,
|
|
4931
|
+
lastOutEventId: errorTurnCompleteResult?.lastEventId,
|
|
4932
|
+
lastInEventId: errorSnapshotInCursor !== undefined
|
|
4933
|
+
? String(errorSnapshotInCursor)
|
|
4934
|
+
: undefined,
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
catch (error) {
|
|
4938
|
+
logger.warn("chat.agent: error-path snapshot write failed", {
|
|
4939
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4940
|
+
sessionId: sessionIdForSnapshot,
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4658
4944
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
4659
4945
|
if (locals.get(chatUpgradeRequestedKey) ||
|
|
4660
4946
|
locals.get(chatEndRunRequestedKey)) {
|
|
@@ -5275,8 +5561,19 @@ async function pipeChatAndCapture(source, options) {
|
|
|
5275
5561
|
const onFinishPromise = new Promise((r) => {
|
|
5276
5562
|
resolveOnFinish = r;
|
|
5277
5563
|
});
|
|
5564
|
+
const resolvedOptions = resolveUIMessageStreamOptions();
|
|
5278
5565
|
const uiStream = source.toUIMessageStream({
|
|
5279
|
-
...
|
|
5566
|
+
...resolvedOptions,
|
|
5567
|
+
// Thread the prior chain (incl. a spliced handover partial) so a resumed
|
|
5568
|
+
// tool round's tool-output chunks merge into the originating tool-call
|
|
5569
|
+
// instead of throwing "No tool invocation found".
|
|
5570
|
+
...(options?.originalMessages ? { originalMessages: options.originalMessages } : {}),
|
|
5571
|
+
// Stamp a server-generated id on the start chunk, same as chat.agent's
|
|
5572
|
+
// pipe. Without it the AI SDK regenerates the assistant id when a
|
|
5573
|
+
// prepareStep injection (steering) starts a new step mid-stream, and
|
|
5574
|
+
// the frontend replaces the partial message — wiping the
|
|
5575
|
+
// pre-injection text from the UI and the captured response.
|
|
5576
|
+
generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId,
|
|
5280
5577
|
onFinish: ({ responseMessage }) => {
|
|
5281
5578
|
captured = responseMessage;
|
|
5282
5579
|
resolveOnFinish();
|
|
@@ -5346,10 +5643,65 @@ class ChatMessageAccumulator {
|
|
|
5346
5643
|
this.uiMessages = [...uiMessages];
|
|
5347
5644
|
this.modelMessages = await toModelMessages(uiMessages);
|
|
5348
5645
|
}
|
|
5646
|
+
/**
|
|
5647
|
+
* Splice a `chat.headStart` handover partial into the accumulator (the warm
|
|
5648
|
+
* step-1 response). Dedups by `messageId` so a seeded/hydrated history that
|
|
5649
|
+
* already carries the partial isn't doubled. Seed any prior history first
|
|
5650
|
+
* (e.g. `setMessages(payload.headStartMessages)`). Low-level — see
|
|
5651
|
+
* `consumeHandover` for the wait+seed+apply convenience.
|
|
5652
|
+
*/
|
|
5653
|
+
applyHandover(signal) {
|
|
5654
|
+
spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
|
|
5655
|
+
}
|
|
5656
|
+
/**
|
|
5657
|
+
* One-call `chat.headStart` handover for a custom-agent loop: waits for the
|
|
5658
|
+
* handover signal, seeds prior history from `payload.headStartMessages`,
|
|
5659
|
+
* applies the warm step-1 partial, and reports what to do next.
|
|
5660
|
+
*
|
|
5661
|
+
* Returns `{ isFinal, skipped }`:
|
|
5662
|
+
* - `skipped: true` — not a `handover-prepare` run, the wait idled out, or the
|
|
5663
|
+
* warm handler aborted. Exit the run without a turn.
|
|
5664
|
+
* - `isFinal: true` — step 1 IS the response (pure text). Write turn-complete
|
|
5665
|
+
* and continue; do not call `streamText`.
|
|
5666
|
+
* - `isFinal: false` — fall through to `streamText`, which runs the pending
|
|
5667
|
+
* tool round handed over from step 1.
|
|
5668
|
+
*/
|
|
5669
|
+
async consumeHandover(options) {
|
|
5670
|
+
const signal = await waitForHandover({
|
|
5671
|
+
payload: options.payload,
|
|
5672
|
+
idleTimeoutInSeconds: options.idleTimeoutInSeconds,
|
|
5673
|
+
timeout: options.timeout,
|
|
5674
|
+
});
|
|
5675
|
+
if (!signal || signal.kind === "handover-skip") {
|
|
5676
|
+
return { isFinal: false, skipped: true };
|
|
5677
|
+
}
|
|
5678
|
+
if (options.payload.headStartMessages && options.payload.headStartMessages.length > 0) {
|
|
5679
|
+
await this.setMessages(options.payload.headStartMessages);
|
|
5680
|
+
}
|
|
5681
|
+
this.applyHandover(signal);
|
|
5682
|
+
return { isFinal: signal.isFinal, skipped: false };
|
|
5683
|
+
}
|
|
5349
5684
|
async addResponse(response) {
|
|
5350
5685
|
if (!response.id) {
|
|
5351
5686
|
response = { ...response, id: generateMessageId() };
|
|
5352
5687
|
}
|
|
5688
|
+
// Tool-approval and handover-resume continuations reuse the trailing
|
|
5689
|
+
// assistant's ID (via originalMessages on the pipe), so the captured
|
|
5690
|
+
// response can carry the same ID as a message already in the chain
|
|
5691
|
+
// (e.g. a spliced handover partial). Replace in place instead of pushing
|
|
5692
|
+
// a duplicate, mirroring the chat.agent accumulator.
|
|
5693
|
+
const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
|
|
5694
|
+
if (existingIdx !== -1) {
|
|
5695
|
+
this.uiMessages[existingIdx] = response;
|
|
5696
|
+
try {
|
|
5697
|
+
// Reconvert all model messages since we replaced rather than appended.
|
|
5698
|
+
this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
|
|
5699
|
+
}
|
|
5700
|
+
catch {
|
|
5701
|
+
// Conversion failed — leave the existing model messages in place
|
|
5702
|
+
}
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5353
5705
|
this.uiMessages.push(response);
|
|
5354
5706
|
try {
|
|
5355
5707
|
const msgs = await toModelMessages([stripProviderMetadata(response)]);
|
|
@@ -5482,14 +5834,18 @@ class ChatMessageAccumulator {
|
|
|
5482
5834
|
* signaling, and idle/suspend between turns. You control: initialization,
|
|
5483
5835
|
* model/tool selection, persistence, and any custom per-turn logic.
|
|
5484
5836
|
*
|
|
5837
|
+
* Call from inside a `chat.customAgent()` run — the wrapper binds the
|
|
5838
|
+
* backing Session that the iterator's stop signal and message channels
|
|
5839
|
+
* resolve to. (A plain `task()` does not bind it, so `createSession`
|
|
5840
|
+
* would throw "session handle is not initialized".)
|
|
5841
|
+
*
|
|
5485
5842
|
* @example
|
|
5486
5843
|
* ```ts
|
|
5487
|
-
* import { task } from "@trigger.dev/sdk";
|
|
5488
5844
|
* import { chat, type ChatTaskWirePayload } from "@trigger.dev/sdk/ai";
|
|
5489
5845
|
* import { streamText } from "ai";
|
|
5490
5846
|
* import { openai } from "@ai-sdk/openai";
|
|
5491
5847
|
*
|
|
5492
|
-
* export const myChat =
|
|
5848
|
+
* export const myChat = chat.customAgent({
|
|
5493
5849
|
* id: "my-chat",
|
|
5494
5850
|
* run: async (payload: ChatTaskWirePayload, { signal }) => {
|
|
5495
5851
|
* const session = chat.createSession(payload, { signal });
|
|
@@ -5513,25 +5869,72 @@ function createChatSession(payload, options) {
|
|
|
5513
5869
|
[Symbol.asyncIterator]() {
|
|
5514
5870
|
let currentPayload = payload;
|
|
5515
5871
|
let turn = -1;
|
|
5516
|
-
|
|
5872
|
+
// Created on the first next() call, AFTER the resume-cursor seed —
|
|
5873
|
+
// createStopSignal attaches the `.in` SSE tail, and attaching
|
|
5874
|
+
// before the seed replays every record from seq 0 (the seed is a
|
|
5875
|
+
// no-op when the chatCustomAgent wrapper already ran it).
|
|
5876
|
+
let stop;
|
|
5877
|
+
let booted = false;
|
|
5517
5878
|
const accumulator = new ChatMessageAccumulator();
|
|
5518
5879
|
let previousTurnUsage;
|
|
5519
5880
|
let cumulativeUsage = emptyUsage();
|
|
5520
5881
|
return {
|
|
5521
5882
|
async next() {
|
|
5883
|
+
if (!booted) {
|
|
5884
|
+
booted = true;
|
|
5885
|
+
await seedSessionInResumeCursorForCustomLoop(currentPayload);
|
|
5886
|
+
stop = createStopSignal();
|
|
5887
|
+
}
|
|
5522
5888
|
turn++;
|
|
5523
|
-
//
|
|
5524
|
-
|
|
5889
|
+
// Head-start handover: the server triggered this run with
|
|
5890
|
+
// `trigger: "handover-prepare"` and signals the warm step-1 partial on
|
|
5891
|
+
// `session.in`. Wait for it BEFORE any `messagesInput.waitWithIdleTimeout`
|
|
5892
|
+
// (that facade consumes-and-discards non-message chunks and would swallow
|
|
5893
|
+
// the signal). Turn-0 only — continuation boots never carry this trigger.
|
|
5894
|
+
let handoverThisTurn = null;
|
|
5895
|
+
let pendingHandoverSignal = null;
|
|
5896
|
+
if (turn === 0 && currentPayload.trigger === "handover-prepare") {
|
|
5897
|
+
const signal = await waitForHandover({
|
|
5898
|
+
payload: currentPayload,
|
|
5899
|
+
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds,
|
|
5900
|
+
timeout,
|
|
5901
|
+
});
|
|
5902
|
+
if (!signal || signal.kind === "handover-skip" || runSignal.aborted) {
|
|
5903
|
+
stop.cleanup();
|
|
5904
|
+
return { done: true, value: undefined };
|
|
5905
|
+
}
|
|
5906
|
+
pendingHandoverSignal = signal;
|
|
5907
|
+
handoverThisTurn = { isFinal: signal.isFinal };
|
|
5908
|
+
// Rewrite to a normal first-turn message turn so the rest of the loop
|
|
5909
|
+
// (steering setup, addIncoming, turnObj) runs unchanged.
|
|
5910
|
+
currentPayload = { ...currentPayload, trigger: "submit-message", message: undefined };
|
|
5911
|
+
}
|
|
5912
|
+
// First turn: wait when the boot payload carries no message.
|
|
5913
|
+
// Preload boots wait for the first real message; continuation
|
|
5914
|
+
// boots (fresh run via `ensureRunForSession` / end-and-continue)
|
|
5915
|
+
// arrive with the sticky boot-payload fields stripped, so running
|
|
5916
|
+
// a turn immediately would invoke the model with no user input.
|
|
5917
|
+
const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message;
|
|
5918
|
+
if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
|
|
5525
5919
|
const result = await messagesInput.waitWithIdleTimeout({
|
|
5526
5920
|
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
|
|
5527
5921
|
timeout,
|
|
5528
|
-
spanName:
|
|
5922
|
+
spanName: currentPayload.trigger === "preload"
|
|
5923
|
+
? "waiting for first message"
|
|
5924
|
+
: "waiting for first message (continuation)",
|
|
5529
5925
|
});
|
|
5530
5926
|
if (!result.ok || runSignal.aborted) {
|
|
5531
5927
|
stop.cleanup();
|
|
5532
5928
|
return { done: true, value: undefined };
|
|
5533
5929
|
}
|
|
5930
|
+
const continuationBoot = isMessagelessContinuationBoot;
|
|
5534
5931
|
currentPayload = result.output;
|
|
5932
|
+
// Preserve the continuation flag — the wire payload of the next
|
|
5933
|
+
// message doesn't carry it, and `turn.continuation` is how the
|
|
5934
|
+
// user knows to seed history (e.g. `turn.setMessages(stored)`).
|
|
5935
|
+
if (continuationBoot && currentPayload.continuation === undefined) {
|
|
5936
|
+
currentPayload = { ...currentPayload, continuation: true };
|
|
5937
|
+
}
|
|
5535
5938
|
}
|
|
5536
5939
|
// Subsequent turns: wait for the next message
|
|
5537
5940
|
if (turn > 0) {
|
|
@@ -5612,6 +6015,16 @@ function createChatSession(payload, options) {
|
|
|
5612
6015
|
? [currentPayload.message]
|
|
5613
6016
|
: [];
|
|
5614
6017
|
const messages = await accumulator.addIncoming(incomingForAccumulator, currentPayload.trigger, turn);
|
|
6018
|
+
// Apply the head-start handover AFTER addIncoming — turn-0 addIncoming
|
|
6019
|
+
// replaces accumulator state, which would wipe a pre-applied splice.
|
|
6020
|
+
// Seed prior history first, then splice the warm step-1 partial.
|
|
6021
|
+
if (pendingHandoverSignal) {
|
|
6022
|
+
const priorHistory = currentPayload.headStartMessages;
|
|
6023
|
+
if (priorHistory && priorHistory.length > 0) {
|
|
6024
|
+
await accumulator.setMessages(priorHistory);
|
|
6025
|
+
}
|
|
6026
|
+
accumulator.applyHandover(pendingHandoverSignal);
|
|
6027
|
+
}
|
|
5615
6028
|
// chat.requestUpgrade() called before this turn — signal transport and exit
|
|
5616
6029
|
if (locals.get(chatUpgradeRequestedKey)) {
|
|
5617
6030
|
await writeUpgradeRequiredChunk();
|
|
@@ -5638,13 +6051,38 @@ function createChatSession(payload, options) {
|
|
|
5638
6051
|
continuation: currentPayload.continuation ?? false,
|
|
5639
6052
|
previousTurnUsage,
|
|
5640
6053
|
totalUsage: cumulativeUsage,
|
|
6054
|
+
handover: handoverThisTurn,
|
|
5641
6055
|
async setMessages(uiMessages) {
|
|
5642
6056
|
await accumulator.setMessages(uiMessages);
|
|
5643
6057
|
},
|
|
5644
6058
|
async complete(source) {
|
|
6059
|
+
// Head-start final turn: the warm step-1 partial is already spliced
|
|
6060
|
+
// into the accumulator and IS the response — nothing to pipe. Only
|
|
6061
|
+
// valid on a final handover; a missing source on any other turn is a
|
|
6062
|
+
// mistake (it would silently finalize without an assistant response).
|
|
6063
|
+
if (!source) {
|
|
6064
|
+
if (!handoverThisTurn?.isFinal) {
|
|
6065
|
+
throw new Error("turn.complete() requires a stream source unless turn.handover.isFinal is true");
|
|
6066
|
+
}
|
|
6067
|
+
const response = accumulator.uiMessages.at(-1);
|
|
6068
|
+
if (!response || response.role !== "assistant") {
|
|
6069
|
+
throw new Error("turn.complete() could not find the spliced handover response");
|
|
6070
|
+
}
|
|
6071
|
+
sessionMsgSub.off();
|
|
6072
|
+
await chatWriteTurnComplete();
|
|
6073
|
+
return response;
|
|
6074
|
+
}
|
|
5645
6075
|
let response;
|
|
5646
6076
|
try {
|
|
5647
|
-
response = await pipeChatAndCapture(source, {
|
|
6077
|
+
response = await pipeChatAndCapture(source, {
|
|
6078
|
+
signal: combinedSignal,
|
|
6079
|
+
// On a non-final handover turn, thread the spliced partial so a
|
|
6080
|
+
// resumed tool round's tool-output chunks merge into the
|
|
6081
|
+
// handed-over tool-call. Gated on the handover turn only — a
|
|
6082
|
+
// normal turn must not pass originalMessages (it would merge the
|
|
6083
|
+
// fresh response into the prior assistant message).
|
|
6084
|
+
...(handoverThisTurn ? { originalMessages: accumulator.uiMessages } : {}),
|
|
6085
|
+
});
|
|
5648
6086
|
}
|
|
5649
6087
|
catch (error) {
|
|
5650
6088
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -5684,14 +6122,22 @@ function createChatSession(payload, options) {
|
|
|
5684
6122
|
locals.set(chatResponsePartsKey, []);
|
|
5685
6123
|
}
|
|
5686
6124
|
}
|
|
5687
|
-
// Capture token usage from the streamText result
|
|
6125
|
+
// Capture token usage from the streamText result. Race with a 2s
|
|
6126
|
+
// timeout — on stop-abort the AI SDK's totalUsage promise can hang
|
|
6127
|
+
// indefinitely, which would wedge the turn loop (same guard as
|
|
6128
|
+
// chat.agent's turn loop).
|
|
5688
6129
|
let turnUsage;
|
|
5689
6130
|
if (typeof source.totalUsage?.then === "function") {
|
|
5690
6131
|
try {
|
|
5691
|
-
const usage = await
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
6132
|
+
const usage = (await Promise.race([
|
|
6133
|
+
source.totalUsage,
|
|
6134
|
+
new Promise((r) => setTimeout(() => r(undefined), 2_000)),
|
|
6135
|
+
]));
|
|
6136
|
+
if (usage) {
|
|
6137
|
+
turnUsage = usage;
|
|
6138
|
+
previousTurnUsage = usage;
|
|
6139
|
+
cumulativeUsage = addUsage(cumulativeUsage, usage);
|
|
6140
|
+
}
|
|
5695
6141
|
}
|
|
5696
6142
|
catch {
|
|
5697
6143
|
/* non-fatal */
|
|
@@ -5791,7 +6237,8 @@ function createChatSession(payload, options) {
|
|
|
5791
6237
|
return { done: false, value: turnObj };
|
|
5792
6238
|
},
|
|
5793
6239
|
async return() {
|
|
5794
|
-
stop
|
|
6240
|
+
// `stop` only exists once next() has booted the iterator.
|
|
6241
|
+
stop?.cleanup();
|
|
5795
6242
|
return { done: true, value: undefined };
|
|
5796
6243
|
},
|
|
5797
6244
|
};
|
|
@@ -6034,6 +6481,7 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
6034
6481
|
// run-list filter by chat works without the customer having to wire it
|
|
6035
6482
|
// up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
|
|
6036
6483
|
const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
|
|
6484
|
+
// SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot.
|
|
6037
6485
|
const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
|
|
6038
6486
|
const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
|
|
6039
6487
|
const triggerConfig = {
|
|
@@ -6058,6 +6506,20 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
6058
6506
|
maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts,
|
|
6059
6507
|
}
|
|
6060
6508
|
: {}),
|
|
6509
|
+
...(options?.triggerConfig?.maxDuration !== undefined ||
|
|
6510
|
+
params.triggerConfig?.maxDuration !== undefined
|
|
6511
|
+
? {
|
|
6512
|
+
maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration,
|
|
6513
|
+
}
|
|
6514
|
+
: {}),
|
|
6515
|
+
...(options?.triggerConfig?.region || params.triggerConfig?.region
|
|
6516
|
+
? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region }
|
|
6517
|
+
: {}),
|
|
6518
|
+
...(options?.triggerConfig?.lockToVersion || params.triggerConfig?.lockToVersion
|
|
6519
|
+
? {
|
|
6520
|
+
lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
|
|
6521
|
+
}
|
|
6522
|
+
: {}),
|
|
6061
6523
|
...(options?.triggerConfig?.idleTimeoutInSeconds !== undefined ||
|
|
6062
6524
|
params.triggerConfig?.idleTimeoutInSeconds !== undefined
|
|
6063
6525
|
? {
|
|
@@ -6236,10 +6698,20 @@ export const chat = {
|
|
|
6236
6698
|
MessageAccumulator: ChatMessageAccumulator,
|
|
6237
6699
|
/** Create a chat session (async iterator). See {@link createChatSession}. */
|
|
6238
6700
|
createSession: createChatSession,
|
|
6701
|
+
/**
|
|
6702
|
+
* Wait for a `chat.headStart` handover signal inside a `chat.customAgent`
|
|
6703
|
+
* loop (turn 0). See {@link waitForHandover}. For most loops prefer the
|
|
6704
|
+
* `chat.MessageAccumulator.consumeHandover()` convenience, which also seeds
|
|
6705
|
+
* `payload.headStartMessages` and applies the partial.
|
|
6706
|
+
*/
|
|
6707
|
+
waitForHandover,
|
|
6239
6708
|
/**
|
|
6240
6709
|
* Store and retrieve a resolved prompt for the current run.
|
|
6241
6710
|
*
|
|
6242
6711
|
* - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
|
|
6712
|
+
* - `chat.prompt.set(resolved, { providerOptions })` — also attach provider
|
|
6713
|
+
* options to the system block so a provider can cache it (e.g. Anthropic
|
|
6714
|
+
* prompt caching). See the prompt-caching guide.
|
|
6243
6715
|
* - `chat.prompt()` — read the stored prompt (throws if not set)
|
|
6244
6716
|
*/
|
|
6245
6717
|
prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),
|
|
@@ -6332,8 +6804,19 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
6332
6804
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
6333
6805
|
// first-turn-ever (or first turn post-OOM without a snapshot seed) is
|
|
6334
6806
|
// fine — the chain catches up next turn.
|
|
6335
|
-
|
|
6336
|
-
|
|
6807
|
+
//
|
|
6808
|
+
// Lazily create the slot if a caller reached here without one (a plain
|
|
6809
|
+
// `task()` driving `chat.createSession` / `chat.writeTurnComplete`, vs.
|
|
6810
|
+
// chatAgent/chatCustomAgent which seed it at boot). The first call then
|
|
6811
|
+
// does no trim (nothing before it) and records its seq; later calls trim
|
|
6812
|
+
// — so `.out` is bounded for every writeTurnComplete caller, not just the
|
|
6813
|
+
// built-in agents.
|
|
6814
|
+
let slot = locals.get(lastTurnCompleteSeqNumKey);
|
|
6815
|
+
if (!slot) {
|
|
6816
|
+
slot = { value: undefined };
|
|
6817
|
+
locals.set(lastTurnCompleteSeqNumKey, slot);
|
|
6818
|
+
}
|
|
6819
|
+
const prev = slot.value;
|
|
6337
6820
|
if (slot && prev !== undefined) {
|
|
6338
6821
|
try {
|
|
6339
6822
|
await session.out.trimTo(prev);
|