@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/commonjs/v3/ai.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
|
|
4
|
+
exports.__findLatestSessionInCursorForTests = __findLatestSessionInCursorForTests;
|
|
4
5
|
exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
|
|
5
6
|
exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
|
|
6
7
|
exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
|
|
@@ -62,6 +63,10 @@ const chatTurnContextKey = locals_js_1.locals.create("chat.turnContext");
|
|
|
62
63
|
* @internal
|
|
63
64
|
*/
|
|
64
65
|
const chatSessionHandleKey = locals_js_1.locals.create("chat.sessionHandle");
|
|
66
|
+
// The external `chatId` from the boot payload — the value `ToolCallExecutionOptions.chatId`
|
|
67
|
+
// is documented to carry. Custom-agent loops never set per-turn context, so subtask tool
|
|
68
|
+
// metadata reads this directly rather than the Session handle id.
|
|
69
|
+
const chatExternalIdKey = locals_js_1.locals.create("chat.externalId");
|
|
65
70
|
/**
|
|
66
71
|
* S2 seq_num of the most recent `turn-complete` control record written by
|
|
67
72
|
* this worker. Read by `writeTurnCompleteChunk` to know what to trim back
|
|
@@ -81,51 +86,78 @@ const lastTurnCompleteSeqNumKey = locals_js_1.locals.create("chat.lastTurnComple
|
|
|
81
86
|
* the `.in` subscription so already-processed user messages don't get
|
|
82
87
|
* replayed from S2.
|
|
83
88
|
*
|
|
84
|
-
* Implementation
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
89
|
+
* Implementation is a non-blocking records read (`wait=0`) — the
|
|
90
|
+
* endpoint returns everything currently stored (including pre-trim
|
|
91
|
+
* records, since S2 trims are eventually consistent) in one shot, and
|
|
92
|
+
* we keep the LAST matching header. The previous SSE-based scan had to
|
|
93
|
+
* idle-wait a full 5s window to know it reached the tail, which put a
|
|
94
|
+
* constant ~6s tax on every continuation boot.
|
|
90
95
|
*
|
|
91
96
|
* Returns `undefined` if no `turn-complete` carrying the header has been
|
|
92
97
|
* written yet — first-turn-ever, first turn post-OOM-with-no-prior-runs,
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* messages.
|
|
98
|
+
* a `turn-complete` written before this header existed, or a server old
|
|
99
|
+
* enough that the records endpoint doesn't serialize headers. Callers
|
|
100
|
+
* fall back to subscribing `.in` from seq 0 in that case; the slim-wire
|
|
101
|
+
* merge handles any dedup against snapshot-restored messages.
|
|
97
102
|
* @internal
|
|
98
103
|
*/
|
|
99
104
|
async function findLatestSessionInCursor(chatId) {
|
|
100
105
|
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
106
|
+
const response = await apiClient.readSessionStreamRecords(chatId, "out");
|
|
101
107
|
let latestCursor;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (event.subtype !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
112
|
-
return;
|
|
113
|
-
const raw = (0, v3_1.headerValue)(event.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
|
|
114
|
-
if (!raw)
|
|
115
|
-
return;
|
|
116
|
-
const parsed = Number.parseInt(raw, 10);
|
|
117
|
-
if (Number.isFinite(parsed))
|
|
118
|
-
latestCursor = parsed;
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
// Drain the stream so the underlying SSE reader runs to completion. We
|
|
122
|
-
// don't accumulate chunks; `onControl` fires inline as turn-complete
|
|
123
|
-
// records arrive.
|
|
124
|
-
for await (const _ of stream) {
|
|
125
|
-
// intentionally empty
|
|
108
|
+
for (const record of response.records) {
|
|
109
|
+
if ((0, v3_1.controlSubtype)(record.headers) !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
110
|
+
continue;
|
|
111
|
+
const raw = (0, v3_1.headerValue)(record.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
|
|
112
|
+
if (!raw)
|
|
113
|
+
continue;
|
|
114
|
+
const parsed = Number.parseInt(raw, 10);
|
|
115
|
+
if (Number.isFinite(parsed))
|
|
116
|
+
latestCursor = parsed;
|
|
126
117
|
}
|
|
127
118
|
return latestCursor;
|
|
128
119
|
}
|
|
120
|
+
/** Test-only entry point for the records-based cursor scan. @internal */
|
|
121
|
+
async function __findLatestSessionInCursorForTests(chatId) {
|
|
122
|
+
return findLatestSessionInCursor(chatId);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Seed the `.in` resume cursor for custom-agent loops (`chat.customAgent`
|
|
126
|
+
* raw loops and `chat.createSession`) the way `chat.agent`'s boot does.
|
|
127
|
+
*
|
|
128
|
+
* MUST run before anything attaches a `.in` listener (`createStopSignal`,
|
|
129
|
+
* `chat.messages.on`, the first wait): attaching opens the SSE tail with
|
|
130
|
+
* `Last-Event-ID` from the seeded cursor, so attach-then-seed replays
|
|
131
|
+
* every record from seq 0 — already-answered user messages get delivered
|
|
132
|
+
* into the new run's first wait and the loop re-answers them.
|
|
133
|
+
*
|
|
134
|
+
* Seeds both cursors: `setLastSeqNum` controls the SSE `Last-Event-ID`,
|
|
135
|
+
* `setLastDispatchedSeqNum` gates waiter dispatch — seeding only the
|
|
136
|
+
* former still re-delivers records the manager buffered before the seed.
|
|
137
|
+
*
|
|
138
|
+
* No-ops on fresh boots and when a cursor is already seeded (e.g. the
|
|
139
|
+
* `chatCustomAgent` wrapper ran before a nested `createChatSession`).
|
|
140
|
+
* @internal
|
|
141
|
+
*/
|
|
142
|
+
async function seedSessionInResumeCursorForCustomLoop(payload) {
|
|
143
|
+
if (v3_1.sessionStreams.lastSeqNum(payload.chatId, "in") !== undefined)
|
|
144
|
+
return;
|
|
145
|
+
// No continuation/attempt gate: the wire may omit `continuation` on a
|
|
146
|
+
// run that still has prior turns (chat.agent covers that case via its
|
|
147
|
+
// snapshot). The scan doubles as the prior-state probe — a fresh
|
|
148
|
+
// session has no turn-complete on `.out`, returns no cursor, and
|
|
149
|
+
// seeds nothing. Cost on fresh boots is one non-blocking records read.
|
|
150
|
+
try {
|
|
151
|
+
const cursor = await findLatestSessionInCursor(payload.chatId);
|
|
152
|
+
if (cursor !== undefined) {
|
|
153
|
+
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
154
|
+
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
v3_1.logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", { error: error instanceof Error ? error.message : String(error) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
129
161
|
let readChatSnapshotImpl;
|
|
130
162
|
function __setReadChatSnapshotImplForTests(impl) {
|
|
131
163
|
readChatSnapshotImpl = impl;
|
|
@@ -677,6 +709,16 @@ function createTaskToolExecuteHandler(task) {
|
|
|
677
709
|
toolMeta.continuation = chatCtx.continuation;
|
|
678
710
|
toolMeta.clientData = chatCtx.clientData;
|
|
679
711
|
}
|
|
712
|
+
else {
|
|
713
|
+
// Hand-rolled chat.customAgent loops never set per-turn context, but
|
|
714
|
+
// the wrapper records the boot payload's external chatId at run boot
|
|
715
|
+
// — thread it so subtask chat helpers (`chat.stream.writer` with
|
|
716
|
+
// target "root") can open the parent's session.
|
|
717
|
+
const chatExternalId = locals_js_1.locals.get(chatExternalIdKey);
|
|
718
|
+
if (chatExternalId) {
|
|
719
|
+
toolMeta.chatId = chatExternalId;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
680
722
|
const chatLocals = {};
|
|
681
723
|
for (const entry of chatLocalRegistry) {
|
|
682
724
|
const value = locals_js_1.locals.get(entry.key);
|
|
@@ -993,8 +1035,15 @@ const messagesInput = {
|
|
|
993
1035
|
on(handler) {
|
|
994
1036
|
return getChatSession().in.on((chunk) => {
|
|
995
1037
|
if (chunk.kind === "message") {
|
|
996
|
-
|
|
1038
|
+
// Returning `true` marks the record CONSUMED at the manager level:
|
|
1039
|
+
// it is neither buffered for a later `once()` nor re-delivered by
|
|
1040
|
+
// the buffer drain when the next turn re-attaches its handler.
|
|
1041
|
+
// Without this, a message arriving mid-stream was delivered twice
|
|
1042
|
+
// and ran a duplicate turn.
|
|
1043
|
+
void Promise.resolve(handler(chunk.payload)).catch(() => { });
|
|
1044
|
+
return true;
|
|
997
1045
|
}
|
|
1046
|
+
return undefined;
|
|
998
1047
|
});
|
|
999
1048
|
},
|
|
1000
1049
|
once(options) {
|
|
@@ -1088,8 +1137,13 @@ const stopInput = {
|
|
|
1088
1137
|
on(handler) {
|
|
1089
1138
|
return getChatSession().in.on((chunk) => {
|
|
1090
1139
|
if (chunk.kind === "stop") {
|
|
1091
|
-
|
|
1140
|
+
// Consume stop records (see the messages facade above). A stop is
|
|
1141
|
+
// only meaningful to the turn it interrupts — buffering it would
|
|
1142
|
+
// let a stale stop abort a future turn.
|
|
1143
|
+
void Promise.resolve(handler({ stop: true, message: chunk.message })).catch(() => { });
|
|
1144
|
+
return true;
|
|
1092
1145
|
}
|
|
1146
|
+
return undefined;
|
|
1093
1147
|
});
|
|
1094
1148
|
},
|
|
1095
1149
|
once(options) {
|
|
@@ -1197,6 +1251,36 @@ const handoverInput = {
|
|
|
1197
1251
|
}
|
|
1198
1252
|
},
|
|
1199
1253
|
};
|
|
1254
|
+
/**
|
|
1255
|
+
* Wait for a `chat.headStart` handover signal inside a custom-agent loop or
|
|
1256
|
+
* `chat.createSession`. Returns:
|
|
1257
|
+
* - `null` — this run is not a `handover-prepare` boot, or the wait idled out /
|
|
1258
|
+
* the warm handler crashed before signaling. Treat as "no handover".
|
|
1259
|
+
* - `{ kind: "handover-skip" }` — the warm handler aborted; exit without a turn.
|
|
1260
|
+
* - `{ kind: "handover", partialAssistantMessage, messageId?, isFinal }` — splice
|
|
1261
|
+
* the partial (`chat.MessageAccumulator.applyHandover`) and, when `isFinal` is
|
|
1262
|
+
* false, fall through to `streamText` to run the handed-over tool round.
|
|
1263
|
+
*
|
|
1264
|
+
* For the common case prefer `accumulator.consumeHandover()`, which also seeds
|
|
1265
|
+
* `payload.headStartMessages` and applies the partial for you.
|
|
1266
|
+
*
|
|
1267
|
+
* Must be called at turn 0 before any `chat.messages.waitWithIdleTimeout` —
|
|
1268
|
+
* that facade consumes and discards non-message chunks, which would swallow the
|
|
1269
|
+
* handover signal.
|
|
1270
|
+
*/
|
|
1271
|
+
async function waitForHandover(options) {
|
|
1272
|
+
if (options.payload.trigger !== "handover-prepare")
|
|
1273
|
+
return null;
|
|
1274
|
+
const result = await handoverInput.waitWithIdleTimeout({
|
|
1275
|
+
idleTimeoutInSeconds: options.idleTimeoutInSeconds ?? options.payload.idleTimeoutInSeconds ?? 60,
|
|
1276
|
+
timeout: options.timeout,
|
|
1277
|
+
spanName: options.spanName ?? "waiting for handover signal",
|
|
1278
|
+
});
|
|
1279
|
+
// Non-ok = idle timeout or the warm handler crashed without signaling.
|
|
1280
|
+
if (!result.ok)
|
|
1281
|
+
return null;
|
|
1282
|
+
return result.output;
|
|
1283
|
+
}
|
|
1200
1284
|
/**
|
|
1201
1285
|
* Per-turn deferred promises. Registered via `chat.defer()`, awaited
|
|
1202
1286
|
* before `onTurnComplete` fires. Reset each turn.
|
|
@@ -1243,6 +1327,9 @@ const chatHandoverIsFinalKey = locals_js_1.locals.create("chat.handoverIsFinal")
|
|
|
1243
1327
|
* `tool-approval-response` rows are AI-SDK-internal and don't need a
|
|
1244
1328
|
* UIMessage representation. We map:
|
|
1245
1329
|
* - `text` parts → `{ type: "text", text }`
|
|
1330
|
+
* - `reasoning` parts → `{ type: "reasoning", text, state: "done" }`
|
|
1331
|
+
* (provider metadata carried so an Anthropic thinking signature
|
|
1332
|
+
* survives a UIMessage → ModelMessage round trip)
|
|
1246
1333
|
* - `tool-call` parts → `{ type: "tool-${name}", toolCallId,
|
|
1247
1334
|
* state: "input-available", input }`
|
|
1248
1335
|
* - `tool-approval-request` parts → skipped (AI SDK derives the
|
|
@@ -1259,6 +1346,14 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1259
1346
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1260
1347
|
parts.push({ type: "text", text: part.text });
|
|
1261
1348
|
}
|
|
1349
|
+
else if (part.type === "reasoning" && typeof part.text === "string") {
|
|
1350
|
+
parts.push({
|
|
1351
|
+
type: "reasoning",
|
|
1352
|
+
text: part.text,
|
|
1353
|
+
state: "done",
|
|
1354
|
+
...(part.providerOptions ? { providerMetadata: part.providerOptions } : {}),
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1262
1357
|
else if (part.type === "tool-call" && part.toolCallId && part.toolName) {
|
|
1263
1358
|
parts.push({
|
|
1264
1359
|
type: `tool-${part.toolName}`,
|
|
@@ -1282,6 +1377,27 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1282
1377
|
parts,
|
|
1283
1378
|
};
|
|
1284
1379
|
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Splice a head-start handover partial into an accumulating message pair
|
|
1382
|
+
* (model + UI). Dedups by `messageId` against the UI chain (so a hydrated
|
|
1383
|
+
* history that already persisted the partial isn't doubled), then pushes the
|
|
1384
|
+
* partial into `modelMessages` and the synthesized UIMessage into `uiMessages`.
|
|
1385
|
+
* Shared by the `chat.agent` turn-0 splice and `ChatMessageAccumulator.applyHandover`.
|
|
1386
|
+
* @internal
|
|
1387
|
+
*/
|
|
1388
|
+
function spliceHandoverPartial(modelMessages, uiMessages, signal) {
|
|
1389
|
+
if (!signal.partialAssistantMessage || signal.partialAssistantMessage.length === 0) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
// Skip if the hydrated chain already persisted the partial under this id.
|
|
1393
|
+
const alreadyInChain = signal.messageId !== undefined && uiMessages.some((m) => m.id === signal.messageId);
|
|
1394
|
+
if (alreadyInChain)
|
|
1395
|
+
return;
|
|
1396
|
+
modelMessages.push(...signal.partialAssistantMessage);
|
|
1397
|
+
const partialUI = synthesizeHandoverUIMessage(signal.partialAssistantMessage, signal.messageId);
|
|
1398
|
+
if (partialUI)
|
|
1399
|
+
uiMessages.push(partialUI);
|
|
1400
|
+
}
|
|
1285
1401
|
/**
|
|
1286
1402
|
* Per-turn background context queue. Messages added via `chat.backgroundWork.inject()`
|
|
1287
1403
|
* are drained at the next `prepareStep` boundary and appended to the model messages.
|
|
@@ -2236,11 +2352,18 @@ function isCompactionSafe(messages) {
|
|
|
2236
2352
|
}
|
|
2237
2353
|
/** @internal */
|
|
2238
2354
|
const chatPromptKey = locals_js_1.locals.create("chat.prompt");
|
|
2355
|
+
/**
|
|
2356
|
+
* @internal Provider options attached to the system message that
|
|
2357
|
+
* `toStreamTextOptions()` builds from the stored prompt — lets a provider cache
|
|
2358
|
+
* the system block. Stored separately so it works for both the `ResolvedPrompt`
|
|
2359
|
+
* and plain-string forms without mutating the prompt object.
|
|
2360
|
+
*/
|
|
2361
|
+
const chatPromptProviderOptionsKey = locals_js_1.locals.create("chat.prompt.providerOptions");
|
|
2239
2362
|
/**
|
|
2240
2363
|
* Store a resolved prompt (or plain string) for the current run.
|
|
2241
2364
|
* Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
|
|
2242
2365
|
*/
|
|
2243
|
-
function setChatPrompt(resolved) {
|
|
2366
|
+
function setChatPrompt(resolved, options) {
|
|
2244
2367
|
if (typeof resolved === "string") {
|
|
2245
2368
|
locals_js_1.locals.set(chatPromptKey, {
|
|
2246
2369
|
text: resolved,
|
|
@@ -2257,6 +2380,9 @@ function setChatPrompt(resolved) {
|
|
|
2257
2380
|
else {
|
|
2258
2381
|
locals_js_1.locals.set(chatPromptKey, resolved);
|
|
2259
2382
|
}
|
|
2383
|
+
// Always overwrite the slot (even with undefined) so a later prompt.set with
|
|
2384
|
+
// no options clears a previous prompt's cache opt-in rather than leaking it.
|
|
2385
|
+
locals_js_1.locals.set(chatPromptProviderOptionsKey, options?.providerOptions);
|
|
2260
2386
|
}
|
|
2261
2387
|
/**
|
|
2262
2388
|
* Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
|
|
@@ -2422,7 +2548,21 @@ function toStreamTextOptions(options) {
|
|
|
2422
2548
|
const promptText = prompt?.text ?? "";
|
|
2423
2549
|
const skillsText = skills && skills.length > 0 ? buildSkillsSystemPrompt(skills) : "";
|
|
2424
2550
|
if (promptText || skillsText) {
|
|
2425
|
-
|
|
2551
|
+
const systemText = [promptText, skillsText].filter(Boolean).join("\n\n");
|
|
2552
|
+
// Resolve system-prompt provider options for caching. Precedence (most
|
|
2553
|
+
// specific wins, no deep merge): explicit `systemProviderOptions` →
|
|
2554
|
+
// `cacheControl` sugar → `providerOptions` stored on `chat.prompt.set()`.
|
|
2555
|
+
const systemProviderOptions = options?.systemProviderOptions ??
|
|
2556
|
+
(options?.cacheControl
|
|
2557
|
+
? { anthropic: { cacheControl: options.cacheControl } }
|
|
2558
|
+
: undefined) ??
|
|
2559
|
+
locals_js_1.locals.get(chatPromptProviderOptionsKey);
|
|
2560
|
+
// A bare string stays a bare string (the unchanged default). With provider
|
|
2561
|
+
// options, emit a structured `SystemModelMessage` so the provider can cache
|
|
2562
|
+
// the system block — `streamText`'s `system` accepts string | message.
|
|
2563
|
+
result.system = systemProviderOptions
|
|
2564
|
+
? { role: "system", content: systemText, providerOptions: systemProviderOptions }
|
|
2565
|
+
: systemText;
|
|
2426
2566
|
}
|
|
2427
2567
|
// Prompt-related options (only if chat.prompt.set() was called)
|
|
2428
2568
|
if (prompt) {
|
|
@@ -2604,10 +2744,20 @@ function chatCustomAgent(options) {
|
|
|
2604
2744
|
// `chat.createStartSessionAction`) before this run is triggered.
|
|
2605
2745
|
// No client-side upsert needed.
|
|
2606
2746
|
locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
|
|
2747
|
+
locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
|
|
2607
2748
|
locals_js_1.locals.set(chatAgentRunContextKey, runOptions.ctx);
|
|
2749
|
+
// Initialize the turn-complete trim slot so `chat.writeTurnComplete`
|
|
2750
|
+
// trims `session.out` back to the previous turn boundary. Without
|
|
2751
|
+
// this the slot is undefined and the trim never runs, so `.out`
|
|
2752
|
+
// grows without bound for the whole custom-agent surface.
|
|
2753
|
+
locals_js_1.locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
|
|
2608
2754
|
(0, streams_js_1.markChatAgentRunForStreamsWarning)();
|
|
2609
2755
|
v3_1.taskContext.setConversationId(payload.chatId);
|
|
2610
2756
|
stampConversationIdOnActiveSpan(payload.chatId);
|
|
2757
|
+
// Seed the `.in` resume cursor before user code attaches any `.in`
|
|
2758
|
+
// listener — otherwise a continuation boot replays already-answered
|
|
2759
|
+
// messages into the loop's first wait.
|
|
2760
|
+
await seedSessionInResumeCursorForCustomLoop(payload);
|
|
2611
2761
|
return userRun(payload, runOptions);
|
|
2612
2762
|
},
|
|
2613
2763
|
});
|
|
@@ -2654,6 +2804,7 @@ function chatAgent(options) {
|
|
|
2654
2804
|
// `chat.createStartSessionAction` or browser-direct) before this
|
|
2655
2805
|
// run is triggered — no client-side upsert needed here.
|
|
2656
2806
|
locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
|
|
2807
|
+
locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
|
|
2657
2808
|
// Mutable holder; advances in `writeTurnCompleteChunk` after each turn
|
|
2658
2809
|
// and is the trim target for the NEXT turn's trim record.
|
|
2659
2810
|
locals_js_1.locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
|
|
@@ -2730,6 +2881,11 @@ function chatAgent(options) {
|
|
|
2730
2881
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
2731
2882
|
const bootInjectedQueue = [];
|
|
2732
2883
|
const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
|
|
2884
|
+
// `.in` resume cursor, computed at most once per boot. The boot
|
|
2885
|
+
// block below resolves it (snapshot field or records scan) and the
|
|
2886
|
+
// resume-cursor block reuses it instead of re-scanning.
|
|
2887
|
+
let bootInCursor;
|
|
2888
|
+
let bootInCursorResolved = false;
|
|
2733
2889
|
if (!hydrateMessages && couldHavePriorState) {
|
|
2734
2890
|
// Single parent span for the whole boot read phase — snapshot
|
|
2735
2891
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
@@ -2765,23 +2921,28 @@ function chatAgent(options) {
|
|
|
2765
2921
|
slot.value = seeded;
|
|
2766
2922
|
}
|
|
2767
2923
|
}
|
|
2768
|
-
//
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2924
|
+
// The `.out` replay and the `.in` cursor + tail read are
|
|
2925
|
+
// independent (both depend only on the snapshot) — run them
|
|
2926
|
+
// concurrently. Each phase keeps its own catch + duration
|
|
2927
|
+
// attribute.
|
|
2928
|
+
const replayOutPhase = async () => {
|
|
2929
|
+
const replayOutStart = Date.now();
|
|
2930
|
+
try {
|
|
2931
|
+
const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
|
|
2932
|
+
replayedSettled = replayResult.settled;
|
|
2933
|
+
replayedPartial = replayResult.partial;
|
|
2934
|
+
replayedPartialRaw = replayResult.partialRaw;
|
|
2935
|
+
}
|
|
2936
|
+
catch (error) {
|
|
2937
|
+
v3_1.logger.warn("chat.agent: session.out replay failed; using snapshot only", {
|
|
2938
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2939
|
+
sessionId: sessionIdForSnapshot,
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart);
|
|
2943
|
+
bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length);
|
|
2944
|
+
bootSpan.setAttribute("chat.boot.replay.out.partialPresent", replayedPartial !== undefined);
|
|
2945
|
+
};
|
|
2785
2946
|
// session.in tail read
|
|
2786
2947
|
//
|
|
2787
2948
|
// session.in carries the user-side of the conversation
|
|
@@ -2792,20 +2953,43 @@ function chatAgent(options) {
|
|
|
2792
2953
|
// visible via the live SSE subscription — by which point they
|
|
2793
2954
|
// would arrive AFTER the partial-assistant orphan and look like
|
|
2794
2955
|
// brand-new turns to the model, producing inverted chains.
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2956
|
+
//
|
|
2957
|
+
// The cursor comes from the snapshot when present (written
|
|
2958
|
+
// there since `lastInEventId` was added) — otherwise from a
|
|
2959
|
+
// records scan of `.out`'s latest turn-complete header.
|
|
2960
|
+
const replayInPhase = async () => {
|
|
2961
|
+
const replayInStart = Date.now();
|
|
2962
|
+
const snapshotInCursor = bootSnapshot?.lastInEventId !== undefined
|
|
2963
|
+
? Number.parseInt(bootSnapshot.lastInEventId, 10)
|
|
2964
|
+
: undefined;
|
|
2965
|
+
if (snapshotInCursor !== undefined && Number.isFinite(snapshotInCursor)) {
|
|
2966
|
+
bootInCursor = snapshotInCursor;
|
|
2967
|
+
bootInCursorResolved = true;
|
|
2968
|
+
}
|
|
2969
|
+
else {
|
|
2970
|
+
try {
|
|
2971
|
+
bootInCursor = await findLatestSessionInCursor(payload.chatId);
|
|
2972
|
+
bootInCursorResolved = true;
|
|
2973
|
+
}
|
|
2974
|
+
catch {
|
|
2975
|
+
// Transient scan failure: leave unresolved so the
|
|
2976
|
+
// resume-cursor block below retries the lookup.
|
|
2977
|
+
bootInCursor = undefined;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
bootSpan.setAttribute("chat.boot.replay.in.cursorFromSnapshot", snapshotInCursor !== undefined);
|
|
2981
|
+
try {
|
|
2982
|
+
replayedInTail = await replaySessionInTail(payload.chatId, {
|
|
2983
|
+
lastEventId: bootInCursor !== undefined ? String(bootInCursor) : undefined,
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
catch (error) {
|
|
2987
|
+
v3_1.logger.warn("chat.agent: session.in replay failed; in-flight users may not be recovered", { error: error instanceof Error ? error.message : String(error) });
|
|
2988
|
+
}
|
|
2989
|
+
bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart);
|
|
2990
|
+
bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length);
|
|
2991
|
+
};
|
|
2992
|
+
await Promise.all([replayOutPhase(), replayInPhase()]);
|
|
2809
2993
|
}, {
|
|
2810
2994
|
attributes: {
|
|
2811
2995
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-rotate-clockwise",
|
|
@@ -2846,7 +3030,12 @@ function chatAgent(options) {
|
|
|
2846
3030
|
bootSnapshot !== undefined;
|
|
2847
3031
|
if (needsResumeCursor) {
|
|
2848
3032
|
try {
|
|
2849
|
-
|
|
3033
|
+
// Reuse the cursor the boot block already resolved (snapshot
|
|
3034
|
+
// field or records scan) — only scan here when the boot block
|
|
3035
|
+
// was skipped (hydrateMessages, or snapshot-only signals).
|
|
3036
|
+
const cursor = bootInCursorResolved
|
|
3037
|
+
? bootInCursor
|
|
3038
|
+
: await findLatestSessionInCursor(payload.chatId);
|
|
2850
3039
|
if (cursor !== undefined) {
|
|
2851
3040
|
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
2852
3041
|
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
@@ -3610,6 +3799,18 @@ function chatAgent(options) {
|
|
|
3610
3799
|
// therefore a delta merge, not a full-history reset.
|
|
3611
3800
|
if (currentWirePayload.trigger !== "action") {
|
|
3612
3801
|
let cleanedUIMessages = cleanedIncomingMessages;
|
|
3802
|
+
// Turn-0 head-start with hydrateMessages: the boot seeding from
|
|
3803
|
+
// `payload.headStartMessages` is non-hydrate-only, so ship the
|
|
3804
|
+
// route handler's first-turn history to the hydrate hook as
|
|
3805
|
+
// incoming messages instead (gated on the pending handover).
|
|
3806
|
+
if (turn === 0 &&
|
|
3807
|
+
hydrateMessages &&
|
|
3808
|
+
cleanedUIMessages.length === 0 &&
|
|
3809
|
+
(locals_js_1.locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
|
|
3810
|
+
Array.isArray(payload.headStartMessages) &&
|
|
3811
|
+
payload.headStartMessages.length > 0) {
|
|
3812
|
+
cleanedUIMessages = payload.headStartMessages;
|
|
3813
|
+
}
|
|
3613
3814
|
// Validate/transform UIMessages before conversion — catches malformed
|
|
3614
3815
|
// messages from storage or untrusted input before they reach the model.
|
|
3615
3816
|
// Slim wire: triggers like `regenerate-message` carry no incoming
|
|
@@ -3788,40 +3989,39 @@ function chatAgent(options) {
|
|
|
3788
3989
|
// `preload` / `close` / `handover-prepare` and submits
|
|
3789
3990
|
// with no incoming message fall through with the boot-
|
|
3790
3991
|
// seeded accumulator unchanged.
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
}
|
|
3992
|
+
}
|
|
3993
|
+
if (turn === 0) {
|
|
3994
|
+
// Head-start handover splice (turn 0 only, BOTH
|
|
3995
|
+
// accumulation branches — hydrate and default): the
|
|
3996
|
+
// `chat.handover` route handler signalled a mid-turn
|
|
3997
|
+
// handover, so splice its partial assistant response
|
|
3998
|
+
// (text + pending tool-calls + the synthesized
|
|
3999
|
+
// tool-approval round) onto the accumulator.
|
|
4000
|
+
// `streamText` then hits AI SDK's initial-tool-
|
|
4001
|
+
// execution branch, runs the agent-side tool executes,
|
|
4002
|
+
// and resumes from step 2 — skipping the first model
|
|
4003
|
+
// call (already done by the handler).
|
|
4004
|
+
//
|
|
4005
|
+
// We also synthesize a UIMessage form of the partial
|
|
4006
|
+
// assistant and push it to `accumulatedUIMessages` so
|
|
4007
|
+
// AI SDK's `processUIMessageStream` (invoked when the
|
|
4008
|
+
// run loop calls `runResult.toUIMessageStream({
|
|
4009
|
+
// onFinish })`) can initialize `state.message` from
|
|
4010
|
+
// the trailing assistant in `originalMessages`. Without
|
|
4011
|
+
// that, the `tool-output-available` chunks emitted by
|
|
4012
|
+
// the initial-tool-execution branch can't find their
|
|
4013
|
+
// matching tool-call in state and AI SDK throws
|
|
4014
|
+
// `UIMessageStreamError: No tool invocation found`.
|
|
4015
|
+
const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
|
|
4016
|
+
if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
|
|
4017
|
+
spliceHandoverPartial(accumulatedMessages, accumulatedUIMessages, {
|
|
4018
|
+
partialAssistantMessage: pendingHandoverPartial,
|
|
4019
|
+
messageId: locals_js_1.locals.get(chatHandoverMessageIdKey),
|
|
4020
|
+
});
|
|
4021
|
+
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
3822
4022
|
}
|
|
3823
|
-
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3824
4023
|
}
|
|
4024
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3825
4025
|
} // end if (trigger !== "action")
|
|
3826
4026
|
// ── Action result handling ──────────────────────────────
|
|
3827
4027
|
// For action turns, skip the turn machinery entirely.
|
|
@@ -4518,11 +4718,15 @@ function chatAgent(options) {
|
|
|
4518
4718
|
if (!hydrateMessages) {
|
|
4519
4719
|
try {
|
|
4520
4720
|
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
4721
|
+
const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4521
4722
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4522
4723
|
version: 1,
|
|
4523
4724
|
savedAt: Date.now(),
|
|
4524
4725
|
messages: accumulatedUIMessages,
|
|
4525
4726
|
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
4727
|
+
lastInEventId: snapshotInCursor !== undefined
|
|
4728
|
+
? String(snapshotInCursor)
|
|
4729
|
+
: undefined,
|
|
4526
4730
|
});
|
|
4527
4731
|
}, {
|
|
4528
4732
|
attributes: {
|
|
@@ -4659,17 +4863,100 @@ function chatAgent(options) {
|
|
|
4659
4863
|
if (turnError instanceof v3_1.OutOfMemoryError) {
|
|
4660
4864
|
throw turnError;
|
|
4661
4865
|
}
|
|
4866
|
+
let errorTurnCompleteResult;
|
|
4662
4867
|
try {
|
|
4663
4868
|
await withChatWriter(async (writer) => {
|
|
4664
4869
|
const errorText = turnError instanceof Error ? turnError.message : "An unexpected error occurred";
|
|
4665
4870
|
writer.write({ type: "error", errorText });
|
|
4666
4871
|
});
|
|
4667
4872
|
// Signal turn complete so the client knows this turn is done
|
|
4668
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4873
|
+
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4669
4874
|
}
|
|
4670
4875
|
catch {
|
|
4671
4876
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
4672
4877
|
}
|
|
4878
|
+
// The submit-message merge into the accumulator may not have run
|
|
4879
|
+
// yet (a pre-run hook threw), so fold the wire message in for the
|
|
4880
|
+
// error event + snapshot — the cursor has already advanced past it,
|
|
4881
|
+
// so otherwise it survives in neither the snapshot nor the `.in` tail.
|
|
4882
|
+
const erroredWireMessage = currentWirePayload.message;
|
|
4883
|
+
const erroredUIMessages = erroredWireMessage &&
|
|
4884
|
+
!accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
|
|
4885
|
+
? [...accumulatedUIMessages, erroredWireMessage]
|
|
4886
|
+
: accumulatedUIMessages;
|
|
4887
|
+
// Fire onTurnComplete on the error path too — the docs promise it
|
|
4888
|
+
// runs "after every turn, successful or errored" so customers can
|
|
4889
|
+
// mark the turn failed. `responseMessage` is undefined/partial and
|
|
4890
|
+
// `error` carries the thrown value.
|
|
4891
|
+
if (onTurnComplete) {
|
|
4892
|
+
try {
|
|
4893
|
+
await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
|
|
4894
|
+
await onTurnComplete({
|
|
4895
|
+
ctx,
|
|
4896
|
+
chatId: currentWirePayload.chatId,
|
|
4897
|
+
messages: accumulatedMessages,
|
|
4898
|
+
uiMessages: erroredUIMessages,
|
|
4899
|
+
newMessages: [],
|
|
4900
|
+
newUIMessages: erroredWireMessage ? [erroredWireMessage] : [],
|
|
4901
|
+
responseMessage: undefined,
|
|
4902
|
+
rawResponseMessage: undefined,
|
|
4903
|
+
turn,
|
|
4904
|
+
runId: ctx.run.id,
|
|
4905
|
+
chatAccessToken: "",
|
|
4906
|
+
// Parsed `clientData` isn't reliably in scope here (parsing
|
|
4907
|
+
// may itself be the failure), and the raw metadata is the
|
|
4908
|
+
// wrong shape — leave it undefined on the error path.
|
|
4909
|
+
clientData: undefined,
|
|
4910
|
+
stopped: false,
|
|
4911
|
+
continuation,
|
|
4912
|
+
previousRunId,
|
|
4913
|
+
preloaded,
|
|
4914
|
+
totalUsage: cumulativeUsage,
|
|
4915
|
+
finishReason: "error",
|
|
4916
|
+
error: turnError,
|
|
4917
|
+
lastEventId: errorTurnCompleteResult?.lastEventId,
|
|
4918
|
+
});
|
|
4919
|
+
}, {
|
|
4920
|
+
attributes: {
|
|
4921
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
4922
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
4923
|
+
"chat.id": currentWirePayload.chatId,
|
|
4924
|
+
"chat.turn": turn + 1,
|
|
4925
|
+
"chat.errored": true,
|
|
4926
|
+
},
|
|
4927
|
+
});
|
|
4928
|
+
}
|
|
4929
|
+
catch {
|
|
4930
|
+
// A throwing onTurnComplete on the error path must not crash
|
|
4931
|
+
// the run — keep the conversation alive for the next message.
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
// Persist a snapshot so the failed turn's user message isn't
|
|
4935
|
+
// stranded. `writeTurnCompleteChunk` already advanced the `.in`
|
|
4936
|
+
// cursor past it (via the session-in-event-id header), and the
|
|
4937
|
+
// success-path snapshot write is skipped on error — without this
|
|
4938
|
+
// the next boot would resume past a message that exists in
|
|
4939
|
+
// neither the snapshot nor the replayable `.in` tail.
|
|
4940
|
+
if (!hydrateMessages) {
|
|
4941
|
+
try {
|
|
4942
|
+
const errorSnapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4943
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4944
|
+
version: 1,
|
|
4945
|
+
savedAt: Date.now(),
|
|
4946
|
+
messages: erroredUIMessages,
|
|
4947
|
+
lastOutEventId: errorTurnCompleteResult?.lastEventId,
|
|
4948
|
+
lastInEventId: errorSnapshotInCursor !== undefined
|
|
4949
|
+
? String(errorSnapshotInCursor)
|
|
4950
|
+
: undefined,
|
|
4951
|
+
});
|
|
4952
|
+
}
|
|
4953
|
+
catch (error) {
|
|
4954
|
+
v3_1.logger.warn("chat.agent: error-path snapshot write failed", {
|
|
4955
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4956
|
+
sessionId: sessionIdForSnapshot,
|
|
4957
|
+
});
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4673
4960
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
4674
4961
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) ||
|
|
4675
4962
|
locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
@@ -5290,8 +5577,19 @@ async function pipeChatAndCapture(source, options) {
|
|
|
5290
5577
|
const onFinishPromise = new Promise((r) => {
|
|
5291
5578
|
resolveOnFinish = r;
|
|
5292
5579
|
});
|
|
5580
|
+
const resolvedOptions = resolveUIMessageStreamOptions();
|
|
5293
5581
|
const uiStream = source.toUIMessageStream({
|
|
5294
|
-
...
|
|
5582
|
+
...resolvedOptions,
|
|
5583
|
+
// Thread the prior chain (incl. a spliced handover partial) so a resumed
|
|
5584
|
+
// tool round's tool-output chunks merge into the originating tool-call
|
|
5585
|
+
// instead of throwing "No tool invocation found".
|
|
5586
|
+
...(options?.originalMessages ? { originalMessages: options.originalMessages } : {}),
|
|
5587
|
+
// Stamp a server-generated id on the start chunk, same as chat.agent's
|
|
5588
|
+
// pipe. Without it the AI SDK regenerates the assistant id when a
|
|
5589
|
+
// prepareStep injection (steering) starts a new step mid-stream, and
|
|
5590
|
+
// the frontend replaces the partial message — wiping the
|
|
5591
|
+
// pre-injection text from the UI and the captured response.
|
|
5592
|
+
generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
|
|
5295
5593
|
onFinish: ({ responseMessage }) => {
|
|
5296
5594
|
captured = responseMessage;
|
|
5297
5595
|
resolveOnFinish();
|
|
@@ -5361,10 +5659,65 @@ class ChatMessageAccumulator {
|
|
|
5361
5659
|
this.uiMessages = [...uiMessages];
|
|
5362
5660
|
this.modelMessages = await toModelMessages(uiMessages);
|
|
5363
5661
|
}
|
|
5662
|
+
/**
|
|
5663
|
+
* Splice a `chat.headStart` handover partial into the accumulator (the warm
|
|
5664
|
+
* step-1 response). Dedups by `messageId` so a seeded/hydrated history that
|
|
5665
|
+
* already carries the partial isn't doubled. Seed any prior history first
|
|
5666
|
+
* (e.g. `setMessages(payload.headStartMessages)`). Low-level — see
|
|
5667
|
+
* `consumeHandover` for the wait+seed+apply convenience.
|
|
5668
|
+
*/
|
|
5669
|
+
applyHandover(signal) {
|
|
5670
|
+
spliceHandoverPartial(this.modelMessages, this.uiMessages, signal);
|
|
5671
|
+
}
|
|
5672
|
+
/**
|
|
5673
|
+
* One-call `chat.headStart` handover for a custom-agent loop: waits for the
|
|
5674
|
+
* handover signal, seeds prior history from `payload.headStartMessages`,
|
|
5675
|
+
* applies the warm step-1 partial, and reports what to do next.
|
|
5676
|
+
*
|
|
5677
|
+
* Returns `{ isFinal, skipped }`:
|
|
5678
|
+
* - `skipped: true` — not a `handover-prepare` run, the wait idled out, or the
|
|
5679
|
+
* warm handler aborted. Exit the run without a turn.
|
|
5680
|
+
* - `isFinal: true` — step 1 IS the response (pure text). Write turn-complete
|
|
5681
|
+
* and continue; do not call `streamText`.
|
|
5682
|
+
* - `isFinal: false` — fall through to `streamText`, which runs the pending
|
|
5683
|
+
* tool round handed over from step 1.
|
|
5684
|
+
*/
|
|
5685
|
+
async consumeHandover(options) {
|
|
5686
|
+
const signal = await waitForHandover({
|
|
5687
|
+
payload: options.payload,
|
|
5688
|
+
idleTimeoutInSeconds: options.idleTimeoutInSeconds,
|
|
5689
|
+
timeout: options.timeout,
|
|
5690
|
+
});
|
|
5691
|
+
if (!signal || signal.kind === "handover-skip") {
|
|
5692
|
+
return { isFinal: false, skipped: true };
|
|
5693
|
+
}
|
|
5694
|
+
if (options.payload.headStartMessages && options.payload.headStartMessages.length > 0) {
|
|
5695
|
+
await this.setMessages(options.payload.headStartMessages);
|
|
5696
|
+
}
|
|
5697
|
+
this.applyHandover(signal);
|
|
5698
|
+
return { isFinal: signal.isFinal, skipped: false };
|
|
5699
|
+
}
|
|
5364
5700
|
async addResponse(response) {
|
|
5365
5701
|
if (!response.id) {
|
|
5366
5702
|
response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
|
|
5367
5703
|
}
|
|
5704
|
+
// Tool-approval and handover-resume continuations reuse the trailing
|
|
5705
|
+
// assistant's ID (via originalMessages on the pipe), so the captured
|
|
5706
|
+
// response can carry the same ID as a message already in the chain
|
|
5707
|
+
// (e.g. a spliced handover partial). Replace in place instead of pushing
|
|
5708
|
+
// a duplicate, mirroring the chat.agent accumulator.
|
|
5709
|
+
const existingIdx = this.uiMessages.findIndex((m) => m.id === response.id);
|
|
5710
|
+
if (existingIdx !== -1) {
|
|
5711
|
+
this.uiMessages[existingIdx] = response;
|
|
5712
|
+
try {
|
|
5713
|
+
// Reconvert all model messages since we replaced rather than appended.
|
|
5714
|
+
this.modelMessages = await toModelMessages(this.uiMessages.map((m) => stripProviderMetadata(m)));
|
|
5715
|
+
}
|
|
5716
|
+
catch {
|
|
5717
|
+
// Conversion failed — leave the existing model messages in place
|
|
5718
|
+
}
|
|
5719
|
+
return;
|
|
5720
|
+
}
|
|
5368
5721
|
this.uiMessages.push(response);
|
|
5369
5722
|
try {
|
|
5370
5723
|
const msgs = await toModelMessages([stripProviderMetadata(response)]);
|
|
@@ -5497,14 +5850,18 @@ class ChatMessageAccumulator {
|
|
|
5497
5850
|
* signaling, and idle/suspend between turns. You control: initialization,
|
|
5498
5851
|
* model/tool selection, persistence, and any custom per-turn logic.
|
|
5499
5852
|
*
|
|
5853
|
+
* Call from inside a `chat.customAgent()` run — the wrapper binds the
|
|
5854
|
+
* backing Session that the iterator's stop signal and message channels
|
|
5855
|
+
* resolve to. (A plain `task()` does not bind it, so `createSession`
|
|
5856
|
+
* would throw "session handle is not initialized".)
|
|
5857
|
+
*
|
|
5500
5858
|
* @example
|
|
5501
5859
|
* ```ts
|
|
5502
|
-
* import { task } from "@trigger.dev/sdk";
|
|
5503
5860
|
* import { chat, type ChatTaskWirePayload } from "@trigger.dev/sdk/ai";
|
|
5504
5861
|
* import { streamText } from "ai";
|
|
5505
5862
|
* import { openai } from "@ai-sdk/openai";
|
|
5506
5863
|
*
|
|
5507
|
-
* export const myChat =
|
|
5864
|
+
* export const myChat = chat.customAgent({
|
|
5508
5865
|
* id: "my-chat",
|
|
5509
5866
|
* run: async (payload: ChatTaskWirePayload, { signal }) => {
|
|
5510
5867
|
* const session = chat.createSession(payload, { signal });
|
|
@@ -5528,25 +5885,72 @@ function createChatSession(payload, options) {
|
|
|
5528
5885
|
[Symbol.asyncIterator]() {
|
|
5529
5886
|
let currentPayload = payload;
|
|
5530
5887
|
let turn = -1;
|
|
5531
|
-
|
|
5888
|
+
// Created on the first next() call, AFTER the resume-cursor seed —
|
|
5889
|
+
// createStopSignal attaches the `.in` SSE tail, and attaching
|
|
5890
|
+
// before the seed replays every record from seq 0 (the seed is a
|
|
5891
|
+
// no-op when the chatCustomAgent wrapper already ran it).
|
|
5892
|
+
let stop;
|
|
5893
|
+
let booted = false;
|
|
5532
5894
|
const accumulator = new ChatMessageAccumulator();
|
|
5533
5895
|
let previousTurnUsage;
|
|
5534
5896
|
let cumulativeUsage = emptyUsage();
|
|
5535
5897
|
return {
|
|
5536
5898
|
async next() {
|
|
5899
|
+
if (!booted) {
|
|
5900
|
+
booted = true;
|
|
5901
|
+
await seedSessionInResumeCursorForCustomLoop(currentPayload);
|
|
5902
|
+
stop = createStopSignal();
|
|
5903
|
+
}
|
|
5537
5904
|
turn++;
|
|
5538
|
-
//
|
|
5539
|
-
|
|
5905
|
+
// Head-start handover: the server triggered this run with
|
|
5906
|
+
// `trigger: "handover-prepare"` and signals the warm step-1 partial on
|
|
5907
|
+
// `session.in`. Wait for it BEFORE any `messagesInput.waitWithIdleTimeout`
|
|
5908
|
+
// (that facade consumes-and-discards non-message chunks and would swallow
|
|
5909
|
+
// the signal). Turn-0 only — continuation boots never carry this trigger.
|
|
5910
|
+
let handoverThisTurn = null;
|
|
5911
|
+
let pendingHandoverSignal = null;
|
|
5912
|
+
if (turn === 0 && currentPayload.trigger === "handover-prepare") {
|
|
5913
|
+
const signal = await waitForHandover({
|
|
5914
|
+
payload: currentPayload,
|
|
5915
|
+
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds,
|
|
5916
|
+
timeout,
|
|
5917
|
+
});
|
|
5918
|
+
if (!signal || signal.kind === "handover-skip" || runSignal.aborted) {
|
|
5919
|
+
stop.cleanup();
|
|
5920
|
+
return { done: true, value: undefined };
|
|
5921
|
+
}
|
|
5922
|
+
pendingHandoverSignal = signal;
|
|
5923
|
+
handoverThisTurn = { isFinal: signal.isFinal };
|
|
5924
|
+
// Rewrite to a normal first-turn message turn so the rest of the loop
|
|
5925
|
+
// (steering setup, addIncoming, turnObj) runs unchanged.
|
|
5926
|
+
currentPayload = { ...currentPayload, trigger: "submit-message", message: undefined };
|
|
5927
|
+
}
|
|
5928
|
+
// First turn: wait when the boot payload carries no message.
|
|
5929
|
+
// Preload boots wait for the first real message; continuation
|
|
5930
|
+
// boots (fresh run via `ensureRunForSession` / end-and-continue)
|
|
5931
|
+
// arrive with the sticky boot-payload fields stripped, so running
|
|
5932
|
+
// a turn immediately would invoke the model with no user input.
|
|
5933
|
+
const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message;
|
|
5934
|
+
if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
|
|
5540
5935
|
const result = await messagesInput.waitWithIdleTimeout({
|
|
5541
5936
|
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
|
|
5542
5937
|
timeout,
|
|
5543
|
-
spanName:
|
|
5938
|
+
spanName: currentPayload.trigger === "preload"
|
|
5939
|
+
? "waiting for first message"
|
|
5940
|
+
: "waiting for first message (continuation)",
|
|
5544
5941
|
});
|
|
5545
5942
|
if (!result.ok || runSignal.aborted) {
|
|
5546
5943
|
stop.cleanup();
|
|
5547
5944
|
return { done: true, value: undefined };
|
|
5548
5945
|
}
|
|
5946
|
+
const continuationBoot = isMessagelessContinuationBoot;
|
|
5549
5947
|
currentPayload = result.output;
|
|
5948
|
+
// Preserve the continuation flag — the wire payload of the next
|
|
5949
|
+
// message doesn't carry it, and `turn.continuation` is how the
|
|
5950
|
+
// user knows to seed history (e.g. `turn.setMessages(stored)`).
|
|
5951
|
+
if (continuationBoot && currentPayload.continuation === undefined) {
|
|
5952
|
+
currentPayload = { ...currentPayload, continuation: true };
|
|
5953
|
+
}
|
|
5550
5954
|
}
|
|
5551
5955
|
// Subsequent turns: wait for the next message
|
|
5552
5956
|
if (turn > 0) {
|
|
@@ -5627,6 +6031,16 @@ function createChatSession(payload, options) {
|
|
|
5627
6031
|
? [currentPayload.message]
|
|
5628
6032
|
: [];
|
|
5629
6033
|
const messages = await accumulator.addIncoming(incomingForAccumulator, currentPayload.trigger, turn);
|
|
6034
|
+
// Apply the head-start handover AFTER addIncoming — turn-0 addIncoming
|
|
6035
|
+
// replaces accumulator state, which would wipe a pre-applied splice.
|
|
6036
|
+
// Seed prior history first, then splice the warm step-1 partial.
|
|
6037
|
+
if (pendingHandoverSignal) {
|
|
6038
|
+
const priorHistory = currentPayload.headStartMessages;
|
|
6039
|
+
if (priorHistory && priorHistory.length > 0) {
|
|
6040
|
+
await accumulator.setMessages(priorHistory);
|
|
6041
|
+
}
|
|
6042
|
+
accumulator.applyHandover(pendingHandoverSignal);
|
|
6043
|
+
}
|
|
5630
6044
|
// chat.requestUpgrade() called before this turn — signal transport and exit
|
|
5631
6045
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
5632
6046
|
await writeUpgradeRequiredChunk();
|
|
@@ -5653,13 +6067,38 @@ function createChatSession(payload, options) {
|
|
|
5653
6067
|
continuation: currentPayload.continuation ?? false,
|
|
5654
6068
|
previousTurnUsage,
|
|
5655
6069
|
totalUsage: cumulativeUsage,
|
|
6070
|
+
handover: handoverThisTurn,
|
|
5656
6071
|
async setMessages(uiMessages) {
|
|
5657
6072
|
await accumulator.setMessages(uiMessages);
|
|
5658
6073
|
},
|
|
5659
6074
|
async complete(source) {
|
|
6075
|
+
// Head-start final turn: the warm step-1 partial is already spliced
|
|
6076
|
+
// into the accumulator and IS the response — nothing to pipe. Only
|
|
6077
|
+
// valid on a final handover; a missing source on any other turn is a
|
|
6078
|
+
// mistake (it would silently finalize without an assistant response).
|
|
6079
|
+
if (!source) {
|
|
6080
|
+
if (!handoverThisTurn?.isFinal) {
|
|
6081
|
+
throw new Error("turn.complete() requires a stream source unless turn.handover.isFinal is true");
|
|
6082
|
+
}
|
|
6083
|
+
const response = accumulator.uiMessages.at(-1);
|
|
6084
|
+
if (!response || response.role !== "assistant") {
|
|
6085
|
+
throw new Error("turn.complete() could not find the spliced handover response");
|
|
6086
|
+
}
|
|
6087
|
+
sessionMsgSub.off();
|
|
6088
|
+
await chatWriteTurnComplete();
|
|
6089
|
+
return response;
|
|
6090
|
+
}
|
|
5660
6091
|
let response;
|
|
5661
6092
|
try {
|
|
5662
|
-
response = await pipeChatAndCapture(source, {
|
|
6093
|
+
response = await pipeChatAndCapture(source, {
|
|
6094
|
+
signal: combinedSignal,
|
|
6095
|
+
// On a non-final handover turn, thread the spliced partial so a
|
|
6096
|
+
// resumed tool round's tool-output chunks merge into the
|
|
6097
|
+
// handed-over tool-call. Gated on the handover turn only — a
|
|
6098
|
+
// normal turn must not pass originalMessages (it would merge the
|
|
6099
|
+
// fresh response into the prior assistant message).
|
|
6100
|
+
...(handoverThisTurn ? { originalMessages: accumulator.uiMessages } : {}),
|
|
6101
|
+
});
|
|
5663
6102
|
}
|
|
5664
6103
|
catch (error) {
|
|
5665
6104
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -5699,14 +6138,22 @@ function createChatSession(payload, options) {
|
|
|
5699
6138
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
5700
6139
|
}
|
|
5701
6140
|
}
|
|
5702
|
-
// Capture token usage from the streamText result
|
|
6141
|
+
// Capture token usage from the streamText result. Race with a 2s
|
|
6142
|
+
// timeout — on stop-abort the AI SDK's totalUsage promise can hang
|
|
6143
|
+
// indefinitely, which would wedge the turn loop (same guard as
|
|
6144
|
+
// chat.agent's turn loop).
|
|
5703
6145
|
let turnUsage;
|
|
5704
6146
|
if (typeof source.totalUsage?.then === "function") {
|
|
5705
6147
|
try {
|
|
5706
|
-
const usage = await
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
6148
|
+
const usage = (await Promise.race([
|
|
6149
|
+
source.totalUsage,
|
|
6150
|
+
new Promise((r) => setTimeout(() => r(undefined), 2_000)),
|
|
6151
|
+
]));
|
|
6152
|
+
if (usage) {
|
|
6153
|
+
turnUsage = usage;
|
|
6154
|
+
previousTurnUsage = usage;
|
|
6155
|
+
cumulativeUsage = addUsage(cumulativeUsage, usage);
|
|
6156
|
+
}
|
|
5710
6157
|
}
|
|
5711
6158
|
catch {
|
|
5712
6159
|
/* non-fatal */
|
|
@@ -5806,7 +6253,8 @@ function createChatSession(payload, options) {
|
|
|
5806
6253
|
return { done: false, value: turnObj };
|
|
5807
6254
|
},
|
|
5808
6255
|
async return() {
|
|
5809
|
-
stop
|
|
6256
|
+
// `stop` only exists once next() has booted the iterator.
|
|
6257
|
+
stop?.cleanup();
|
|
5810
6258
|
return { done: true, value: undefined };
|
|
5811
6259
|
},
|
|
5812
6260
|
};
|
|
@@ -6049,6 +6497,7 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
6049
6497
|
// run-list filter by chat works without the customer having to wire it
|
|
6050
6498
|
// up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
|
|
6051
6499
|
const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
|
|
6500
|
+
// SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot.
|
|
6052
6501
|
const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
|
|
6053
6502
|
const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
|
|
6054
6503
|
const triggerConfig = {
|
|
@@ -6073,6 +6522,20 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
6073
6522
|
maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts,
|
|
6074
6523
|
}
|
|
6075
6524
|
: {}),
|
|
6525
|
+
...(options?.triggerConfig?.maxDuration !== undefined ||
|
|
6526
|
+
params.triggerConfig?.maxDuration !== undefined
|
|
6527
|
+
? {
|
|
6528
|
+
maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration,
|
|
6529
|
+
}
|
|
6530
|
+
: {}),
|
|
6531
|
+
...(options?.triggerConfig?.region || params.triggerConfig?.region
|
|
6532
|
+
? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region }
|
|
6533
|
+
: {}),
|
|
6534
|
+
...(options?.triggerConfig?.lockToVersion || params.triggerConfig?.lockToVersion
|
|
6535
|
+
? {
|
|
6536
|
+
lockToVersion: params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion,
|
|
6537
|
+
}
|
|
6538
|
+
: {}),
|
|
6076
6539
|
...(options?.triggerConfig?.idleTimeoutInSeconds !== undefined ||
|
|
6077
6540
|
params.triggerConfig?.idleTimeoutInSeconds !== undefined
|
|
6078
6541
|
? {
|
|
@@ -6251,10 +6714,20 @@ exports.chat = {
|
|
|
6251
6714
|
MessageAccumulator: ChatMessageAccumulator,
|
|
6252
6715
|
/** Create a chat session (async iterator). See {@link createChatSession}. */
|
|
6253
6716
|
createSession: createChatSession,
|
|
6717
|
+
/**
|
|
6718
|
+
* Wait for a `chat.headStart` handover signal inside a `chat.customAgent`
|
|
6719
|
+
* loop (turn 0). See {@link waitForHandover}. For most loops prefer the
|
|
6720
|
+
* `chat.MessageAccumulator.consumeHandover()` convenience, which also seeds
|
|
6721
|
+
* `payload.headStartMessages` and applies the partial.
|
|
6722
|
+
*/
|
|
6723
|
+
waitForHandover,
|
|
6254
6724
|
/**
|
|
6255
6725
|
* Store and retrieve a resolved prompt for the current run.
|
|
6256
6726
|
*
|
|
6257
6727
|
* - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
|
|
6728
|
+
* - `chat.prompt.set(resolved, { providerOptions })` — also attach provider
|
|
6729
|
+
* options to the system block so a provider can cache it (e.g. Anthropic
|
|
6730
|
+
* prompt caching). See the prompt-caching guide.
|
|
6258
6731
|
* - `chat.prompt()` — read the stored prompt (throws if not set)
|
|
6259
6732
|
*/
|
|
6260
6733
|
prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),
|
|
@@ -6347,8 +6820,19 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
6347
6820
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
6348
6821
|
// first-turn-ever (or first turn post-OOM without a snapshot seed) is
|
|
6349
6822
|
// fine — the chain catches up next turn.
|
|
6350
|
-
|
|
6351
|
-
|
|
6823
|
+
//
|
|
6824
|
+
// Lazily create the slot if a caller reached here without one (a plain
|
|
6825
|
+
// `task()` driving `chat.createSession` / `chat.writeTurnComplete`, vs.
|
|
6826
|
+
// chatAgent/chatCustomAgent which seed it at boot). The first call then
|
|
6827
|
+
// does no trim (nothing before it) and records its seq; later calls trim
|
|
6828
|
+
// — so `.out` is bounded for every writeTurnComplete caller, not just the
|
|
6829
|
+
// built-in agents.
|
|
6830
|
+
let slot = locals_js_1.locals.get(lastTurnCompleteSeqNumKey);
|
|
6831
|
+
if (!slot) {
|
|
6832
|
+
slot = { value: undefined };
|
|
6833
|
+
locals_js_1.locals.set(lastTurnCompleteSeqNumKey, slot);
|
|
6834
|
+
}
|
|
6835
|
+
const prev = slot.value;
|
|
6352
6836
|
if (slot && prev !== undefined) {
|
|
6353
6837
|
try {
|
|
6354
6838
|
await session.out.trimTo(prev);
|