@trigger.dev/sdk 4.5.0-rc.0 → 4.5.0-rc.2
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-shared.d.ts +70 -0
- package/dist/commonjs/v3/ai-shared.js +144 -0
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +21 -5
- package/dist/commonjs/v3/ai.js +153 -17
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/auth.d.ts +1 -0
- package/dist/commonjs/v3/auth.js +1 -0
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +8 -3
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat.js +10 -2
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/index.d.ts +1 -0
- package/dist/commonjs/v3/index.js +3 -1
- package/dist/commonjs/v3/index.js.map +1 -1
- package/dist/commonjs/v3/shared.js +13 -7
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/triggerClient.d.ts +45 -0
- package/dist/commonjs/v3/triggerClient.js +104 -0
- package/dist/commonjs/v3/triggerClient.js.map +1 -0
- package/dist/commonjs/v3/triggerClient.test.d.ts +1 -0
- package/dist/commonjs/v3/triggerClient.test.js +226 -0
- package/dist/commonjs/v3/triggerClient.test.js.map +1 -0
- package/dist/commonjs/v3/triggerClient.types.test.d.ts +1 -0
- package/dist/commonjs/v3/triggerClient.types.test.js +113 -0
- package/dist/commonjs/v3/triggerClient.types.test.js.map +1 -0
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai-shared.d.ts +70 -0
- package/dist/esm/v3/ai-shared.js +142 -0
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +21 -5
- package/dist/esm/v3/ai.js +152 -17
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.d.ts +1 -0
- package/dist/esm/v3/auth.js +1 -0
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/chat-client.js +8 -3
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat.js +10 -2
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/index.d.ts +1 -0
- package/dist/esm/v3/index.js +1 -0
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/esm/v3/shared.js +14 -8
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/triggerClient.d.ts +45 -0
- package/dist/esm/v3/triggerClient.js +77 -0
- package/dist/esm/v3/triggerClient.js.map +1 -0
- package/dist/esm/v3/triggerClient.test.d.ts +1 -0
- package/dist/esm/v3/triggerClient.test.js +224 -0
- package/dist/esm/v3/triggerClient.test.js.map +1 -0
- package/dist/esm/v3/triggerClient.types.test.d.ts +1 -0
- package/dist/esm/v3/triggerClient.types.test.js +88 -0
- package/dist/esm/v3/triggerClient.types.test.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/package.json +3 -2
|
@@ -171,3 +171,73 @@ export type InferChatClientData<TTask extends AnyTask> = TTask extends Task<stri
|
|
|
171
171
|
* ```
|
|
172
172
|
*/
|
|
173
173
|
export type InferChatUIMessage<TTask extends AnyTask> = TTask extends Task<string, ChatTaskWirePayload<infer TUIM extends UIMessage, any>, any> ? TUIM : UIMessage;
|
|
174
|
+
/**
|
|
175
|
+
* Upsert an incoming wire message into the customer's DB-backed chain
|
|
176
|
+
* inside a `hydrateMessages` hook. Returns `true` iff the chain was
|
|
177
|
+
* mutated (the caller should persist).
|
|
178
|
+
*
|
|
179
|
+
* Handles the three cases that matter:
|
|
180
|
+
*
|
|
181
|
+
* - **Non-submit-message trigger** (`regenerate-message` / `action`,
|
|
182
|
+
* or `submit-message` with no incoming): no-op. Returns `false`.
|
|
183
|
+
* - **Incoming id already in `stored`** (HITL `addToolOutput` /
|
|
184
|
+
* `addToolApproveResponse` continuation — the wire carries the
|
|
185
|
+
* existing assistant's id with a slim resolution payload): no-op.
|
|
186
|
+
* The runtime's per-turn merge overlays the new tool-state advance
|
|
187
|
+
* onto the existing entry; pushing again would duplicate the row
|
|
188
|
+
* in the chain you return, and the duplicate slim copy would hit
|
|
189
|
+
* `toModelMessages` with no `input`. Returns `false`.
|
|
190
|
+
* - **Incoming id not in `stored`** (typically a fresh user message
|
|
191
|
+
* on a new turn): push. Returns `true`.
|
|
192
|
+
*
|
|
193
|
+
* Mutates `stored` in place. The caller persists `stored`, not the
|
|
194
|
+
* return value.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* import { chat, upsertIncomingMessage } from "@trigger.dev/sdk/ai";
|
|
199
|
+
*
|
|
200
|
+
* chat.agent({
|
|
201
|
+
* hydrateMessages: async ({ chatId, trigger, incomingMessages }) => {
|
|
202
|
+
* const record = await db.chat.findUnique({ where: { id: chatId } });
|
|
203
|
+
* const stored = record?.messages ?? [];
|
|
204
|
+
* if (upsertIncomingMessage(stored, { trigger, incomingMessages })) {
|
|
205
|
+
* await db.chat.update({ where: { id: chatId }, data: { messages: stored } });
|
|
206
|
+
* }
|
|
207
|
+
* return stored;
|
|
208
|
+
* },
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export declare function upsertIncomingMessage<TMsg extends UIMessage = UIMessage>(stored: TMsg[], event: {
|
|
213
|
+
trigger: "submit-message" | "regenerate-message" | "action";
|
|
214
|
+
incomingMessages: TMsg[];
|
|
215
|
+
}): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Slim an outgoing assistant message before it ships on `submit-message`.
|
|
218
|
+
*
|
|
219
|
+
* When the client calls `addToolOutput(...)` to resolve a HITL tool (or
|
|
220
|
+
* `addToolApproveResponse(...)` to approve/deny one), the AI SDK turns
|
|
221
|
+
* it into a `submit-message` whose `messages.at(-1)` is the existing
|
|
222
|
+
* assistant message with the new state stitched onto a single tool
|
|
223
|
+
* part. On a reasoning-heavy multi-step turn, that full assistant
|
|
224
|
+
* message can be 600 KB – 1 MB (encrypted reasoning blobs, reasoning
|
|
225
|
+
* text, full tool `input` JSON, prior tool outputs) — well over the
|
|
226
|
+
* `.in/append` cap.
|
|
227
|
+
*
|
|
228
|
+
* The agent runtime only consumes the wire-advanced fields of those
|
|
229
|
+
* tool parts (state + output / errorText / approval). Everything else
|
|
230
|
+
* (text, reasoning, tool `input`) is rebuilt server-side from the
|
|
231
|
+
* durable snapshot or `hydrateMessages`. So we drop everything but
|
|
232
|
+
* the advanced tool parts here, and reduce those to just the fields
|
|
233
|
+
* the server overlays.
|
|
234
|
+
*
|
|
235
|
+
* The slim only fires when the assistant message carries at least one
|
|
236
|
+
* wire-advanceable tool part. Plain assistant resends (no resolved /
|
|
237
|
+
* approval-responded tool) and non-assistant messages pass through
|
|
238
|
+
* untouched.
|
|
239
|
+
*
|
|
240
|
+
* Pairs with the per-turn merge on the agent side
|
|
241
|
+
* (`mergeIncomingIntoHydrated` in `ai.ts`).
|
|
242
|
+
*/
|
|
243
|
+
export declare function slimSubmitMessageForWire<TMsg extends UIMessage | undefined>(message: TMsg): TMsg;
|
|
@@ -17,9 +17,153 @@
|
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.PENDING_MESSAGE_INJECTED_TYPE = void 0;
|
|
20
|
+
exports.upsertIncomingMessage = upsertIncomingMessage;
|
|
21
|
+
exports.slimSubmitMessageForWire = slimSubmitMessageForWire;
|
|
20
22
|
/**
|
|
21
23
|
* Message-part `type` value for the pending-message data part the agent
|
|
22
24
|
* injects when a follow-up message arrives mid-turn.
|
|
23
25
|
*/
|
|
24
26
|
exports.PENDING_MESSAGE_INJECTED_TYPE = "data-pending-message-injected";
|
|
27
|
+
/**
|
|
28
|
+
* Upsert an incoming wire message into the customer's DB-backed chain
|
|
29
|
+
* inside a `hydrateMessages` hook. Returns `true` iff the chain was
|
|
30
|
+
* mutated (the caller should persist).
|
|
31
|
+
*
|
|
32
|
+
* Handles the three cases that matter:
|
|
33
|
+
*
|
|
34
|
+
* - **Non-submit-message trigger** (`regenerate-message` / `action`,
|
|
35
|
+
* or `submit-message` with no incoming): no-op. Returns `false`.
|
|
36
|
+
* - **Incoming id already in `stored`** (HITL `addToolOutput` /
|
|
37
|
+
* `addToolApproveResponse` continuation — the wire carries the
|
|
38
|
+
* existing assistant's id with a slim resolution payload): no-op.
|
|
39
|
+
* The runtime's per-turn merge overlays the new tool-state advance
|
|
40
|
+
* onto the existing entry; pushing again would duplicate the row
|
|
41
|
+
* in the chain you return, and the duplicate slim copy would hit
|
|
42
|
+
* `toModelMessages` with no `input`. Returns `false`.
|
|
43
|
+
* - **Incoming id not in `stored`** (typically a fresh user message
|
|
44
|
+
* on a new turn): push. Returns `true`.
|
|
45
|
+
*
|
|
46
|
+
* Mutates `stored` in place. The caller persists `stored`, not the
|
|
47
|
+
* return value.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { chat, upsertIncomingMessage } from "@trigger.dev/sdk/ai";
|
|
52
|
+
*
|
|
53
|
+
* chat.agent({
|
|
54
|
+
* hydrateMessages: async ({ chatId, trigger, incomingMessages }) => {
|
|
55
|
+
* const record = await db.chat.findUnique({ where: { id: chatId } });
|
|
56
|
+
* const stored = record?.messages ?? [];
|
|
57
|
+
* if (upsertIncomingMessage(stored, { trigger, incomingMessages })) {
|
|
58
|
+
* await db.chat.update({ where: { id: chatId }, data: { messages: stored } });
|
|
59
|
+
* }
|
|
60
|
+
* return stored;
|
|
61
|
+
* },
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
function upsertIncomingMessage(stored, event) {
|
|
66
|
+
if (event.trigger !== "submit-message")
|
|
67
|
+
return false;
|
|
68
|
+
if (event.incomingMessages.length === 0)
|
|
69
|
+
return false;
|
|
70
|
+
const newMsg = event.incomingMessages[event.incomingMessages.length - 1];
|
|
71
|
+
if (!newMsg)
|
|
72
|
+
return false;
|
|
73
|
+
if (newMsg.id) {
|
|
74
|
+
const existingIdx = stored.findIndex((m) => m.id === newMsg.id);
|
|
75
|
+
if (existingIdx !== -1)
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
stored.push(newMsg);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Tool-part states that the client advances and ships back over the wire.
|
|
83
|
+
* Covers HITL `addToolOutput` (output-available / output-error) and the
|
|
84
|
+
* approval flow (approval-responded / output-denied). `input-streaming` /
|
|
85
|
+
* `input-available` / `approval-requested` are server-emitted only — if
|
|
86
|
+
* we see them on the wire we treat them as no-ops and skip the slim/merge.
|
|
87
|
+
*/
|
|
88
|
+
function isWireAdvanceableToolState(state) {
|
|
89
|
+
return (state === "output-available" ||
|
|
90
|
+
state === "output-error" ||
|
|
91
|
+
state === "approval-responded" ||
|
|
92
|
+
state === "output-denied");
|
|
93
|
+
}
|
|
94
|
+
/** Whether a tool-UI part is a static (`tool-${name}`) or dynamic tool. */
|
|
95
|
+
function isToolPartType(type) {
|
|
96
|
+
return typeof type === "string" && (type.startsWith("tool-") || type === "dynamic-tool");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Slim an outgoing assistant message before it ships on `submit-message`.
|
|
100
|
+
*
|
|
101
|
+
* When the client calls `addToolOutput(...)` to resolve a HITL tool (or
|
|
102
|
+
* `addToolApproveResponse(...)` to approve/deny one), the AI SDK turns
|
|
103
|
+
* it into a `submit-message` whose `messages.at(-1)` is the existing
|
|
104
|
+
* assistant message with the new state stitched onto a single tool
|
|
105
|
+
* part. On a reasoning-heavy multi-step turn, that full assistant
|
|
106
|
+
* message can be 600 KB – 1 MB (encrypted reasoning blobs, reasoning
|
|
107
|
+
* text, full tool `input` JSON, prior tool outputs) — well over the
|
|
108
|
+
* `.in/append` cap.
|
|
109
|
+
*
|
|
110
|
+
* The agent runtime only consumes the wire-advanced fields of those
|
|
111
|
+
* tool parts (state + output / errorText / approval). Everything else
|
|
112
|
+
* (text, reasoning, tool `input`) is rebuilt server-side from the
|
|
113
|
+
* durable snapshot or `hydrateMessages`. So we drop everything but
|
|
114
|
+
* the advanced tool parts here, and reduce those to just the fields
|
|
115
|
+
* the server overlays.
|
|
116
|
+
*
|
|
117
|
+
* The slim only fires when the assistant message carries at least one
|
|
118
|
+
* wire-advanceable tool part. Plain assistant resends (no resolved /
|
|
119
|
+
* approval-responded tool) and non-assistant messages pass through
|
|
120
|
+
* untouched.
|
|
121
|
+
*
|
|
122
|
+
* Pairs with the per-turn merge on the agent side
|
|
123
|
+
* (`mergeIncomingIntoHydrated` in `ai.ts`).
|
|
124
|
+
*/
|
|
125
|
+
function slimSubmitMessageForWire(message) {
|
|
126
|
+
if (!message)
|
|
127
|
+
return message;
|
|
128
|
+
if (message.role !== "assistant")
|
|
129
|
+
return message;
|
|
130
|
+
const parts = (message.parts ?? []);
|
|
131
|
+
const advancedToolParts = parts.filter((p) => p &&
|
|
132
|
+
typeof p === "object" &&
|
|
133
|
+
isToolPartType(p.type) &&
|
|
134
|
+
isWireAdvanceableToolState(p.state));
|
|
135
|
+
if (advancedToolParts.length === 0)
|
|
136
|
+
return message;
|
|
137
|
+
const slimParts = advancedToolParts.map((p) => {
|
|
138
|
+
const base = {
|
|
139
|
+
type: p.type,
|
|
140
|
+
toolCallId: p.toolCallId,
|
|
141
|
+
state: p.state,
|
|
142
|
+
};
|
|
143
|
+
if (p.type === "dynamic-tool" && typeof p.toolName === "string") {
|
|
144
|
+
base.toolName = p.toolName;
|
|
145
|
+
}
|
|
146
|
+
if (p.state === "output-available") {
|
|
147
|
+
base.output = p.output;
|
|
148
|
+
if (p.approval !== undefined)
|
|
149
|
+
base.approval = p.approval;
|
|
150
|
+
}
|
|
151
|
+
else if (p.state === "output-error") {
|
|
152
|
+
if (p.errorText !== undefined)
|
|
153
|
+
base.errorText = p.errorText;
|
|
154
|
+
if (p.approval !== undefined)
|
|
155
|
+
base.approval = p.approval;
|
|
156
|
+
}
|
|
157
|
+
else if (p.state === "approval-responded" || p.state === "output-denied") {
|
|
158
|
+
if (p.approval !== undefined)
|
|
159
|
+
base.approval = p.approval;
|
|
160
|
+
}
|
|
161
|
+
return base;
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
id: message.id,
|
|
165
|
+
role: message.role,
|
|
166
|
+
parts: slimParts,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
25
169
|
//# sourceMappingURL=ai-shared.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-shared.js","sourceRoot":"","sources":["../../../src/v3/ai-shared.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;
|
|
1
|
+
{"version":3,"file":"ai-shared.js","sourceRoot":"","sources":["../../../src/v3/ai-shared.ts"],"names":[],"mappings":"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"}
|
package/dist/commonjs/v3/ai.d.ts
CHANGED
|
@@ -446,7 +446,7 @@ export type PendingMessagesOptions<TUIM extends UIMessage = UIMessage> = {
|
|
|
446
446
|
* between tool-call steps. The frontend can match on this to render
|
|
447
447
|
* injection points inline in the assistant response.
|
|
448
448
|
*/
|
|
449
|
-
export { PENDING_MESSAGE_INJECTED_TYPE } from "./ai-shared.js";
|
|
449
|
+
export { PENDING_MESSAGE_INJECTED_TYPE, upsertIncomingMessage } from "./ai-shared.js";
|
|
450
450
|
/**
|
|
451
451
|
* Event passed to the `prepareMessages` hook.
|
|
452
452
|
*/
|
|
@@ -1085,7 +1085,14 @@ export type HydrateMessagesEvent<TClientData = unknown, TUIM extends UIMessage =
|
|
|
1085
1085
|
* Event passed to the `onValidateMessages` callback.
|
|
1086
1086
|
*/
|
|
1087
1087
|
export type ValidateMessagesEvent<TUIM extends UIMessage = UIMessage> = {
|
|
1088
|
-
/**
|
|
1088
|
+
/**
|
|
1089
|
+
* The incoming UI messages for this turn (after cleanup of aborted tool parts).
|
|
1090
|
+
*
|
|
1091
|
+
* For HITL continuations the assistant entry is slim — `state` + `output` /
|
|
1092
|
+
* `errorText` / `approval` only, no `input` or other parts. Don't pass the
|
|
1093
|
+
* full `messages` array to `validateUIMessages` from `ai`; filter to user
|
|
1094
|
+
* messages (or your own subset) first.
|
|
1095
|
+
*/
|
|
1089
1096
|
messages: TUIM[];
|
|
1090
1097
|
/** The unique identifier for the chat session. */
|
|
1091
1098
|
chatId: string;
|
|
@@ -1534,8 +1541,13 @@ export type ChatAgentOptions<TIdentifier extends string, TClientDataSchema exten
|
|
|
1534
1541
|
*
|
|
1535
1542
|
* Return the validated messages array. Throw to abort the turn with an error.
|
|
1536
1543
|
*
|
|
1537
|
-
* This is the right place to call the AI SDK's `validateUIMessages`
|
|
1538
|
-
*
|
|
1544
|
+
* This is the right place to call the AI SDK's `validateUIMessages` on fresh
|
|
1545
|
+
* user input. For HITL continuations (`addToolOutput` /
|
|
1546
|
+
* `addToolApproveResponse`), the wire carries a slim assistant message — only
|
|
1547
|
+
* the resolved tool parts, with `state` + `output` / `errorText` / `approval`
|
|
1548
|
+
* and no `input`. `validateUIMessages` against the AI SDK schema rejects
|
|
1549
|
+
* that shape, so filter to user messages (or skip validation entirely) on
|
|
1550
|
+
* those turns.
|
|
1539
1551
|
*
|
|
1540
1552
|
* @example
|
|
1541
1553
|
* ```ts
|
|
@@ -1544,7 +1556,11 @@ export type ChatAgentOptions<TIdentifier extends string, TClientDataSchema exten
|
|
|
1544
1556
|
* chat.agent({
|
|
1545
1557
|
* id: "my-chat",
|
|
1546
1558
|
* onValidateMessages: async ({ messages }) => {
|
|
1547
|
-
*
|
|
1559
|
+
* const userMessages = messages.filter((m) => m.role === "user");
|
|
1560
|
+
* if (userMessages.length > 0) {
|
|
1561
|
+
* await validateUIMessages({ messages: userMessages, tools: chatTools });
|
|
1562
|
+
* }
|
|
1563
|
+
* return messages;
|
|
1548
1564
|
* },
|
|
1549
1565
|
* run: async ({ messages }) => {
|
|
1550
1566
|
* return streamText({ model, messages, tools: chatTools });
|
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.chat = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
|
|
3
|
+
exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
|
|
4
4
|
exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
|
|
5
5
|
exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
|
|
6
6
|
exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
|
|
@@ -1534,6 +1534,107 @@ function extractNewToolResultsFromHistory(message, messages) {
|
|
|
1534
1534
|
}
|
|
1535
1535
|
return out;
|
|
1536
1536
|
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Per-turn merge of an incoming wire `UIMessage` onto the matching entry
|
|
1539
|
+
* a `hydrateMessages` hook (or the default accumulator) provides. Used
|
|
1540
|
+
* to fold tool-state advances from the client into the agent's
|
|
1541
|
+
* authoritative chain without trusting the wire copy for fields the
|
|
1542
|
+
* LLM consumes.
|
|
1543
|
+
*
|
|
1544
|
+
* `hydrated` is treated as the source of truth for everything outside
|
|
1545
|
+
* tool-state advancement: text, reasoning blobs, provider metadata,
|
|
1546
|
+
* and tool `input` all stay as hydrated had them. We only overlay
|
|
1547
|
+
* tool parts whose incoming state is wire-advanced — `output-available`
|
|
1548
|
+
* / `output-error` (HITL `addToolOutput`) or `approval-responded` /
|
|
1549
|
+
* `output-denied` (approval flow) — and only the corresponding
|
|
1550
|
+
* resolution fields (`output` / `errorText` / `approval`). Hydrated
|
|
1551
|
+
* `input` and everything else stay put.
|
|
1552
|
+
*
|
|
1553
|
+
* Without this, a slim wire copy (which `TriggerChatTransport` /
|
|
1554
|
+
* `AgentChat.sendRaw` ship by default on HITL continuations) would
|
|
1555
|
+
* clobber the hydrated assistant — the next LLM call would receive a
|
|
1556
|
+
* tool call with no `input` and 4xx.
|
|
1557
|
+
*
|
|
1558
|
+
* @internal
|
|
1559
|
+
*/
|
|
1560
|
+
function mergeIncomingIntoHydrated(hydrated, incoming) {
|
|
1561
|
+
const incomingAdvancedByCallId = new Map();
|
|
1562
|
+
for (const part of (incoming.parts ?? [])) {
|
|
1563
|
+
if (!(0, ai_1.isToolUIPart)(part))
|
|
1564
|
+
continue;
|
|
1565
|
+
const toolCallId = part.toolCallId;
|
|
1566
|
+
if (typeof toolCallId !== "string" || toolCallId.length === 0)
|
|
1567
|
+
continue;
|
|
1568
|
+
if (!isWireAdvanceableToolState(part.state))
|
|
1569
|
+
continue;
|
|
1570
|
+
incomingAdvancedByCallId.set(toolCallId, part);
|
|
1571
|
+
}
|
|
1572
|
+
if (incomingAdvancedByCallId.size === 0)
|
|
1573
|
+
return hydrated;
|
|
1574
|
+
let mutated = false;
|
|
1575
|
+
const hydratedParts = (hydrated.parts ?? []);
|
|
1576
|
+
const mergedParts = hydratedParts.map((part) => {
|
|
1577
|
+
if (!(0, ai_1.isToolUIPart)(part))
|
|
1578
|
+
return part;
|
|
1579
|
+
const toolCallId = part.toolCallId;
|
|
1580
|
+
if (typeof toolCallId !== "string" || toolCallId.length === 0)
|
|
1581
|
+
return part;
|
|
1582
|
+
const incomingPart = incomingAdvancedByCallId.get(toolCallId);
|
|
1583
|
+
if (!incomingPart)
|
|
1584
|
+
return part;
|
|
1585
|
+
// Terminal hydrated states (`output-available`, `output-error`,
|
|
1586
|
+
// `output-denied`) are authoritative — never regressed by a stale
|
|
1587
|
+
// wire arrival (replay, retry, out-of-order). `output-denied`
|
|
1588
|
+
// matters here because the wire's `approval-responded` could
|
|
1589
|
+
// otherwise overwrite a hydrated denial back to a non-terminal
|
|
1590
|
+
// state.
|
|
1591
|
+
if (isResolvedToolState(part.state) || part.state === "output-denied") {
|
|
1592
|
+
return part;
|
|
1593
|
+
}
|
|
1594
|
+
// Same state on both sides — no progression to apply.
|
|
1595
|
+
if (part.state === incomingPart.state)
|
|
1596
|
+
return part;
|
|
1597
|
+
mutated = true;
|
|
1598
|
+
if (incomingPart.state === "output-available") {
|
|
1599
|
+
return {
|
|
1600
|
+
...part,
|
|
1601
|
+
state: incomingPart.state,
|
|
1602
|
+
output: incomingPart.output,
|
|
1603
|
+
...(incomingPart.approval !== undefined ? { approval: incomingPart.approval } : {}),
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
if (incomingPart.state === "output-error") {
|
|
1607
|
+
return {
|
|
1608
|
+
...part,
|
|
1609
|
+
state: incomingPart.state,
|
|
1610
|
+
errorText: incomingPart.errorText,
|
|
1611
|
+
...(incomingPart.approval !== undefined ? { approval: incomingPart.approval } : {}),
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
// approval-responded / output-denied — overlay state + approval.
|
|
1615
|
+
return {
|
|
1616
|
+
...part,
|
|
1617
|
+
state: incomingPart.state,
|
|
1618
|
+
...(incomingPart.approval !== undefined ? { approval: incomingPart.approval } : {}),
|
|
1619
|
+
};
|
|
1620
|
+
});
|
|
1621
|
+
if (!mutated)
|
|
1622
|
+
return hydrated;
|
|
1623
|
+
return { ...hydrated, parts: mergedParts };
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Mirror of `slimSubmitMessageForWire`'s predicate. Kept here so the
|
|
1627
|
+
* agent runtime doesn't have to import from `ai-shared.ts` for a
|
|
1628
|
+
* one-liner. See that file for the full state-machine docs.
|
|
1629
|
+
*
|
|
1630
|
+
* @internal
|
|
1631
|
+
*/
|
|
1632
|
+
function isWireAdvanceableToolState(state) {
|
|
1633
|
+
return (state === "output-available" ||
|
|
1634
|
+
state === "output-error" ||
|
|
1635
|
+
state === "approval-responded" ||
|
|
1636
|
+
state === "output-denied");
|
|
1637
|
+
}
|
|
1537
1638
|
/**
|
|
1538
1639
|
* Imperative API for reading and modifying the accumulated message history.
|
|
1539
1640
|
*
|
|
@@ -1663,6 +1764,7 @@ const chatAgentCompactionKey = locals_js_1.locals.create("chat.agentCompaction")
|
|
|
1663
1764
|
// `@trigger.dev/sdk/ai` consumers still see it.
|
|
1664
1765
|
var ai_shared_js_1 = require("./ai-shared.js");
|
|
1665
1766
|
Object.defineProperty(exports, "PENDING_MESSAGE_INJECTED_TYPE", { enumerable: true, get: function () { return ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE; } });
|
|
1767
|
+
Object.defineProperty(exports, "upsertIncomingMessage", { enumerable: true, get: function () { return ai_shared_js_1.upsertIncomingMessage; } });
|
|
1666
1768
|
const ai_shared_js_2 = require("./ai-shared.js");
|
|
1667
1769
|
/** @internal */
|
|
1668
1770
|
const chatPendingMessagesKey = locals_js_1.locals.create("chat.pendingMessages");
|
|
@@ -3429,6 +3531,17 @@ function chatAgent(options) {
|
|
|
3429
3531
|
}));
|
|
3430
3532
|
}
|
|
3431
3533
|
if (hydrateMessages) {
|
|
3534
|
+
// Snapshot the ids the accumulator knew BEFORE this
|
|
3535
|
+
// turn ran — used below to decide whether an
|
|
3536
|
+
// incoming wire message is genuinely new or just a
|
|
3537
|
+
// state advance on an existing entry. We can't use
|
|
3538
|
+
// the post-`hydrateMessages` array for this because
|
|
3539
|
+
// the canonical hook pattern pushes the incoming
|
|
3540
|
+
// user message into the persisted chain and
|
|
3541
|
+
// returns it.
|
|
3542
|
+
const previouslyKnownMessageIds = new Set(accumulatedUIMessages
|
|
3543
|
+
.map((m) => m.id)
|
|
3544
|
+
.filter((id) => typeof id === "string"));
|
|
3432
3545
|
// Backend hydration: load the full message history from the user's
|
|
3433
3546
|
// backend, replacing the built-in accumulator entirely. With slim
|
|
3434
3547
|
// wire, `incomingMessages` is consistently 0-or-1-length — what
|
|
@@ -3455,29 +3568,50 @@ function chatAgent(options) {
|
|
|
3455
3568
|
"chat.incoming_messages.count": cleanedUIMessages.length,
|
|
3456
3569
|
},
|
|
3457
3570
|
});
|
|
3458
|
-
//
|
|
3459
|
-
//
|
|
3460
|
-
//
|
|
3571
|
+
// Per-turn merge of incoming wire messages onto the hydrated
|
|
3572
|
+
// chain. Hydrated stays authoritative for text, reasoning
|
|
3573
|
+
// blobs, provider metadata, and tool `input`; we only
|
|
3574
|
+
// overlay tool-part state/output/errorText for tool calls
|
|
3575
|
+
// the wire copy has just resolved. Apps that slim the wire
|
|
3576
|
+
// copy to fit the .in/append cap (or drop fields they
|
|
3577
|
+
// re-source from their own DB) get the hydrated copy
|
|
3578
|
+
// through unchanged.
|
|
3461
3579
|
const merged = [...hydrated];
|
|
3462
3580
|
for (const incoming of cleanedUIMessages) {
|
|
3463
3581
|
if (!incoming.id)
|
|
3464
3582
|
continue;
|
|
3465
3583
|
const idx = merged.findIndex((m) => m.id === incoming.id);
|
|
3466
3584
|
if (idx !== -1) {
|
|
3467
|
-
merged[idx] = incoming;
|
|
3585
|
+
merged[idx] = mergeIncomingIntoHydrated(merged[idx], incoming);
|
|
3468
3586
|
}
|
|
3469
3587
|
}
|
|
3470
3588
|
accumulatedUIMessages = merged;
|
|
3471
3589
|
accumulatedMessages = await toModelMessages(merged);
|
|
3472
3590
|
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3473
|
-
// Track new messages for onTurnComplete.newUIMessages
|
|
3591
|
+
// Track new messages for onTurnComplete.newUIMessages.
|
|
3592
|
+
// Only push for genuinely new ids — HITL continuations
|
|
3593
|
+
// whose incoming wire id matches an existing entry are
|
|
3594
|
+
// state advances on an old message, not new messages.
|
|
3595
|
+
// We compare against `previouslyKnownMessageIds`
|
|
3596
|
+
// captured BEFORE hydration, not against `hydrated`:
|
|
3597
|
+
// the canonical hydrate pattern pushes the incoming
|
|
3598
|
+
// user message into the persisted chain and returns
|
|
3599
|
+
// it, so the new id IS in `hydrated`, which would
|
|
3600
|
+
// wrongly drop every fresh user turn from
|
|
3601
|
+
// `newUIMessages`. The non-hydrate branch below has
|
|
3602
|
+
// the same "push only on append" semantic via its
|
|
3603
|
+
// own append-vs-replace path.
|
|
3474
3604
|
if (currentWirePayload.trigger === "submit-message" &&
|
|
3475
3605
|
cleanedUIMessages.length > 0) {
|
|
3476
3606
|
const lastUI = cleanedUIMessages[cleanedUIMessages.length - 1];
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
if (
|
|
3480
|
-
|
|
3607
|
+
const matchedExisting = lastUI.id !== undefined &&
|
|
3608
|
+
previouslyKnownMessageIds.has(lastUI.id);
|
|
3609
|
+
if (!matchedExisting) {
|
|
3610
|
+
turnNewUIMessages.push(lastUI);
|
|
3611
|
+
const lastModel = (await toModelMessages([lastUI]))[0];
|
|
3612
|
+
if (lastModel)
|
|
3613
|
+
turnNewModelMessages.push(lastModel);
|
|
3614
|
+
}
|
|
3481
3615
|
}
|
|
3482
3616
|
}
|
|
3483
3617
|
else {
|
|
@@ -3503,15 +3637,17 @@ function chatAgent(options) {
|
|
|
3503
3637
|
else if (cleanedUIMessages.length > 0) {
|
|
3504
3638
|
// Submit-message (and the special-cased
|
|
3505
3639
|
// handover-prepare → submit-message rewrite earlier in
|
|
3506
|
-
// this scope):
|
|
3507
|
-
//
|
|
3640
|
+
// this scope): merge-or-append for the single delta
|
|
3641
|
+
// message.
|
|
3508
3642
|
//
|
|
3509
3643
|
// Tool approval responses arrive as a single assistant
|
|
3510
3644
|
// message whose id collides with the existing assistant
|
|
3511
|
-
// in the accumulator — we
|
|
3512
|
-
//
|
|
3513
|
-
//
|
|
3514
|
-
// `
|
|
3645
|
+
// in the accumulator — we merge the resolved tool-part
|
|
3646
|
+
// resolutions onto the existing entry, keeping text,
|
|
3647
|
+
// reasoning, and tool `input` from the prior snapshot.
|
|
3648
|
+
// The fallback for HITL `addToolOutput` continuations
|
|
3649
|
+
// where AI SDK regenerates the id (TRI-9137) still
|
|
3650
|
+
// applies via `rewriteIncomingIdViaToolCallMap`.
|
|
3515
3651
|
let replaced = false;
|
|
3516
3652
|
for (const raw of cleanedUIMessages) {
|
|
3517
3653
|
let incoming = raw;
|
|
@@ -3524,7 +3660,7 @@ function chatAgent(options) {
|
|
|
3524
3660
|
}
|
|
3525
3661
|
}
|
|
3526
3662
|
if (idx !== -1) {
|
|
3527
|
-
accumulatedUIMessages[idx] = incoming;
|
|
3663
|
+
accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(accumulatedUIMessages[idx], incoming);
|
|
3528
3664
|
replaced = true;
|
|
3529
3665
|
}
|
|
3530
3666
|
else {
|