@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.
Files changed (57) hide show
  1. package/dist/commonjs/v3/ai-shared.d.ts +70 -0
  2. package/dist/commonjs/v3/ai-shared.js +144 -0
  3. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  4. package/dist/commonjs/v3/ai.d.ts +21 -5
  5. package/dist/commonjs/v3/ai.js +153 -17
  6. package/dist/commonjs/v3/ai.js.map +1 -1
  7. package/dist/commonjs/v3/auth.d.ts +1 -0
  8. package/dist/commonjs/v3/auth.js +1 -0
  9. package/dist/commonjs/v3/auth.js.map +1 -1
  10. package/dist/commonjs/v3/chat-client.js +8 -3
  11. package/dist/commonjs/v3/chat-client.js.map +1 -1
  12. package/dist/commonjs/v3/chat.js +10 -2
  13. package/dist/commonjs/v3/chat.js.map +1 -1
  14. package/dist/commonjs/v3/index.d.ts +1 -0
  15. package/dist/commonjs/v3/index.js +3 -1
  16. package/dist/commonjs/v3/index.js.map +1 -1
  17. package/dist/commonjs/v3/shared.js +13 -7
  18. package/dist/commonjs/v3/shared.js.map +1 -1
  19. package/dist/commonjs/v3/triggerClient.d.ts +45 -0
  20. package/dist/commonjs/v3/triggerClient.js +104 -0
  21. package/dist/commonjs/v3/triggerClient.js.map +1 -0
  22. package/dist/commonjs/v3/triggerClient.test.d.ts +1 -0
  23. package/dist/commonjs/v3/triggerClient.test.js +226 -0
  24. package/dist/commonjs/v3/triggerClient.test.js.map +1 -0
  25. package/dist/commonjs/v3/triggerClient.types.test.d.ts +1 -0
  26. package/dist/commonjs/v3/triggerClient.types.test.js +113 -0
  27. package/dist/commonjs/v3/triggerClient.types.test.js.map +1 -0
  28. package/dist/commonjs/version.js +1 -1
  29. package/dist/esm/v3/ai-shared.d.ts +70 -0
  30. package/dist/esm/v3/ai-shared.js +142 -0
  31. package/dist/esm/v3/ai-shared.js.map +1 -1
  32. package/dist/esm/v3/ai.d.ts +21 -5
  33. package/dist/esm/v3/ai.js +152 -17
  34. package/dist/esm/v3/ai.js.map +1 -1
  35. package/dist/esm/v3/auth.d.ts +1 -0
  36. package/dist/esm/v3/auth.js +1 -0
  37. package/dist/esm/v3/auth.js.map +1 -1
  38. package/dist/esm/v3/chat-client.js +8 -3
  39. package/dist/esm/v3/chat-client.js.map +1 -1
  40. package/dist/esm/v3/chat.js +10 -2
  41. package/dist/esm/v3/chat.js.map +1 -1
  42. package/dist/esm/v3/index.d.ts +1 -0
  43. package/dist/esm/v3/index.js +1 -0
  44. package/dist/esm/v3/index.js.map +1 -1
  45. package/dist/esm/v3/shared.js +14 -8
  46. package/dist/esm/v3/shared.js.map +1 -1
  47. package/dist/esm/v3/triggerClient.d.ts +45 -0
  48. package/dist/esm/v3/triggerClient.js +77 -0
  49. package/dist/esm/v3/triggerClient.js.map +1 -0
  50. package/dist/esm/v3/triggerClient.test.d.ts +1 -0
  51. package/dist/esm/v3/triggerClient.test.js +224 -0
  52. package/dist/esm/v3/triggerClient.test.js.map +1 -0
  53. package/dist/esm/v3/triggerClient.types.test.d.ts +1 -0
  54. package/dist/esm/v3/triggerClient.types.test.js +88 -0
  55. package/dist/esm/v3/triggerClient.types.test.js.map +1 -0
  56. package/dist/esm/version.js +1 -1
  57. 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;;;AAKH;;;GAGG;AACU,QAAA,6BAA6B,GAAG,+BAAwC,CAAC"}
1
+ {"version":3,"file":"ai-shared.js","sourceRoot":"","sources":["../../../src/v3/ai-shared.ts"],"names":[],"mappings":"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"}
@@ -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
- /** The incoming UI messages for this turn (after cleanup of aborted tool parts). */
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` to catch
1538
- * malformed messages from storage or untrusted input before they reach the model.
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
- * return validateUIMessages({ messages, tools: chatTools });
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 });
@@ -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
- // Auto-merge tool approval updates: if any incoming wire message
3459
- // has an ID that matches a hydrated message, replace it. This makes
3460
- // tool approvals work transparently with backend hydration.
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
- turnNewUIMessages.push(lastUI);
3478
- const lastModel = (await toModelMessages([lastUI]))[0];
3479
- if (lastModel)
3480
- turnNewModelMessages.push(lastModel);
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): append-or-replace-by-id for the single
3507
- // delta message.
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 replace by id. The fallback
3512
- // for HITL `addToolOutput` continuations where AI SDK
3513
- // regenerates the id (TRI-9137) still applies via
3514
- // `rewriteIncomingIdViaToolCallMap`.
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 {