@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
@@ -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 });
package/dist/esm/v3/ai.js CHANGED
@@ -1521,6 +1521,107 @@ function extractNewToolResultsFromHistory(message, messages) {
1521
1521
  }
1522
1522
  return out;
1523
1523
  }
1524
+ /**
1525
+ * Per-turn merge of an incoming wire `UIMessage` onto the matching entry
1526
+ * a `hydrateMessages` hook (or the default accumulator) provides. Used
1527
+ * to fold tool-state advances from the client into the agent's
1528
+ * authoritative chain without trusting the wire copy for fields the
1529
+ * LLM consumes.
1530
+ *
1531
+ * `hydrated` is treated as the source of truth for everything outside
1532
+ * tool-state advancement: text, reasoning blobs, provider metadata,
1533
+ * and tool `input` all stay as hydrated had them. We only overlay
1534
+ * tool parts whose incoming state is wire-advanced — `output-available`
1535
+ * / `output-error` (HITL `addToolOutput`) or `approval-responded` /
1536
+ * `output-denied` (approval flow) — and only the corresponding
1537
+ * resolution fields (`output` / `errorText` / `approval`). Hydrated
1538
+ * `input` and everything else stay put.
1539
+ *
1540
+ * Without this, a slim wire copy (which `TriggerChatTransport` /
1541
+ * `AgentChat.sendRaw` ship by default on HITL continuations) would
1542
+ * clobber the hydrated assistant — the next LLM call would receive a
1543
+ * tool call with no `input` and 4xx.
1544
+ *
1545
+ * @internal
1546
+ */
1547
+ function mergeIncomingIntoHydrated(hydrated, incoming) {
1548
+ const incomingAdvancedByCallId = new Map();
1549
+ for (const part of (incoming.parts ?? [])) {
1550
+ if (!isToolUIPart(part))
1551
+ continue;
1552
+ const toolCallId = part.toolCallId;
1553
+ if (typeof toolCallId !== "string" || toolCallId.length === 0)
1554
+ continue;
1555
+ if (!isWireAdvanceableToolState(part.state))
1556
+ continue;
1557
+ incomingAdvancedByCallId.set(toolCallId, part);
1558
+ }
1559
+ if (incomingAdvancedByCallId.size === 0)
1560
+ return hydrated;
1561
+ let mutated = false;
1562
+ const hydratedParts = (hydrated.parts ?? []);
1563
+ const mergedParts = hydratedParts.map((part) => {
1564
+ if (!isToolUIPart(part))
1565
+ return part;
1566
+ const toolCallId = part.toolCallId;
1567
+ if (typeof toolCallId !== "string" || toolCallId.length === 0)
1568
+ return part;
1569
+ const incomingPart = incomingAdvancedByCallId.get(toolCallId);
1570
+ if (!incomingPart)
1571
+ return part;
1572
+ // Terminal hydrated states (`output-available`, `output-error`,
1573
+ // `output-denied`) are authoritative — never regressed by a stale
1574
+ // wire arrival (replay, retry, out-of-order). `output-denied`
1575
+ // matters here because the wire's `approval-responded` could
1576
+ // otherwise overwrite a hydrated denial back to a non-terminal
1577
+ // state.
1578
+ if (isResolvedToolState(part.state) || part.state === "output-denied") {
1579
+ return part;
1580
+ }
1581
+ // Same state on both sides — no progression to apply.
1582
+ if (part.state === incomingPart.state)
1583
+ return part;
1584
+ mutated = true;
1585
+ if (incomingPart.state === "output-available") {
1586
+ return {
1587
+ ...part,
1588
+ state: incomingPart.state,
1589
+ output: incomingPart.output,
1590
+ ...(incomingPart.approval !== undefined ? { approval: incomingPart.approval } : {}),
1591
+ };
1592
+ }
1593
+ if (incomingPart.state === "output-error") {
1594
+ return {
1595
+ ...part,
1596
+ state: incomingPart.state,
1597
+ errorText: incomingPart.errorText,
1598
+ ...(incomingPart.approval !== undefined ? { approval: incomingPart.approval } : {}),
1599
+ };
1600
+ }
1601
+ // approval-responded / output-denied — overlay state + approval.
1602
+ return {
1603
+ ...part,
1604
+ state: incomingPart.state,
1605
+ ...(incomingPart.approval !== undefined ? { approval: incomingPart.approval } : {}),
1606
+ };
1607
+ });
1608
+ if (!mutated)
1609
+ return hydrated;
1610
+ return { ...hydrated, parts: mergedParts };
1611
+ }
1612
+ /**
1613
+ * Mirror of `slimSubmitMessageForWire`'s predicate. Kept here so the
1614
+ * agent runtime doesn't have to import from `ai-shared.ts` for a
1615
+ * one-liner. See that file for the full state-machine docs.
1616
+ *
1617
+ * @internal
1618
+ */
1619
+ function isWireAdvanceableToolState(state) {
1620
+ return (state === "output-available" ||
1621
+ state === "output-error" ||
1622
+ state === "approval-responded" ||
1623
+ state === "output-denied");
1624
+ }
1524
1625
  /**
1525
1626
  * Imperative API for reading and modifying the accumulated message history.
1526
1627
  *
@@ -1648,7 +1749,7 @@ const chatAgentCompactionKey = locals.create("chat.agentCompaction");
1648
1749
  // React hooks (`@trigger.dev/sdk/chat/react`) can import it without
1649
1750
  // dragging `ai.ts` into the browser graph. Re-exported here so
1650
1751
  // `@trigger.dev/sdk/ai` consumers still see it.
1651
- export { PENDING_MESSAGE_INJECTED_TYPE } from "./ai-shared.js";
1752
+ export { PENDING_MESSAGE_INJECTED_TYPE, upsertIncomingMessage } from "./ai-shared.js";
1652
1753
  import { PENDING_MESSAGE_INJECTED_TYPE } from "./ai-shared.js";
1653
1754
  /** @internal */
