@trigger.dev/sdk 0.0.0-chat-prerelease-20260506093419 → 0.0.0-chat-prerelease-20260507131256

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.
@@ -263,6 +263,28 @@ export type ChatTurnUsage = LanguageModelUsage;
263
263
  * and converts to `ModelMessage[]` internally.
264
264
  */
265
265
  declare function setChatMessages<TUIM extends UIMessage = UIMessage>(uiMessages: TUIM[]): void;
266
+ /**
267
+ * A tool call surfaced by `chat.history.getPendingToolCalls()` /
268
+ * `getResolvedToolCalls()`. Identifies the call by its `toolCallId` plus
269
+ * the `messageId` of the assistant message that hosts it, so callers can
270
+ * locate the part precisely without re-walking the chain.
271
+ */
272
+ export type ChatToolCallRef = {
273
+ toolCallId: string;
274
+ toolName: string;
275
+ messageId: string;
276
+ };
277
+ /**
278
+ * A new tool result surfaced by `chat.history.extractNewToolResults()`.
279
+ * `errorText` is set iff the part is in `output-error` state; otherwise
280
+ * `output` carries the resolved value.
281
+ */
282
+ export type ChatNewToolResult = {
283
+ toolCallId: string;
284
+ toolName: string;
285
+ output: unknown;
286
+ errorText?: string;
287
+ };
266
288
  /** State stored in locals during prepareStep compaction. */
