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

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,40 @@ 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[]`. Alias for `all()` —
2520
+ * use this when working alongside parent-aware APIs (TRI-9120) where
2521
+ * "chain" disambiguates from "graph".
2522
+ */
2523
+ getChain(): UIMessage[];
2524
+ /**
2525
+ * Find a message by id. Returns `undefined` if no message with that id
2526
+ * is present in the current chain.
2527
+ */
2528
+ findMessage(messageId: string): UIMessage | undefined;
2529
+ /**
2530
+ * Tool calls on the leaf assistant message still waiting on an answer
2531
+ * (`input-available` state). Use this to gate fresh user turns during
2532
+ * HITL flows: if `getPendingToolCalls().length > 0`, an `addToolOutput`
2533
+ * is expected before any new user message.
2534
+ *
2535
+ * Returns `[]` if there is no assistant message yet, or if the leaf
2536
+ * assistant has no pending tool calls.
2537
+ */
2538
+ getPendingToolCalls(): ChatToolCallRef[];
2539
+ /**
2540
+ * Tool calls across the chain with a final result (`output-available`
2541
+ * or `output-error`). Use this to dedup re-saves when the AI SDK
2542
+ * resends an assistant message with progressively more answered parts.
2543
+ */
2544
+ getResolvedToolCalls(): ChatToolCallRef[];
2545
+ /**
2546
+ * Pure helper: returns the tool parts in `message` whose results are
2547
+ * not already represented in the current chain. Use this when
2548
+ * persisting tool results to your own store: each call surfaces only
2549
+ * the *new* answers, so writes stay idempotent across re-streams.
2550
+ */
2551
+ extractNewToolResults(message: UIMessage): ChatNewToolResult[];
2496
2552
  /** Replace all accumulated messages. Same as `chat.setMessages()`. */
2497
2553
  set(messages: UIMessage[]): void;
2498
2554
  /** 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,161 @@ 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
+ * @internal
1001
+ */
1002
+ function extractNewToolResultsFromHistory(message, messages) {
1003
+ const resolved = new Set(getResolvedToolCallsFromHistory(messages).map((r) => r.toolCallId));
1004
+ const out = [];
1005
+ for (const { part, toolCallId, toolName, state } of iterateToolParts(message)) {
1006
+ if (!isResolvedToolState(state))
1007
+ continue;
1008
+ if (resolved.has(toolCallId))
1009
+ continue;
1010
+ if (state === "output-error") {
1011
+ out.push({ toolCallId, toolName, output: undefined, errorText: part.errorText });
1012
+ }
1013
+ else {
1014
+ out.push({ toolCallId, toolName, output: part.output });
1015
+ }
1016
+ }
1017
+ return out;
1018
+ }
1019
+ /**
1020
+ * Imperative API for reading and modifying the accumulated message history.
876
1021
  *
877
1022
  * Mutations use the same deferred override mechanism as `chat.setMessages()`:
878
- * they are applied at lifecycle checkpoints (after hooks return).
1023
+ * they are applied at lifecycle checkpoints (after hooks return). Reads are
1024
+ * synchronous against the current accumulator state.
879
1025
  *
880
1026
  * Can be called from `onTurnStart`, `onBeforeTurnComplete`, `onTurnComplete`,
881
- * `run()`, or AI SDK tools.
1027
+ * `run()`, `onAction`, or AI SDK tools.
882
1028
  */
883
1029
  const chatHistory = {
884
1030
  /** Read the current accumulated UI messages (copy). */
885
1031
  all() {
886
1032
  return [...getChatHistoryState()];
887
1033
  },
1034
+ /**
1035
+ * Read the current chain as an ordered `UIMessage[]`. Alias for `all()` —
1036
+ * use this when working alongside parent-aware APIs (TRI-9120) where
1037
+ * "chain" disambiguates from "graph".
1038
+ */
1039
+ getChain() {
1040
+ return [...getChatHistoryState()];
1041
+ },
1042
+ /**
1043
+ * Find a message by id. Returns `undefined` if no message with that id
1044
+ * is present in the current chain.
1045
+ */
1046
+ findMessage(messageId) {
1047
+ return getChatHistoryState().find((m) => m.id === messageId);
1048
+ },
1049
+ /**
1050
+ * Tool calls on the leaf assistant message still waiting on an answer
1051
+ * (`input-available` state). Use this to gate fresh user turns during
1052
+ * HITL flows: if `getPendingToolCalls().length > 0`, an `addToolOutput`
1053
+ * is expected before any new user message.
1054
+ *
1055
+ * Returns `[]` if there is no assistant message yet, or if the leaf
1056
+ * assistant has no pending tool calls.
1057
+ */
1058
+ getPendingToolCalls() {
1059
+ return getPendingToolCallsFromHistory(getChatHistoryState());
1060
+ },
1061
+ /**
1062
+ * Tool calls across the chain with a final result (`output-available`
1063
+ * or `output-error`). Use this to dedup re-saves when the AI SDK
1064
+ * resends an assistant message with progressively more answered parts.
1065
+ */
1066
+ getResolvedToolCalls() {
1067
+ return getResolvedToolCallsFromHistory(getChatHistoryState());
1068
+ },
1069
+ /**
1070
+ * Pure helper: returns the tool parts in `message` whose results are
1071
+ * not already represented in the current chain. Use this when
1072
+ * persisting tool results to your own store: each call surfaces only
1073
+ * the *new* answers, so writes stay idempotent across re-streams.
1074
+ */
1075
+ extractNewToolResults(message) {
1076
+ return extractNewToolResultsFromHistory(message, getChatHistoryState());
1077
+ },
888
1078
  /** Replace all accumulated messages. Same as `chat.setMessages()`. */
889
1079
  set(messages) {
890
1080
  locals_js_1.locals.set(chatOverrideMessagesKey, messages);
@@ -2361,9 +2551,24 @@ function chatAgent(options) {
2361
2551
  // IDs match because we always pass generateMessageId + originalMessages
2362
2552
  // to toUIMessageStream, so the backend's start chunk carries the same
2363
2553
  // messageId that the frontend uses.
2554
+ //
2555
+ // Fallback for HITL `addToolOutput` continuations where the AI SDK
2556
+ // regenerates the assistant id (Arena AI report, TRI-9137): if the
2557
+ // id-match fails, look up the head messageId via toolCallId and
2558
+ // rewrite the incoming id before retrying. The mapping is
2559
+ // populated whenever an assistant containing tool parts lands in
2560
+ // the accumulator.
2364
2561
  let replaced = false;
2365
- for (const incoming of cleanedUIMessages) {
2366
- const idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
2562
+ for (const raw of cleanedUIMessages) {
2563
+ let incoming = raw;
2564
+ let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
2565
+ if (idx === -1) {
2566
+ const rewritten = rewriteIncomingIdViaToolCallMap(incoming);
2567
+ if (rewritten.id !== incoming.id) {
2568
+ incoming = rewritten;
2569
+ idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id);
2570
+ }
2571
+ }
2367
2572
  if (idx !== -1) {
2368
2573
  accumulatedUIMessages[idx] = incoming;
2369
2574
  replaced = true;
@@ -2372,6 +2577,7 @@ function chatAgent(options) {
2372
2577
  accumulatedUIMessages.push(incoming);
2373
2578
  turnNewUIMessages.push(incoming);
2374
2579
  }
2580
+ recordToolCallIdsFromMessage(incoming);
2375
2581
  }
2376
2582
  if (replaced) {
2377
2583
  // Reconvert all model messages since a replacement changes the structure
@@ -2773,6 +2979,12 @@ function chatAgent(options) {
2773
2979
  }
2774
2980
  turnNewUIMessages.push(capturedResponseMessage);
2775
2981
  locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
2982
+ // Record toolCallId → head messageId so a HITL
2983
+ // continuation next turn can recover the head id
2984
+ // even if the AI SDK regenerates it. See
2985
+ // `chatToolCallToMessageIdKey` for the full
2986
+ // rationale (TRI-9137).
2987
+ recordToolCallIdsFromMessage(capturedResponseMessage);
2776
2988
  try {
2777
2989
  const responseModelMessages = await toModelMessages([
2778
2990
  stripProviderMetadata(capturedResponseMessage),