1654
1755
  const chatPendingMessagesKey = locals.create("chat.pendingMessages");
@@ -3415,6 +3516,17 @@ function chatAgent(options) {
3415
3516
  }));
3416
3517
  }
3417
3518
  if (hydrateMessages) {
3519
+ // Snapshot the ids the accumulator knew BEFORE this
3520
+ // turn ran — used below to decide whether an
3521
+ // incoming wire message is genuinely new or just a
3522
+ // state advance on an existing entry. We can't use
3523
+ // the post-`hydrateMessages` array for this because
3524
+ // the canonical hook pattern pushes the incoming
3525
+ // user message into the persisted chain and
3526
+ // returns it.
3527
+ const previouslyKnownMessageIds = new Set(accumulatedUIMessages
3528
+ .map((m) => m.id)
3529
+ .filter((id) => typeof id === "string"));
3418
3530
  // Backend hydration: load the full message history from the user's
3419
3531
  // backend, replacing the built-in accumulator entirely. With slim
3420
3532
  // wire, `incomingMessages` is consistently 0-or-1-length — what
@@ -3441,29 +3553,50 @@ function chatAgent(options) {
3441
3553
  "chat.incoming_messages.count": cleanedUIMessages.length,
3442
3554
  },
3443
3555
  });
3444
- // Auto-merge tool approval updates: if any incoming wire message
3445
- // has an ID that matches a hydrated message, replace it. This makes
3446
- // tool approvals work transparently with backend hydration.
3556
+ // Per-turn merge of incoming wire messages onto the hydrated
3557
+ // chain. Hydrated stays authoritative for text, reasoning
3558
+ // blobs, provider metadata, and tool `input`; we only
3559
+ // overlay tool-part state/output/errorText for tool calls
3560
+ // the wire copy has just resolved. Apps that slim the wire
3561
+ // copy to fit the .in/append cap (or drop fields they
3562
+ // re-source from their own DB) get the hydrated copy
3563
+ // through unchanged.
3447
3564
  const merged = [...hydrated];