267
289
  interface CompactionState {
268
290
  summary: string;
@@ -2493,6 +2515,50 @@ export declare const chat: {
2493
2515
  history: {
2494
2516
  /** Read the current accumulated UI messages (copy). */
2495
2517
  all(): UIMessage[];
2518
+ /**
2519
+ * Read the current chain as an ordered `UIMessage[]`. Identical to
2520
+ * `all()`; use whichever name reads better in context.
2521
+ */
2522
+ getChain(): UIMessage[];
2523
+ /**
2524
+ * Find a message by id. Returns `undefined` if no message with that id
2525
+ * is present in the current chain.
2526
+ */
2527
+ findMessage(messageId: string): UIMessage | undefined;
2528
+ /**
2529
+ * Tool calls on the *most recent* assistant message that are still in
2530
+ * `input-available` state (waiting on an `addToolOutput` answer). The
2531
+ * scan walks back from the tail and stops at the first assistant
2532
+ * message it finds, so a trailing user message does not change the
2533
+ * result — pending tool calls remain pending until they're resolved
2534
+ * on that assistant or the assistant is removed.
2535
+ *
2536
+ * Use this to gate fresh user turns or actions during HITL flows: if
2537
+ * `getPendingToolCalls().length > 0`, an `addToolOutput` is expected.
2538
+ *
2539
+ * Returns `[]` if there is no assistant message yet, or if the most
2540
+ * recent assistant has no pending tool calls.
2541
+ *
2542
+ * Approval flows (`approval-requested` / `approval-responded` states)
2543
+ * are not surfaced here. Those are about the user authorizing a tool
2544
+ * to run; "pending" is about the user *answering* a tool call.
2545
+ */
2546
+ getPendingToolCalls(): ChatToolCallRef[];
2547
+ /**
2548
+ * Tool calls across the chain with a final result (`output-available`
2549
+ * or `output-error`). Use this to dedup re-saves when the AI SDK
2550
+ * resends an assistant message with progressively more answered parts.
2551
+ */
2552
+ getResolvedToolCalls(): ChatToolCallRef[];
2553
+ /**
2554
+ * Pure helper: returns the tool parts in `message` whose results are
2555
+ * not already represented in the current chain. Use this when
2556
+ * persisting tool results to your own store: each call surfaces only
2557
+ * the *new* answers, so writes stay idempotent across re-streams.
2558
+ * Duplicate `toolCallId`s within `message` itself are also collapsed
2559
+ * to a single entry.
2560
+ */
2561
+ extractNewToolResults(message: UIMessage): ChatNewToolResult[];
2496
2562
  /** Replace all accumulated messages. Same as `chat.setMessages()`. */
2497
2563
  set(messages: UIMessage[]): void;
2498
2564
  /** Remove a specific message by ID. */
@@ -807,6 +807,54 @@ const chatStopControllerKey = locals_js_1.locals.create("chat.stopController");
807
807
  const chatUIStreamStaticKey = locals_js_1.locals.create("chat.uiMessageStreamOptions.static");
808
808
  /** Per-turn UIMessageStream options, set via chat.setUIMessageStreamOptions(). @internal */
809
809
  const chatUIStreamPerTurnKey = locals_js_1.locals.create("chat.uiMessageStreamOptions.perTurn");
810
+ /**
811
+ * Run-scoped `toolCallId → assistant messageId` map. Records the head
812
+ * assistant id whenever the accumulator absorbs an assistant message
813
+ * containing tool parts. Used as a fallback in the id-merge for
814
+ * incoming tool-answer messages — if the AI SDK regenerates the
815
+ * assistant id on a HITL `addToolOutput` resume, we look up the
816
+ * original head id by `toolCallId` and rewrite it before the merge.
817
+ *
818
+ * Customer-side workaround for the same case is documented in Arena
819
+ * AI's chat-agent task; lifting it into the SDK so customers don't
820
+ * have to. See TRI-9137.
821
+ * @internal
822
+ */
823
+ const chatToolCallToMessageIdKey = locals_js_1.locals.create("chat.toolCallToMessageId");
824
+ function recordToolCallIdsFromMessage(message) {
825
+ if (!message || message.role !== "assistant" || !message.id)
826
+ return;
827
+ let map = locals_js_1.locals.get(chatToolCallToMessageIdKey);
828
+ if (!map) {
829
+ map = new Map();
830
+ locals_js_1.locals.set(chatToolCallToMessageIdKey, map);
831
+ }
832
+ for (const part of message.parts ?? []) {
833
+ if (typeof part !== "object" || part == null)
834
+ continue;
835
+ const toolCallId = part.toolCallId;
836
+ if (typeof toolCallId === "string" && toolCallId.length > 0) {
837
+ map.set(toolCallId, message.id);
838
+ }
839
+ }
840
+ }
841
+ function rewriteIncomingIdViaToolCallMap(incoming) {
842
+ const map = locals_js_1.locals.get(chatToolCallToMessageIdKey);
843
+ if (!map || map.size === 0)
844
+ return incoming;
845
+ for (const part of incoming.parts ?? []) {
846
+ if (typeof part !== "object" || part == null)
847
+ continue;
848
+ const toolCallId = part.toolCallId;
849
+ if (typeof toolCallId !== "string" || toolCallId.length === 0)
850
+ continue;
851
+ const headId = map.get(toolCallId);
852
+ if (headId && headId !== incoming.id) {
853
+ return { ...incoming, id: headId };
854
+ }
855
+ }
856
+ return incoming;
857
+ }
810
858
  function emptyUsage() {
811
859
  return {
812
860
  inputTokens: undefined,
@@ -872,19 +920,179 @@ function getChatHistoryState() {
872
920
  return locals_js_1.locals.get(chatCurrentUIMessagesKey) ?? [];
873
921
  }
874
922
  /**
875
- * Imperative API for modifying the accumulated message history.
923
+ * Tool parts that are "done" either succeeded with a value or failed
924
+ * with an error. Excludes pending (`input-streaming`/`input-available`)
925
+ * and approval (`approval-requested`/`approval-responded`) states.
926
+ * @internal
927
+ */
928
+ function isResolvedToolState(state) {
929
+ return state === "output-available" || state === "output-error";
930
+ }
931
+ /** @internal */
932
+ function isPendingToolState(state) {
933
+ return state === "input-available";
934
+ }
935
+ /**
936
+ * Walk an assistant message and yield each tool part with its callId,
937
+ * name, and state. Skips non-assistant messages and non-tool parts.
938
+ * @internal
939
+ */
940
+ function* iterateToolParts(message) {
941
+ if (message.role !== "assistant")
942
+ return;
943
+ for (const part of (message.parts ?? [])) {
944
+ if (!(0, ai_1.isToolUIPart)(part))
945
+ continue;
946
+ const toolCallId = part.toolCallId;
947
+ if (typeof toolCallId !== "string" || toolCallId.length === 0)
948
+ continue;
949
+ yield {
950
+ part,
951
+ toolCallId,
952
+ toolName: (0, ai_1.getToolName)(part),
953
+ state: part.state,
954
+ };
955
+ }
956
+ }
957
+ /**
958
+ * Tool parts on the *leaf* assistant message that are still waiting on
959
+ * an answer (`input-available` state). Used to gate fresh user turns
960
+ * during HITL flows.
961
+ * @internal
962
+ */
963
+ function getPendingToolCallsFromHistory(messages) {
964
+ for (let i = messages.length - 1; i >= 0; i--) {
965
+ const msg = messages[i];
966
+ if (msg.role !== "assistant")
967
+ continue;
968
+ const pending = [];
969
+ for (const { toolCallId, toolName, state } of iterateToolParts(msg)) {
970
+ if (isPendingToolState(state)) {
971
+ pending.push({ toolCallId, toolName, messageId: msg.id });
972
+ }
973
+ }
974
+ return pending;
975
+ }
976
+ return [];
977
+ }
978
+ /**
979
+ * All tool parts across the chain that have already produced an output
980
+ * (`output-available` or `output-error`). Used to dedup re-saves when
981
+ * the AI SDK resends an assistant with progressively more answered
982
+ * parts.
983
+ * @internal
984
+ */
985
+ function getResolvedToolCallsFromHistory(messages) {
986
+ const out = [];
987
+ for (const msg of messages) {
988
+ for (const { toolCallId, toolName, state } of iterateToolParts(msg)) {
989
+ if (isResolvedToolState(state)) {
990
+ out.push({ toolCallId, toolName, messageId: msg.id });
991
+ }
992
+ }
993
+ }
994
+ return out;
995
+ }
996
+ /**
997
+ * Pure helper: tool parts in `message` that have a fresh result not
998
+ * already represented by the resolved toolCallIds in `messages`. The
999
+ * `errorText` field is present only for `output-error` parts.
1000
+ *
1001
+ * Within a single `message`, duplicate `toolCallId`s emit only once
1002
+ * (first occurrence wins). This guards against malformed assistants
1003
+ * with repeated tool parts.
1004
+ * @internal
1005
+ */
1006
+ function extractNewToolResultsFromHistory(message, messages) {
1007
+ const resolved = new Set(getResolvedToolCallsFromHistory(messages).map((r) => r.toolCallId));
1008
+ const seen = new Set();
1009
+ const out = [];
1010
+ for (const { part, toolCallId, toolName, state } of iterateToolParts(message)) {
1011
+ if (!isResolvedToolState(state))
1012
+ continue;
1013
+ if (resolved.has(toolCallId))
1014
+ continue;
1015
+ if (seen.has(toolCallId))
1016
+ continue;
1017
+ seen.add(toolCallId);
1018
+ if (state === "output-error") {
1019
+ out.push({ toolCallId, toolName, output: undefined, errorText: part.errorText });
1020
+ }
1021
+ else {
1022
+ out.push({ toolCallId, toolName, output: part.output });
1023
+ }
1024
+ }
1025
+ return out;
1026
+ }
1027
+ /**
1028
+ * Imperative API for reading and modifying the accumulated message history.
876
1029
  *
877
1030
  * Mutations use the same deferred override mechanism as `chat.setMessages()`:
878
- * they are applied at lifecycle checkpoints (after hooks return).
1031
+ * they are applied at lifecycle checkpoints (after hooks return). Reads are
1032
+ * synchronous against the current accumulator state.
879
1033
  *
880
1034
  * Can be called from `onTurnStart`, `onBeforeTurnComplete`, `onTurnComplete`,
881
- * `run()`, or AI SDK tools.
1035
+ * `run()`, `onAction`, or AI SDK tools.
882
1036
  */
883
1037
  const chatHistory = {
884
1038
  /** Read the current accumulated UI messages (copy). */
885
1039
  all() {
886
1040
  return [...getChatHistoryState()];
887
1041
  },
1042
+ /**
1043
+ * Read the current chain as an ordered `UIMessage[]`. Identical to
1044
+ * `all()`; use whichever name reads better in context.
1045
+ */
1046
+ getChain() {
1047
+ return chatHistory.all();
1048
+ },
1049
+ /**
1050
+ * Find a message by id. Returns `undefined` if no message with that id
1051
+ * is present in the current chain.
1052
+ */
1053
+ findMessage(messageId) {
1054
+ return getChatHistoryState().find((m) => m.id === messageId);
1055
+ },
1056
+ /**
1057
+ * Tool calls on the *most recent* assistant message that are still in
1058
+ * `input-available` state (waiting on an `addToolOutput` answer). The
1059
+ * scan walks back from the tail and stops at the first assistant
1060
+ * message it finds, so a trailing user message does not change the
1061
+ * result — pending tool calls remain pending until they're resolved
1062
+ * on that assistant or the assistant is removed.
1063
+ *
1064
+ * Use this to gate fresh user turns or actions during HITL flows: if
1065
+ * `getPendingToolCalls().length > 0`, an `addToolOutput` is expected.
1066
+ *
1067
+ * Returns `[]` if there is no assistant message yet, or if the most
1068
+ * recent assistant has no pending tool calls.
1069
+ *
1070
+ * Approval flows (`approval-requested` / `approval-responded` states)
1071
+ * are not surfaced here. Those are about the user authorizing a tool
1072
+ * to run; "pending" is about the user *answering* a tool call.
1073
+ */
1074
+ getPendingToolCalls() {
1075
+ return getPendingToolCallsFromHistory(getChatHistoryState());
1076
+ },
1077
+ /**
1078
+ * Tool calls across the chain with a final result (`output-available`
1079
+ * or `output-error`). Use this to dedup re-saves when the AI SDK
1080
+ * resends an assistant message with progressively more answered parts.
1081
+ */
1082
+ getResolvedToolCalls() {
1083
+ return getResolvedToolCallsFromHistory(getChatHistoryState());
1084
+ },
1085
+ /**
1086
+ * Pure helper: returns the tool parts in `message` whose results are
1087
+ * not already represented in the current chain. Use this when
1088
+ * persisting tool results to your own store: each call surfaces only
1089
+ * the *new* answers, so writes stay idempotent across re-streams.
1090
+ * Duplicate `toolCallId`s within `message` itself are also collapsed
1091
+ * to a single entry.
1092
+ */
1093
+ extractNewToolResults(message) {
1094
+ return extractNewToolResultsFromHistory(message, getChatHistoryState());
1095
+ },
888
1096
  /** Replace all accumulated messages. Same as `chat.setMessages()`. */
889
1097
  set(messages) {
890
1098
  locals_js_1.locals.set(chatOverrideMessagesKey, messages);
@@ -2361,9 +2569,24 @@ function chatAgent(options) {
2361
2569
  // IDs match because we always pass generateMessageId + originalMessages
2362
2570
  // to toUIMessageStream, so the backend's start chunk carries the same
2363
2571
  // messageId that the frontend uses.
2572
+ //
2573
+ // Fallback for HITL `addToolOutput` continuations where the AI SDK
2574
+ // regenerates the assistant id (Arena AI report, TRI-9137): if the
2575
+ // id-match fails, look up the head messageId via toolCallId and
2576
+ // rewrite the incoming id before retrying. The mapping is
2577
+ // populated whenever an assistant containing tool parts lands in
2578
+ // the accumulator.
2364
2579
  let replaced = false;
2365
- for (const incoming of cleanedUIMessages) {
2366
- const idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
2580
+ for (const raw of cleanedUIMessages) {
2581
+ let incoming = raw;
2582
+ let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
2583
+ if (idx === -1) {
2584
+ const rewritten = rewriteIncomingIdViaToolCallMap(incoming);
2585
+ if (rewritten.id !== incoming.id) {
2586
+ incoming = rewritten;
2587
+ idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
2588
+ }
2589
+ }
2367
2590
  if (idx !== -1) {
2368
2591
  accumulatedUIMessages[idx] = incoming;
2369
2592
  replaced = true;
@@ -2372,6 +2595,7 @@ function chatAgent(options) {
2372
2595
  accumulatedUIMessages.push(incoming);
2373
2596
  turnNewUIMessages.push(incoming);
2374
2597
  }
2598
+ recordToolCallIdsFromMessage(incoming);
2375
2599
  }
2376
2600
  if (replaced) {
2377
2601
  // Reconvert all model messages since a replacement changes the structure
@@ -2773,6 +2997,12 @@ function chatAgent(options) {
2773
2997
  }
2774
2998
  turnNewUIMessages.push(capturedResponseMessage);
2775
2999
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
3000
+ // Record toolCallId → head messageId so a HITL
3001
+ // continuation next turn can recover the head id
3002
+ // even if the AI SDK regenerates it. See
3003
+ // `chatToolCallToMessageIdKey` for the full
3004
+ // rationale (TRI-9137).
3005
+ recordToolCallIdsFromMessage(capturedResponseMessage);
2776
3006
  try {
2777
3007
  const responseModelMessages = await toModelMessages([
2778
3008
  stripProviderMetadata(capturedResponseMessage),