3448
3565
  for (const incoming of cleanedUIMessages) {
3449
3566
  if (!incoming.id)
3450
3567
  continue;
3451
3568
  const idx = merged.findIndex((m) => m.id === incoming.id);
3452
3569
  if (idx !== -1) {
3453
- merged[idx] = incoming;
3570
+ merged[idx] = mergeIncomingIntoHydrated(merged[idx], incoming);
3454
3571
  }
3455
3572
  }
3456
3573
  accumulatedUIMessages = merged;
3457
3574
  accumulatedMessages = await toModelMessages(merged);
3458
3575
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
3459
- // Track new messages for onTurnComplete.newUIMessages
3576
+ // Track new messages for onTurnComplete.newUIMessages.
3577
+ // Only push for genuinely new ids — HITL continuations
3578
+ // whose incoming wire id matches an existing entry are
3579
+ // state advances on an old message, not new messages.
3580
+ // We compare against `previouslyKnownMessageIds`
3581
+ // captured BEFORE hydration, not against `hydrated`:
3582
+ // the canonical hydrate pattern pushes the incoming
3583
+ // user message into the persisted chain and returns
3584
+ // it, so the new id IS in `hydrated`, which would
3585
+ // wrongly drop every fresh user turn from
3586
+ // `newUIMessages`. The non-hydrate branch below has
3587
+ // the same "push only on append" semantic via its
3588
+ // own append-vs-replace path.
3460
3589
  if (currentWirePayload.trigger === "submit-message" &&
3461
3590
  cleanedUIMessages.length > 0) {
3462
3591
  const lastUI = cleanedUIMessages[cleanedUIMessages.length - 1];
3463
- turnNewUIMessages.push(lastUI);
3464
- const lastModel = (await toModelMessages([lastUI]))[0];
3465
- if (lastModel)
3466
- turnNewModelMessages.push(lastModel);
3592
+ const matchedExisting = lastUI.id !== undefined &&
3593
+ previouslyKnownMessageIds.has(lastUI.id);
3594
+ if (!matchedExisting) {
3595
+ turnNewUIMessages.push(lastUI);
3596
+ const lastModel = (await toModelMessages([lastUI]))[0];
3597
+ if (lastModel)
3598
+ turnNewModelMessages.push(lastModel);
3599
+ }
3467
3600
  }
3468
3601
  }
3469
3602
  else {
@@ -3489,15 +3622,17 @@ function chatAgent(options) {
3489
3622
  else if (cleanedUIMessages.length > 0) {
3490
3623
  // Submit-message (and the special-cased
3491
3624
  // handover-prepare → submit-message rewrite earlier in
3492
- // this scope): append-or-replace-by-id for the single
3493
- // delta message.
3625
+ // this scope): merge-or-append for the single delta
3626
+ // message.
3494
3627
  //
3495
3628
  // Tool approval responses arrive as a single assistant
3496
3629
  // message whose id collides with the existing assistant
3497
- // in the accumulator — we replace by id. The fallback
3498
- // for HITL `addToolOutput` continuations where AI SDK
3499
- // regenerates the id (TRI-9137) still applies via
3500
- // `rewriteIncomingIdViaToolCallMap`.
3630
+ // in the accumulator — we merge the resolved tool-part
3631
+ // resolutions onto the existing entry, keeping text,
3632
+ // reasoning, and tool `input` from the prior snapshot.
3633
+ // The fallback for HITL `addToolOutput` continuations
3634
+ // where AI SDK regenerates the id (TRI-9137) still
3635
+ // applies via `rewriteIncomingIdViaToolCallMap`.
3501
3636
  let replaced = false;
3502
3637
  for (const raw of cleanedUIMessages) {
3503
3638
  let incoming = raw;
@@ -3510,7 +3645,7 @@ function chatAgent(options) {
3510
3645
  }
3511
3646
  }
3512
3647
  if (idx !== -1) {
3513
- accumulatedUIMessages[idx] = incoming;
3648
+ accumulatedUIMessages[idx] = mergeIncomingIntoHydrated(accumulatedUIMessages[idx], incoming);
3514
3649
  replaced = true;
3515
3650
  }
3516
3651
  else {