@assistant-ui/react-ag-ui 0.1.0 → 0.1.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 (65) hide show
  1. package/README.md +2 -0
  2. package/dist/agUiExtras.js +1 -2
  3. package/dist/hooks.js +1 -2
  4. package/dist/runtime/AgUiThreadController.d.ts +11 -0
  5. package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
  6. package/dist/runtime/AgUiThreadController.js +565 -89
  7. package/dist/runtime/AgUiThreadController.js.map +1 -1
  8. package/dist/runtime/adapter/conversions.d.ts +9 -1
  9. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  10. package/dist/runtime/adapter/conversions.js +194 -12
  11. package/dist/runtime/adapter/conversions.js.map +1 -1
  12. package/dist/runtime/adapter/replay-content.d.ts +9 -0
  13. package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
  14. package/dist/runtime/adapter/replay-content.js +185 -0
  15. package/dist/runtime/adapter/replay-content.js.map +1 -0
  16. package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
  17. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  18. package/dist/runtime/adapter/run-aggregator.js +35 -14
  19. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  20. package/dist/runtime/adapter/subscriber.d.ts +0 -1
  21. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  22. package/dist/runtime/adapter/subscriber.js +2 -10
  23. package/dist/runtime/adapter/subscriber.js.map +1 -1
  24. package/dist/runtime/adapter/tool-approval.d.ts +2 -0
  25. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  26. package/dist/runtime/adapter/tool-approval.js +23 -6
  27. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  28. package/dist/runtime/agUiThreadState.d.ts +1 -1
  29. package/dist/runtime/agUiThreadState.d.ts.map +1 -1
  30. package/dist/runtime/agUiThreadState.js +3 -4
  31. package/dist/runtime/agUiThreadState.js.map +1 -1
  32. package/dist/runtime/event-parser.d.ts.map +1 -1
  33. package/dist/runtime/event-parser.js +2 -2
  34. package/dist/runtime/event-parser.js.map +1 -1
  35. package/dist/runtime/interrupt-internals.js +1 -2
  36. package/dist/runtime/logger.js +1 -2
  37. package/dist/runtime/mcp-tool-result.js +1 -2
  38. package/dist/runtime/types.d.ts +2 -0
  39. package/dist/runtime/types.d.ts.map +1 -1
  40. package/dist/runtime/types.js +3 -3
  41. package/dist/runtime/types.js.map +1 -1
  42. package/dist/useAgUiRuntime.js +1 -2
  43. package/package.json +6 -6
  44. package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
  45. package/src/runtime/AgUiThreadController.disconnect.test.ts +914 -0
  46. package/src/runtime/AgUiThreadController.interrupt-history.test.ts +392 -0
  47. package/src/runtime/AgUiThreadController.replay.test.ts +3939 -0
  48. package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +167 -0
  49. package/src/runtime/AgUiThreadController.subagent-snapshot.test.ts +854 -0
  50. package/src/runtime/AgUiThreadController.test.ts +119 -0
  51. package/src/runtime/AgUiThreadController.ts +1184 -136
  52. package/src/runtime/adapter/conversions.content.test.ts +242 -1
  53. package/src/runtime/adapter/conversions.subagent-messages.test.ts +550 -0
  54. package/src/runtime/adapter/conversions.ts +349 -28
  55. package/src/runtime/adapter/replay-content.test.ts +235 -0
  56. package/src/runtime/adapter/replay-content.ts +306 -0
  57. package/src/runtime/adapter/run-aggregator.metadata.test.ts +151 -0
  58. package/src/runtime/adapter/run-aggregator.ts +52 -9
  59. package/src/runtime/adapter/subscriber.test.ts +1 -1
  60. package/src/runtime/adapter/subscriber.ts +1 -6
  61. package/src/runtime/adapter/tool-approval.ts +41 -3
  62. package/src/runtime/agUiThreadState.test.ts +21 -2
  63. package/src/runtime/agUiThreadState.ts +12 -3
  64. package/src/runtime/event-parser.ts +3 -0
  65. package/src/runtime/types.ts +5 -0
@@ -2,21 +2,27 @@
2
2
  import { createAgUiThreadState, reduceAgUiThreadState } from "./agUiThreadState.js";
3
3
  import { readMcpAppResourceUri, toolContentText } from "./mcp-tool-result.js";
4
4
  import { AG_UI_METADATA_NAMESPACE } from "./types.js";
5
- import { buildToolApprovalResume, projectAgUiToolApprovals, withSettledToolApprovals, withToolApprovalDecision } from "./adapter/tool-approval.js";
5
+ import { buildToolApprovalResume, preserveToolApprovalDecision, projectAgUiToolApprovals, withSettledToolApprovals, withToolApprovalDecision } from "./adapter/tool-approval.js";
6
+ import { fromAgUiSnapshotMessages, toAgUiMessages, toAgUiTools, withAgUiPartMessageMetadata } from "./adapter/conversions.js";
6
7
  import { RunAggregator, isPlainObject, tryParseJSON } from "./adapter/run-aggregator.js";
7
- import { fromAgUiMessages, toAgUiMessages, toAgUiTools } from "./adapter/conversions.js";
8
8
  import { createAgUiSubscriber } from "./adapter/subscriber.js";
9
+ import { createReplayContentMerger, groupReplayToolContent, indexReplayToolOwners, preserveToolResult } from "./adapter/replay-content.js";
9
10
  import { fromThreadMessageLike, generateId, isMcpAppUri } from "@assistant-ui/core";
10
11
  import { appendToolInteraction, createHistoryWriteChain, createMessageRepositorySession, invokeUserCallback, iterateToolCallParts, mapToolCallPartsDeep } from "@assistant-ui/core/internal";
11
12
  import jsonpatch from "fast-json-patch";
12
- //#region src/runtime/AgUiThreadController.ts
13
+ // src/runtime/AgUiThreadController.ts
13
14
  const optimisticPrefix = "__optimistic__";
14
15
  const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
15
16
  const isOptimisticId = (id) => id.startsWith(optimisticPrefix);
16
17
  const isResolvedToolCall = (part) => part.type === "tool-call" && "result" in part && part.result !== void 0;
18
+ const hasResolvedToolCalls = (message) => {
19
+ if (message.role !== "assistant") return false;
20
+ for (const part of iterateToolCallParts(message.content)) if (isResolvedToolCall(part)) return true;
21
+ return false;
22
+ };
17
23
  const listedPendingToolCallIds = (assistant) => {
18
24
  const ids = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE]?.pendingToolCallIds;
19
- return ids?.length ? new Set(ids) : void 0;
25
+ return ids === void 0 ? void 0 : new Set(ids);
20
26
  };
21
27
  const withoutPendingToolCallIds = (metadata) => {
22
28
  const namespace = metadata.custom[AG_UI_METADATA_NAMESPACE];
@@ -31,11 +37,62 @@ const withoutPendingToolCallIds = (metadata) => {
31
37
  }
32
38
  };
33
39
  };
40
+ const withPendingToolCallIds = (metadata, ids) => ({
41
+ ...metadata,
42
+ custom: {
43
+ ...metadata.custom,
44
+ [AG_UI_METADATA_NAMESPACE]: {
45
+ ...metadata.custom[AG_UI_METADATA_NAMESPACE],
46
+ pendingToolCallIds: [...ids]
47
+ }
48
+ }
49
+ });
50
+ const actionableToolCallIds = (message) => {
51
+ if (message?.role !== "assistant" || message.status?.type !== "requires-action" || message.status.reason !== "tool-calls") return /* @__PURE__ */ new Set();
52
+ const permitted = listedPendingToolCallIds(message);
53
+ return new Set(Array.from(iterateToolCallParts(message.content)).filter((part) => !isResolvedToolCall(part) && (!permitted || permitted.has(part.toolCallId))).map((part) => part.toolCallId));
54
+ };
34
55
  const isActivityPart = (part) => part.type === "data" && part.name.startsWith("agui-activity/");
35
56
  const FALLBACK_USER_STATUS = {
36
57
  type: "complete",
37
58
  reason: "unknown"
38
59
  };
60
+ const textOf = (content) => content.flatMap((part) => part.type === "text" ? [part.text] : []).join("");
61
+ /**
62
+ * Swaps `content`'s text parts, in order, for `source`'s. A text part `source`
63
+ * has beyond `content`'s text slots goes after the nearest earlier part of
64
+ * `source` that `content` also has: a tool call by id, another part by its
65
+ * type and position among parts of that type. With none, it goes first.
66
+ */
67
+ const replaceTextParts = (content, source) => {
68
+ const texts = source.filter((part) => part.type === "text");
69
+ const result = [];
70
+ let used = 0;
71
+ for (const part of content) if (part.type !== "text") result.push(part);
72
+ else if (used < texts.length) result.push(texts[used++]);
73
+ const positionIn = (parts, part) => parts.filter((other) => other.type === part.type).indexOf(part);
74
+ for (const text of texts.slice(used)) {
75
+ let anchor = -1;
76
+ for (let index = source.indexOf(text) - 1; index >= 0 && anchor < 0; index--) {
77
+ const before = source[index];
78
+ if (before.type === "tool-call") anchor = result.findIndex((part) => part.type === "tool-call" && part.toolCallId === before.toolCallId);
79
+ else if (before.type === "text") anchor = result.indexOf(before);
80
+ else {
81
+ const match = result.filter((part) => part.type === before.type)[positionIn(source, before)];
82
+ anchor = match ? result.indexOf(match) : -1;
83
+ }
84
+ }
85
+ result.splice(anchor + 1, 0, text);
86
+ }
87
+ return result;
88
+ };
89
+ const settleNestedContent = (content, status) => mapToolCallPartsDeep(content, (part) => part.messages ? {
90
+ ...part,
91
+ messages: part.messages.map((message) => message.role === "assistant" && message.status?.type === "running" ? {
92
+ ...message,
93
+ status
94
+ } : message)
95
+ } : part).content;
39
96
  const invokeRuntimeCallback = (name, callback, ...args) => {
40
97
  invokeUserCallback("react-ag-ui", name, callback, ...args);
41
98
  };
@@ -86,6 +143,7 @@ var AgUiThreadController = class {
86
143
  activeRunAgent = null;
87
144
  history;
88
145
  assistantHistoryParents = /* @__PURE__ */ new Map();
146
+ subagentParentToolCallIds = /* @__PURE__ */ new Map();
89
147
  snapshotHistoryIds = /* @__PURE__ */ new Set();
90
148
  persistedHistoryIds = /* @__PURE__ */ new Set();
91
149
  historyWrites = createHistoryWriteChain();
@@ -167,6 +225,7 @@ var AgUiThreadController = class {
167
225
  if (!repo) return;
168
226
  this.session.applyExternalMessageRepository(repo);
169
227
  this.assistantHistoryParents.clear();
228
+ this.subagentParentToolCallIds.clear();
170
229
  this.snapshotHistoryIds.clear();
171
230
  this.persistedHistoryIds.clear();
172
231
  for (const { message } of repo.messages) this.persistedHistoryIds.add(message.id);
@@ -299,7 +358,7 @@ var AgUiThreadController = class {
299
358
  const historyImport = this.waitForHistoryImport();
300
359
  if (historyImport && !await historyImport) return;
301
360
  this.assertNoPendingInterrupts();
302
- await this.startRun(config.parentId, config.runConfig ?? this.state.lastRunConfig, void 0, config.stream);
361
+ await this.startRun(config.parentId, config.runConfig ?? this.state.lastRunConfig, void 0, config.stream, true);
303
362
  }
304
363
  async resumeInFlightRun(messages) {
305
364
  const historyImport = this.waitForHistoryImport();
@@ -321,9 +380,7 @@ var AgUiThreadController = class {
321
380
  throw new Error("[agui] cannot start a new run while interrupts are pending; resolve them with submitInterruptResponses()");
322
381
  }
323
382
  findRequiresActionAssistant(reason) {
324
- const assistant = this.getMessages().findLast((message) => message.role === "assistant");
325
- if (!assistant || assistant.status?.type !== "requires-action" || assistant.status.reason !== reason) return null;
326
- return assistant;
383
+ return this.getTrailingAssistantTurn().findLast((message) => message.status?.type === "requires-action" && message.status.reason === reason) ?? null;
327
384
  }
328
385
  getTrailingAssistantTurn() {
329
386
  const messages = this.getMessages();
@@ -383,8 +440,9 @@ var AgUiThreadController = class {
383
440
  * unretryable.
384
441
  */
385
442
  async resumeWithResponses(messageId, resume) {
443
+ const parentId = this.session.headId ?? messageId;
386
444
  this.clearPendingInterrupts(messageId, resume);
387
- await this.startRun(messageId, this.state.lastRunConfig, {
445
+ await this.startRun(parentId, this.state.lastRunConfig, {
388
446
  entries: resume,
389
447
  interruptedMessageId: messageId
390
448
  });
@@ -535,7 +593,10 @@ var AgUiThreadController = class {
535
593
  custom: newCustom
536
594
  }
537
595
  };
538
- })) this.publishUpdate();
596
+ })) {
597
+ this.publishUpdate();
598
+ this.persistSettledAssistant(messageId);
599
+ }
539
600
  }
540
601
  findMessageIdForToolCall(toolCallId) {
541
602
  let fallbackMessageId;
@@ -589,9 +650,31 @@ var AgUiThreadController = class {
589
650
  };
590
651
  });
591
652
  if (!matchedToolCall) return message;
653
+ const settled = mapToolCallPartsDeep(content, (part) => {
654
+ if (!part.messages) return part;
655
+ let changed = false;
656
+ const messages = part.messages.map((nested) => {
657
+ if (nested.role !== "assistant" || nested.status.type !== "requires-action" || nested.status.reason !== "tool-calls") return nested;
658
+ const listed = listedPendingToolCallIds(nested);
659
+ if (Array.from(iterateToolCallParts(nested.content)).some((call) => !isResolvedToolCall(call) && (!listed || listed.has(call.toolCallId)))) return nested;
660
+ changed = true;
661
+ return {
662
+ ...nested,
663
+ status: {
664
+ type: "complete",
665
+ reason: "unknown"
666
+ },
667
+ metadata: withoutPendingToolCallIds(nested.metadata)
668
+ };
669
+ });
670
+ return changed ? {
671
+ ...part,
672
+ messages
673
+ } : part;
674
+ }).content;
592
675
  return {
593
676
  ...assistant,
594
- content
677
+ content: settled
595
678
  };
596
679
  })) return;
597
680
  this.publishUpdate();
@@ -657,23 +740,27 @@ var AgUiThreadController = class {
657
740
  }
658
741
  this.startResumeRun(parentId);
659
742
  }
660
- maybeResumeAfterToolResults(messageId) {
743
+ maybeResumeAfterToolResults(messageId, accountedResultIds) {
661
744
  if (!this.maybeCompleteAfterToolResults(messageId)) return;
745
+ if (accountedResultIds) {
746
+ const message = this.session.tryGetMessage(messageId)?.message;
747
+ if (message?.role !== "assistant" || !Array.from(iterateToolCallParts(message.content)).some((part) => isResolvedToolCall(part) && !accountedResultIds.has(part.toolCallId))) return;
748
+ }
662
749
  if (this.getPendingInterrupts()) return;
663
750
  const turn = this.getTrailingAssistantTurn();
664
751
  if (!turn.some((message) => message.id === messageId)) return;
665
- if (this.getPendingToolCalls()) return;
666
- const headId = turn.at(-1)?.id;
667
- if (headId === void 0) return;
668
752
  const owner = this.abortController;
669
753
  if (owner) {
670
754
  this.dispatch({
671
755
  type: "resumeDeferred",
672
756
  owner,
673
- messageId: headId
757
+ messageId
674
758
  });
675
759
  return;
676
760
  }
761
+ if (this.getPendingToolCalls()) return;
762
+ const headId = turn.at(-1)?.id;
763
+ if (headId === void 0) return;
677
764
  this.startResumeRun(headId);
678
765
  }
679
766
  maybeCompleteAfterToolResults(messageId) {
@@ -700,7 +787,8 @@ var AgUiThreadController = class {
700
787
  metadata: withoutPendingToolCallIds(current.metadata)
701
788
  } : current)) return false;
702
789
  this.publishUpdate();
703
- this.persistAssistantHistory(messageId);
790
+ if (this.history?.update) this.persistSettledAssistant(messageId);
791
+ else this.persistAssistantHistory(messageId);
704
792
  return true;
705
793
  }
706
794
  startResumeRun(messageId) {
@@ -729,6 +817,10 @@ var AgUiThreadController = class {
729
817
  let lastAppliedId = null;
730
818
  let hardReplace = false;
731
819
  const seen = /* @__PURE__ */ new Set();
820
+ const retainToolResults = (existing, message) => existing?.role === "assistant" && message.role === "assistant" ? {
821
+ ...message,
822
+ content: this.preserveToolResults(existing.content, message.content)
823
+ } : message;
732
824
  for (const message of messages) {
733
825
  if (seen.has(message.id)) continue;
734
826
  seen.add(message.id);
@@ -737,11 +829,12 @@ var AgUiThreadController = class {
737
829
  hardReplace = true;
738
830
  break;
739
831
  }
740
- this.session.addOrUpdateMessage(expectedParentId, message);
832
+ this.session.addOrUpdateMessage(expectedParentId, retainToolResults(existing?.message, message));
741
833
  expectedParentId = message.id;
742
834
  lastAppliedId = message.id;
743
835
  }
744
836
  if (hardReplace) {
837
+ const previous = new Map(this.getMessageRepository().messages.map(({ message }) => [message.id, message]));
745
838
  this.session.clear();
746
839
  expectedParentId = null;
747
840
  lastAppliedId = null;
@@ -749,17 +842,29 @@ var AgUiThreadController = class {
749
842
  for (const message of messages) {
750
843
  if (seen.has(message.id)) continue;
751
844
  seen.add(message.id);
752
- this.session.addOrUpdateMessage(expectedParentId, message);
845
+ this.session.addOrUpdateMessage(expectedParentId, retainToolResults(previous.get(message.id), message));
753
846
  expectedParentId = message.id;
754
847
  lastAppliedId = message.id;
755
848
  }
756
849
  }
757
850
  this.session.resetHead(lastAppliedId);
851
+ for (const messageId of seen) this.maybeCompleteAfterToolResults(messageId);
758
852
  }
759
853
  this.snapshotHistoryIds.clear();
760
854
  for (const { message } of this.getMessageRepository().messages) this.snapshotHistoryIds.add(message.id);
761
- const resumeTarget = this.state.pendingResume?.messageId;
762
- if (resumeTarget !== void 0 && !this.session.getMessages().some((message) => message.id === resumeTarget)) this.dispatch({ type: "resumeCleared" });
855
+ const pendingResume = this.state.pendingResume;
856
+ if (pendingResume) {
857
+ const selectedResultOwnerIds = new Set(this.session.getMessages().filter(hasResolvedToolCalls).map((message) => message.id));
858
+ const retained = pendingResume.messageIds.filter((id) => selectedResultOwnerIds.has(id));
859
+ if (retained.length !== pendingResume.messageIds.length) {
860
+ this.dispatch({ type: "resumeCleared" });
861
+ for (const messageId of retained) this.dispatch({
862
+ type: "resumeDeferred",
863
+ owner: pendingResume.owner,
864
+ messageId
865
+ });
866
+ }
867
+ }
763
868
  this.publishUpdate();
764
869
  }
765
870
  loadExternalState(state) {
@@ -786,6 +891,7 @@ var AgUiThreadController = class {
786
891
  const controller = this.abortController;
787
892
  const activeRunAgent = this.activeRunAgent;
788
893
  this.dispatch({ type: "resetThread" });
894
+ this.subagentParentToolCallIds.clear();
789
895
  if (controller) {
790
896
  this.abortController = null;
791
897
  this.activeRunAgent = null;
@@ -799,9 +905,10 @@ var AgUiThreadController = class {
799
905
  }
800
906
  } else this.publishUpdate();
801
907
  }
802
- async startRun(parentId, runConfig, resume, resumeStream) {
908
+ async startRun(parentId, runConfig, resume, resumeStream, replayExistingMessages = false) {
803
909
  if (this.isThreadSwitching?.()) return;
804
910
  if (!this.supersedeActiveRun()) return;
911
+ const generation = this.state.historyMutationGeneration;
805
912
  const normalizedRunConfig = runConfig ?? {};
806
913
  this.dispatch({
807
914
  type: "runConfigRecorded",
@@ -811,36 +918,79 @@ var AgUiThreadController = class {
811
918
  const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.session.headId;
812
919
  const historicalMessages = [...shouldEagerlyInsertAssistant ? this.session.tryGetMessages(parentId) ?? this.session.getMessages() : this.session.getMessages()];
813
920
  const runStartMessageIds = new Set(this.getMessageRepository().messages.map(({ message }) => message.id));
921
+ const selectedMessages = this.session.getMessages();
922
+ const parentIndex = selectedMessages.findIndex((message) => message.id === parentId);
923
+ let replayStart = parentIndex + 1;
924
+ if (selectedMessages[parentIndex]?.role === "assistant") {
925
+ replayStart = parentIndex;
926
+ while (replayStart > 0 && selectedMessages[replayStart - 1]?.role === "assistant") replayStart--;
927
+ }
928
+ const replayMessageIds = [];
929
+ if (replayExistingMessages && !resumeStream && parentIndex >= 0) for (const message of selectedMessages.slice(replayStart)) {
930
+ if (message.role !== "assistant") break;
931
+ replayMessageIds.push(message.id);
932
+ }
933
+ const accountedResultIds = new Set(historicalMessages.flatMap((message) => message.role === "assistant" ? Array.from(iterateToolCallParts(message.content)).filter(isResolvedToolCall).map((part) => part.toolCallId) : []));
934
+ const candidateContexts = /* @__PURE__ */ new Map();
935
+ for (const message of selectedMessages) {
936
+ if (message.role !== "assistant" || !replayMessageIds.includes(message.id)) continue;
937
+ candidateContexts.set(message.id, {
938
+ originalContent: message.content,
939
+ accountedResultIds,
940
+ admittedToolCallIds: actionableToolCallIds(message),
941
+ merge: createReplayContentMerger()
942
+ });
943
+ }
944
+ let replayContexts = /* @__PURE__ */ new Map();
945
+ let lastReplayedIndex = -1;
946
+ let replayEnded = replayMessageIds.length === 0;
947
+ const replayTail = replayMessageIds.at(-1);
814
948
  const joinStrategy = this.joinStrategy;
815
949
  const hasRunUserBoundary = parent?.message.role === "user" && !resume;
816
950
  let pendingError = null;
817
951
  const assistantParentId = parent ? parentId : this.session.headId;
818
952
  let assistantMessageId;
953
+ let snapshotAssistantId;
954
+ let replayInterruptMessageId;
819
955
  let assistantCollided = false;
820
956
  const heldMessages = /* @__PURE__ */ new Map();
957
+ let frameOwnerIds = /* @__PURE__ */ new Set();
821
958
  let restrictedPendingToolCallIds;
822
959
  let settlingRun = false;
960
+ let latestSnapshot;
961
+ let snapshotToolCallIds = /* @__PURE__ */ new Set();
823
962
  const ensureAssistant = (allowRecreate = false) => {
824
963
  const cached = assistantMessageId;
825
964
  if (cached !== void 0 && this.session.tryGetMessage(cached)) return cached;
826
965
  if (cached !== void 0 && (assistantCollided || !allowRecreate)) return cached;
827
966
  const repositoryHeadId = this.session.headId;
828
- const parentId = (cached === void 0 || shouldEagerlyInsertAssistant && (repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId))) && assistantParentId && this.session.hasMessage(assistantParentId) ? assistantParentId : repositoryHeadId;
967
+ const parentId = replayContexts.size === 0 && (cached === void 0 || shouldEagerlyInsertAssistant && (repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId))) && (replayMessageIds.length === 0 || repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId)) && assistantParentId && this.session.hasMessage(assistantParentId) ? assistantParentId : repositoryHeadId;
829
968
  const created = this.insertAssistantPlaceholder(parentId, cached);
830
969
  assistantMessageId = created;
831
970
  this.markPendingAssistantHistory(created, parentId);
832
971
  return created;
833
972
  };
834
- if (shouldEagerlyInsertAssistant) ensureAssistant();
973
+ if (shouldEagerlyInsertAssistant && replayEnded) ensureAssistant();
835
974
  const settleHeldMessages = (terminalStatus) => {
836
975
  const pendingMessages = [];
976
+ const producerPending = restrictedPendingToolCallIds;
977
+ const canWaitForTools = terminalStatus.type === "complete" || terminalStatus.type === "requires-action";
978
+ if (producerPending && canWaitForTools) for (const message of this.getTrailingAssistantTurn()) {
979
+ const replay = replayContexts.get(message.id);
980
+ if (message.id === assistantMessageId || heldMessages.has(message.id) || !replay || message.status?.type === "requires-action" && message.status.reason === "interrupt") continue;
981
+ if (replay.admittedToolCallIds.size > 0 || actionableToolCallIds(message).size > 0 || Array.from(iterateToolCallParts(message.content)).some((part) => producerPending.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId))) heldMessages.set(message.id, /* @__PURE__ */ new Set());
982
+ }
837
983
  for (const [messageId, openToolCallIds] of heldMessages) {
984
+ const existing = this.session.tryGetMessage(messageId);
985
+ if (existing) this.markPendingAssistantHistory(messageId, existing.parentId);
838
986
  let requiresToolResult = false;
839
987
  if (this.session.updateMessage(messageId, (message) => {
840
988
  if (message.role !== "assistant") return message;
841
989
  const assistant = message;
842
- const pendingToolCallIds = Array.from(openToolCallIds).filter((id) => !restrictedPendingToolCallIds || restrictedPendingToolCallIds.has(id));
843
- const status = terminalStatus.type === "complete" || terminalStatus.type === "requires-action" ? pendingToolCallIds.length > 0 ? {
990
+ const pendingToolCallIds = producerPending ? Array.from(iterateToolCallParts(assistant.content)).filter((part) => producerPending.has(part.toolCallId) && (!isResolvedToolCall(part) || openToolCallIds.has(part.toolCallId) || replayContexts.has(messageId) && !accountedResultIds.has(part.toolCallId))).map((part) => part.toolCallId) : Array.from(openToolCallIds);
991
+ const replay = replayContexts.get(messageId);
992
+ if (replay) replay.admittedToolCallIds = new Set(pendingToolCallIds);
993
+ const status = canWaitForTools ? pendingToolCallIds.length > 0 ? {
844
994
  type: "requires-action",
845
995
  reason: "tool-calls"
846
996
  } : {
@@ -852,16 +1002,7 @@ var AgUiThreadController = class {
852
1002
  return {
853
1003
  ...assistant,
854
1004
  status,
855
- metadata: status.type === "requires-action" && restrictedPendingToolCallIds ? {
856
- ...metadata,
857
- custom: {
858
- ...metadata.custom,
859
- ["agui"]: {
860
- ...metadata.custom["agui"],
861
- pendingToolCallIds
862
- }
863
- }
864
- } : metadata
1005
+ metadata: status.type === "requires-action" && (restrictedPendingToolCallIds || replayContexts.has(messageId)) ? withPendingToolCallIds(metadata, pendingToolCallIds) : metadata
865
1006
  };
866
1007
  })) {
867
1008
  this.publishUpdate();
@@ -872,29 +1013,139 @@ var AgUiThreadController = class {
872
1013
  heldMessages.clear();
873
1014
  return pendingMessages;
874
1015
  };
1016
+ const accountProducerResults = (content) => {
1017
+ for (const part of iterateToolCallParts(content)) if (isResolvedToolCall(part)) accountedResultIds.add(part.toolCallId);
1018
+ };
1019
+ const reconcileReplaySnapshot = (snapshot, previous) => {
1020
+ accountProducerResults(snapshot.content);
1021
+ const replay = candidateContexts.get(snapshot.id);
1022
+ if (!replay) return snapshot;
1023
+ replay.admittedToolCallIds = actionableToolCallIds(snapshot);
1024
+ if (heldMessages.has(snapshot.id)) heldMessages.set(snapshot.id, new Set(replay.admittedToolCallIds));
1025
+ if (!replayContexts.has(snapshot.id)) return snapshot;
1026
+ return previous ? {
1027
+ ...snapshot,
1028
+ content: this.preserveToolResults(previous.content, snapshot.content, replay.originalContent)
1029
+ } : snapshot;
1030
+ };
875
1031
  const applyUpdate = (update) => {
1032
+ if (!assistantCollided && replayContexts.size && update.content !== void 0) accountProducerResults(update.content);
1033
+ let ownerParts;
1034
+ let contentOwnerIds;
1035
+ const hasStreamContent = Array.isArray(update.content) && update.content.some((part) => part.type !== "data");
1036
+ if (!replayEnded && assistantMessageId === void 0) {
1037
+ if (!update.status || update.status.type === "running") return;
1038
+ replayEnded = true;
1039
+ if (replayContexts.size === 0 && snapshotAssistantId !== void 0 && this.session.headId === snapshotAssistantId && !hasStreamContent) {
1040
+ if (update.status.type !== "complete") this.updateAssistantMessage(snapshotAssistantId, {
1041
+ status: update.status,
1042
+ ...update.metadata ? { metadata: update.metadata } : {}
1043
+ });
1044
+ return;
1045
+ }
1046
+ }
1047
+ const streamMessageId = assistantMessageId;
1048
+ let primaryMessageId = streamMessageId;
876
1049
  const terminalStatus = update.status;
1050
+ const replayInterrupt = replayContexts.size > 0 && terminalStatus?.type === "requires-action" && terminalStatus.reason === "interrupt";
1051
+ if (streamMessageId !== void 0 && !assistantCollided && replayContexts.size > 0 && (update.content !== void 0 || replayInterrupt)) {
1052
+ const owners = this.getTrailingAssistantTurn().filter((message) => replayContexts.has(message.id));
1053
+ const grouped = update.content !== void 0 ? groupReplayToolContent(owners, update.content, streamMessageId) : /* @__PURE__ */ new Map([[streamMessageId, [...update.content ?? []]]]);
1054
+ if (update.content !== void 0) {
1055
+ const currentOwnerIds = new Set(grouped.keys());
1056
+ for (const ownerId of frameOwnerIds) grouped.set(ownerId, grouped.get(ownerId) ?? []);
1057
+ contentOwnerIds = new Set(grouped.keys());
1058
+ frameOwnerIds = currentOwnerIds;
1059
+ }
1060
+ ownerParts = grouped;
1061
+ if (replayInterrupt) {
1062
+ const interrupts = (update.metadata?.custom?.[AG_UI_METADATA_NAMESPACE])?.interrupts;
1063
+ const approvals = projectAgUiToolApprovals(interrupts);
1064
+ for (const owner of owners) if (Array.from(iterateToolCallParts(owner.content)).some((part) => approvals.has(part.toolCallId))) grouped.set(owner.id, grouped.get(owner.id) ?? []);
1065
+ const anchor = owners.find((message) => projectAgUiToolApprovals(interrupts, new Set(Array.from(iterateToolCallParts([...message.content, ...grouped.get(message.id) ?? []])).map((part) => part.toolCallId))).size > 0);
1066
+ if (anchor) {
1067
+ primaryMessageId = anchor.id;
1068
+ replayInterruptMessageId = anchor.id;
1069
+ grouped.set(anchor.id, grouped.get(anchor.id) ?? []);
1070
+ }
1071
+ }
1072
+ }
877
1073
  const pendingMessages = settlingRun && terminalStatus && terminalStatus.type !== "running" ? settleHeldMessages(terminalStatus) : [];
878
- const hasStreamContent = Array.isArray(update.content) && update.content.some((part) => part.type !== "data");
879
- const resolved = this.updateAssistantMessage(ensureAssistant(hasStreamContent), update);
880
- if (resolved !== assistantMessageId) assistantMessageId = resolved;
881
- for (const messageId of pendingMessages) this.maybeResumeAfterToolResults(messageId);
1074
+ const frameMessageId = ensureAssistant(hasStreamContent);
1075
+ const messageId = primaryMessageId ?? frameMessageId;
1076
+ let primaryUpdate = update;
1077
+ if (ownerParts) {
1078
+ const interrupt = terminalStatus?.type === "requires-action" && terminalStatus.reason === "interrupt";
1079
+ const ownerStatus = terminalStatus?.type === "requires-action" ? {
1080
+ type: "complete",
1081
+ reason: "unknown"
1082
+ } : terminalStatus;
1083
+ for (const [ownerId, content] of ownerParts) {
1084
+ const existing = this.session.tryGetMessage(ownerId);
1085
+ if (existing) this.markPendingAssistantHistory(ownerId, existing.parentId);
1086
+ if (ownerId === messageId) continue;
1087
+ this.updateAssistantMessage(ownerId, {
1088
+ ...contentOwnerIds?.has(ownerId) ? { content } : {},
1089
+ ...ownerStatus ? { status: ownerStatus } : {}
1090
+ }, replayContexts.get(ownerId), interrupt ? /* @__PURE__ */ new Set() : restrictedPendingToolCallIds);
1091
+ }
1092
+ const { content: _content, ...controlUpdate } = update;
1093
+ const content = ownerParts.get(messageId) ?? [];
1094
+ primaryUpdate = {
1095
+ ...controlUpdate,
1096
+ ...contentOwnerIds?.has(messageId) ? { content } : {},
1097
+ ...ownerStatus && !interrupt && replayContexts.has(messageId) ? { status: ownerStatus } : {}
1098
+ };
1099
+ }
1100
+ const resolved = this.updateAssistantMessage(messageId, primaryUpdate, replayContexts.get(messageId), restrictedPendingToolCallIds);
1101
+ if (messageId === frameMessageId && resolved !== assistantMessageId) assistantMessageId = resolved;
1102
+ for (const messageId of pendingMessages) this.maybeResumeAfterToolResults(messageId, replayMessageIds.includes(messageId) ? accountedResultIds : void 0);
882
1103
  };
883
1104
  const adoptServerMessageId = (serverId, startNewMessage, openToolCallIds = []) => {
1105
+ const existing = this.session.tryGetMessage(serverId);
1106
+ const replayIndex = replayMessageIds.indexOf(serverId);
1107
+ const adoptsReplay = !replayEnded && !assistantCollided && replayIndex > lastReplayedIndex && existing?.message.role === "assistant" && this.session.getMessages().some((message) => message.id === serverId);
1108
+ if (adoptsReplay && replayContexts.size === 0) {
1109
+ replayContexts = candidateContexts;
1110
+ const selected = this.session.getMessages();
1111
+ let tailId = serverId;
1112
+ for (const message of selected.slice(selected.findIndex((message) => message.id === serverId) + 1)) {
1113
+ if (message.role !== "assistant") break;
1114
+ tailId = message.id;
1115
+ }
1116
+ this.session.selectHead(tailId);
1117
+ }
884
1118
  if (startNewMessage && assistantMessageId !== void 0) {
885
- if (openToolCallIds.length > 0) heldMessages.set(assistantMessageId, new Set(openToolCallIds));
1119
+ let hasReplayCallsToHold = false;
1120
+ const replay = replayContexts.get(assistantMessageId);
1121
+ if (replay) {
1122
+ const current = this.session.tryGetMessage(assistantMessageId)?.message;
1123
+ if (current?.role === "assistant") {
1124
+ const retainedPending = Array.from(iterateToolCallParts(current.content)).filter((part) => !isResolvedToolCall(part) || replay.admittedToolCallIds.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId));
1125
+ hasReplayCallsToHold = retainedPending.length > 0;
1126
+ openToolCallIds = [.../* @__PURE__ */ new Set([...openToolCallIds, ...retainedPending.filter((part) => replay.admittedToolCallIds.has(part.toolCallId)).map((part) => part.toolCallId)])];
1127
+ }
1128
+ }
1129
+ if (openToolCallIds.length > 0 || hasReplayCallsToHold) heldMessages.set(assistantMessageId, new Set(openToolCallIds));
886
1130
  else applyUpdate({ status: {
887
1131
  type: "complete",
888
1132
  reason: "unknown"
889
1133
  } });
890
1134
  const previousId = assistantMessageId;
891
1135
  assistantMessageId = void 0;
892
- if (this.session.tryGetMessage(previousId)) {
1136
+ if (!adoptsReplay && this.session.tryGetMessage(previousId)) {
893
1137
  const created = this.insertAssistantPlaceholder(previousId);
894
1138
  assistantMessageId = created;
895
1139
  this.markPendingAssistantHistory(created, previousId);
896
1140
  }
897
1141
  }
1142
+ if (adoptsReplay) {
1143
+ assistantMessageId = serverId;
1144
+ lastReplayedIndex = replayIndex;
1145
+ this.markPendingAssistantHistory(serverId, existing.parentId);
1146
+ return;
1147
+ }
1148
+ replayEnded = true;
898
1149
  const placeholder = ensureAssistant(true);
899
1150
  if (placeholder === serverId) return;
900
1151
  const reassigned = this.reassignAssistantId(placeholder, serverId);
@@ -913,25 +1164,103 @@ var AgUiThreadController = class {
913
1164
  showThinking: this.showThinking,
914
1165
  joinStrategy,
915
1166
  logger: this.logger,
916
- emit: applyUpdate,
1167
+ emit: (update) => {
1168
+ const currentSnapshotAssistant = assistantMessageId === void 0 ? void 0 : this.session.tryGetMessage(assistantMessageId)?.message;
1169
+ const listedSnapshotToolCallIds = currentSnapshotAssistant?.role === "assistant" ? listedPendingToolCallIds(currentSnapshotAssistant) : void 0;
1170
+ const hasPendingSnapshotToolCall = latestSnapshot?.id === assistantMessageId && currentSnapshotAssistant?.role === "assistant" && Array.from(iterateToolCallParts(currentSnapshotAssistant.content)).some((part) => !isResolvedToolCall(part) && (!listedSnapshotToolCallIds || listedSnapshotToolCallIds.has(part.toolCallId)) && (!restrictedPendingToolCallIds || restrictedPendingToolCallIds.has(part.toolCallId)));
1171
+ const reconciledUpdate = settlingRun && update.status?.type === "complete" && hasPendingSnapshotToolCall ? {
1172
+ ...update,
1173
+ status: {
1174
+ type: "requires-action",
1175
+ reason: "tool-calls"
1176
+ },
1177
+ ...restrictedPendingToolCallIds ? { metadata: {
1178
+ ...update.metadata,
1179
+ custom: {
1180
+ ...update.metadata?.custom,
1181
+ [AG_UI_METADATA_NAMESPACE]: {
1182
+ ...update.metadata?.custom?.[AG_UI_METADATA_NAMESPACE],
1183
+ pendingToolCallIds: [...restrictedPendingToolCallIds]
1184
+ }
1185
+ }
1186
+ } } : {}
1187
+ } : update;
1188
+ if (settlingRun && latestSnapshot && latestSnapshot.id === assistantMessageId && reconciledUpdate.status && reconciledUpdate.status.type !== "running" && reconciledUpdate.status.type !== "requires-action") {
1189
+ const status = reconciledUpdate.status;
1190
+ const current = this.session.tryGetMessage(assistantMessageId)?.message;
1191
+ const content = settleNestedContent(current?.role === "assistant" ? current.content : latestSnapshot.content, status);
1192
+ applyUpdate({
1193
+ ...reconciledUpdate,
1194
+ content
1195
+ });
1196
+ } else if (latestSnapshot && latestSnapshot.id === assistantMessageId && reconciledUpdate.content !== void 0) {
1197
+ const content = this.mergeSnapshotAssistantContent(latestSnapshot.content, reconciledUpdate.content, reconciledUpdate.content.some((part) => part.type === "reasoning"), reconciledUpdate.content.some(isActivityPart), snapshotToolCallIds);
1198
+ applyUpdate({
1199
+ ...reconciledUpdate,
1200
+ content
1201
+ });
1202
+ } else applyUpdate(reconciledUpdate);
1203
+ const current = assistantMessageId ? this.session.tryGetMessage(assistantMessageId)?.message : void 0;
1204
+ latestSnapshot = !settlingRun && current?.role === "assistant" && current.id === latestSnapshot?.id ? current : void 0;
1205
+ },
917
1206
  onServerMessageId: (serverId) => {
918
1207
  adoptServerMessageId(serverId, false);
919
1208
  },
920
- onTextMessageStart: (serverId, openToolCallIds) => adoptServerMessageId(serverId, true, openToolCallIds)
1209
+ onTextMessageStart: (serverId, openToolCallIds) => {
1210
+ const owners = this.getTrailingAssistantTurn().filter((message) => replayContexts.has(message.id));
1211
+ const toolOwners = indexReplayToolOwners(owners);
1212
+ const frameOpenIds = openToolCallIds.filter((id) => {
1213
+ const ownerId = toolOwners.get(id);
1214
+ if (!ownerId || ownerId === assistantMessageId || assistantCollided) return true;
1215
+ const held = heldMessages.get(ownerId) ?? /* @__PURE__ */ new Set();
1216
+ held.add(id);
1217
+ heldMessages.set(ownerId, held);
1218
+ return false;
1219
+ });
1220
+ for (const ownerId of frameOwnerIds) {
1221
+ const replay = replayContexts.get(ownerId);
1222
+ if (replay) replay.merge = createReplayContentMerger();
1223
+ }
1224
+ frameOwnerIds.clear();
1225
+ adoptServerMessageId(serverId, true, frameOpenIds);
1226
+ }
921
1227
  });
922
1228
  let runFinished = false;
923
1229
  const dispatch = (event) => {
924
1230
  if (this.abortController !== abortController) return;
925
1231
  if (event.type === "RUN_FINISHED") {
926
1232
  runFinished = true;
1233
+ if (event.outcome?.type === "interrupt") replayInterruptMessageId = assistantMessageId ?? replayTail;
927
1234
  const ids = event.outcome?.type === "success" ? event.outcome.pendingToolCallIds : void 0;
928
- restrictedPendingToolCallIds = ids?.length ? new Set(ids) : void 0;
1235
+ restrictedPendingToolCallIds = ids === void 0 ? void 0 : new Set(ids);
1236
+ }
1237
+ if (event.type === "TOOL_CALL_RESULT") {
1238
+ if (!assistantCollided && candidateContexts.size) accountedResultIds.add(event.toolCallId);
1239
+ for (const openToolCallIds of heldMessages.values()) openToolCallIds.delete(event.toolCallId);
929
1240
  }
930
- if (event.type === "TOOL_CALL_RESULT") for (const openToolCallIds of heldMessages.values()) openToolCallIds.delete(event.toolCallId);
931
1241
  settlingRun = event.type === "RUN_FINISHED" || event.type === "RUN_ERROR" || event.type === "RUN_CANCELLED";
1242
+ const beforeSnapshot = event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
932
1243
  try {
933
- const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
934
- if (event.type === "MESSAGES_SNAPSHOT") assistantMessageId = nextAssistantMessageId;
1244
+ const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId, joinStrategy, runStartMessageIds, hasRunUserBoundary, candidateContexts.size ? reconcileReplaySnapshot : void 0);
1245
+ if (event.type === "MESSAGES_SNAPSHOT") {
1246
+ for (const message of beforeSnapshot) {
1247
+ const context = replayContexts.get(message.id);
1248
+ const current = this.session.tryGetMessage(message.id)?.message;
1249
+ if (context && current?.content !== message.content) {
1250
+ context.merge = createReplayContentMerger();
1251
+ frameOwnerIds.delete(message.id);
1252
+ }
1253
+ }
1254
+ assistantMessageId = nextAssistantMessageId;
1255
+ const imported = assistantMessageId ? this.session.tryGetMessage(assistantMessageId)?.message : void 0;
1256
+ const source = event.messages.find((message) => message?.role === "assistant" && message.id === imported?.id);
1257
+ latestSnapshot = imported?.role === "assistant" && source ? imported : void 0;
1258
+ snapshotToolCallIds = new Set(Array.isArray(source?.toolCalls) ? source.toolCalls.map((call) => call.id).filter((id) => typeof id === "string") : []);
1259
+ if (!replayEnded && assistantMessageId === void 0) {
1260
+ const tail = this.session.getMessages().at(-1);
1261
+ snapshotAssistantId = tail?.role === "assistant" ? tail.id : void 0;
1262
+ }
1263
+ }
935
1264
  } finally {
936
1265
  settlingRun = false;
937
1266
  }
@@ -974,7 +1303,6 @@ var AgUiThreadController = class {
974
1303
  const input = this.buildRunInput(runId, normalizedRunConfig, historicalMessages, resume);
975
1304
  const subscriber = createAgUiSubscriber({
976
1305
  dispatch,
977
- runId,
978
1306
  logger: this.logger,
979
1307
  onRunFailed: (error) => {
980
1308
  if (abortSignal.aborted) return;
@@ -985,7 +1313,7 @@ var AgUiThreadController = class {
985
1313
  try {
986
1314
  runAgentInstance.messages = input.messages;
987
1315
  runAgentInstance.threadId = input.threadId;
988
- runAgentInstance.state = input.state ?? null;
1316
+ runAgentInstance.state = input.state;
989
1317
  } catch {}
990
1318
  await runAgentInstance.runAgent.bind(runAgentInstance)(input, subscriber, { signal: abortSignal });
991
1319
  }
@@ -1000,6 +1328,19 @@ var AgUiThreadController = class {
1000
1328
  pendingError = err;
1001
1329
  }
1002
1330
  } finally {
1331
+ const current = assistantMessageId ? this.session.tryGetMessage(assistantMessageId)?.message : void 0;
1332
+ if (this.abortController === abortController && current?.role === "assistant" && current.status?.type === "running") {
1333
+ const status = {
1334
+ type: "incomplete",
1335
+ reason: "error",
1336
+ error: "The connection ended before the answer finished."
1337
+ };
1338
+ settleHeldMessages(status);
1339
+ applyUpdate({
1340
+ status,
1341
+ content: settleNestedContent(current.content, status)
1342
+ });
1343
+ }
1003
1344
  this.finishRun(abortController);
1004
1345
  }
1005
1346
  if (pendingError) {
@@ -1008,10 +1349,40 @@ var AgUiThreadController = class {
1008
1349
  this.clearDeferredContinuations(abortController);
1009
1350
  throw err;
1010
1351
  }
1352
+ const pending = replayInterruptMessageId ? this.getPendingInterrupts() : null;
1353
+ if (pending && pending.messageId === replayInterruptMessageId && replayContexts.has(pending.messageId) && !abortSignal.aborted && this.abortController === null && generation === this.state.historyMutationGeneration && !this.isThreadSwitching?.()) {
1354
+ const message = this.session.tryGetMessage(pending.messageId)?.message;
1355
+ const responses = message?.role === "assistant" ? buildToolApprovalResume(message.content, pending.interrupts) : null;
1356
+ if (responses) {
1357
+ this.clearDeferredContinuations(abortController);
1358
+ try {
1359
+ await this.submitInterruptResponses(responses);
1360
+ } catch (error) {
1361
+ const err = error instanceof Error ? error : new Error(String(error));
1362
+ if (!this.reportedErrors.has(err)) {
1363
+ this.reportError(err);
1364
+ this.reportedErrors.add(err);
1365
+ }
1366
+ throw err;
1367
+ }
1368
+ return;
1369
+ }
1370
+ }
1011
1371
  if (this.state.pendingResume?.owner === abortController) {
1012
- const { messageId } = this.state.pendingResume;
1372
+ const { messageIds } = this.state.pendingResume;
1013
1373
  this.dispatch({ type: "resumeCleared" });
1014
- if (!abortSignal.aborted) this.startResumeRun(messageId);
1374
+ if (!abortSignal.aborted && !this.getPendingInterrupts() && !this.getPendingToolCalls()) {
1375
+ const turn = this.getTrailingAssistantTurn();
1376
+ if (turn.some((message) => {
1377
+ if (!messageIds.includes(message.id)) return false;
1378
+ const replay = replayContexts.get(message.id);
1379
+ if (!replay) return hasResolvedToolCalls(message);
1380
+ return Array.from(iterateToolCallParts(message.content)).some((part) => isResolvedToolCall(part) && replay.admittedToolCallIds.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId));
1381
+ })) {
1382
+ const headId = turn.at(-1)?.id;
1383
+ if (headId !== void 0) this.startResumeRun(headId);
1384
+ }
1385
+ }
1015
1386
  }
1016
1387
  if (this.state.pendingA2uiResumeOwner === abortController) {
1017
1388
  this.dispatch({ type: "a2uiResumeCleared" });
@@ -1092,7 +1463,7 @@ var AgUiThreadController = class {
1092
1463
  return {
1093
1464
  threadId,
1094
1465
  runId,
1095
- state: this.state.stateSnapshot ?? null,
1466
+ state: this.state.stateSnapshot === void 0 ? {} : this.state.stateSnapshot,
1096
1467
  messages,
1097
1468
  tools: toAgUiTools(context?.tools),
1098
1469
  context: [...context?.system ? [{
@@ -1194,15 +1565,47 @@ var AgUiThreadController = class {
1194
1565
  this.publishUpdate();
1195
1566
  return !collidesWithExisting;
1196
1567
  }
1197
- updateAssistantMessage(messageId, update) {
1568
+ updateAssistantMessage(messageId, update, replay, pendingToolCallIds) {
1198
1569
  let latestStatus;
1199
1570
  if (!this.session.updateMessage(messageId, (message) => {
1200
1571
  if (message.role !== "assistant") return message;
1201
1572
  const assistant = message;
1202
1573
  const mergedMetadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
1203
1574
  latestStatus = update.status ?? assistant.status;
1204
- const metadata = latestStatus?.type === "requires-action" && latestStatus.reason === "tool-calls" ? mergedMetadata : withoutPendingToolCallIds(mergedMetadata);
1205
- const content = update.content !== void 0 ? this.preserveToolResults(assistant.content, update.content) : assistant.content;
1575
+ let content = update.content !== void 0 ? replay ? replay.merge(assistant.content, update.content) : this.preserveToolResults(assistant.content, update.content) : assistant.content;
1576
+ const calls = replay ? Array.from(iterateToolCallParts(content)) : [];
1577
+ const pending = pendingToolCallIds ?? (replay ? /* @__PURE__ */ new Set([...replay.admittedToolCallIds, ...Array.from(iterateToolCallParts(update.content ?? [])).filter((part) => !isResolvedToolCall(part)).map((part) => part.toolCallId)]) : void 0);
1578
+ if (replay && latestStatus?.type !== "running" && pending) replay.admittedToolCallIds = pending;
1579
+ if (replay) {
1580
+ const interrupts = latestStatus?.type === "requires-action" && latestStatus.reason === "interrupt" ? mergedMetadata.custom["agui"]?.interrupts : void 0;
1581
+ const approvals = projectAgUiToolApprovals(interrupts, new Set(calls.map((part) => part.toolCallId)));
1582
+ const originalCalls = new Map(Array.from(iterateToolCallParts(replay.originalContent), (part) => [part.toolCallId, part]));
1583
+ content = mapToolCallPartsDeep(content, (part) => {
1584
+ const approval = approvals.get(part.toolCallId);
1585
+ if (approval) return {
1586
+ ...part,
1587
+ approval: preserveToolApprovalDecision(originalCalls.get(part.toolCallId), {
1588
+ ...part,
1589
+ approval
1590
+ }) ?? approval
1591
+ };
1592
+ if (!part.approval || part.approval.approved !== void 0 || part.approval.resolution) return part;
1593
+ const { approval: _approval, ...withoutApproval } = part;
1594
+ return withoutApproval;
1595
+ }).content;
1596
+ }
1597
+ if (replay && latestStatus?.type === "complete") {
1598
+ const listed = pending ?? listedPendingToolCallIds({
1599
+ ...assistant,
1600
+ metadata: mergedMetadata
1601
+ });
1602
+ if (calls.some((part) => (!isResolvedToolCall(part) || !replay.accountedResultIds.has(part.toolCallId)) && (!listed || listed.has(part.toolCallId)))) latestStatus = {
1603
+ type: "requires-action",
1604
+ reason: "tool-calls"
1605
+ };
1606
+ }
1607
+ const admission = pending && replay ? withPendingToolCallIds(mergedMetadata, pending) : mergedMetadata;
1608
+ const metadata = latestStatus?.type === "requires-action" && latestStatus.reason === "tool-calls" ? admission : replay && latestStatus?.type === "running" ? mergedMetadata : withoutPendingToolCallIds(admission);
1206
1609
  return {
1207
1610
  ...assistant,
1208
1611
  content,
@@ -1217,24 +1620,25 @@ var AgUiThreadController = class {
1217
1620
  resolvedMessageId = stableId;
1218
1621
  } else this.publishUpdate();
1219
1622
  if (this.isPersistableStatus(latestStatus)) this.persistAssistantHistory(resolvedMessageId);
1220
- this.maybeResumeAfterToolResults(resolvedMessageId);
1623
+ this.maybeResumeAfterToolResults(resolvedMessageId, replay?.accountedResultIds);
1221
1624
  return resolvedMessageId;
1222
1625
  }
1223
- preserveToolResults(previous, next) {
1626
+ preserveToolResults(previous, next, decisionSource) {
1224
1627
  const preserved = /* @__PURE__ */ new Map();
1225
1628
  for (const part of iterateToolCallParts(previous)) if (isResolvedToolCall(part)) preserved.set(part.toolCallId, part);
1226
- if (preserved.size === 0) return this.preserveToolInteractions(previous, next);
1629
+ const decisions = new Map(Array.from(iterateToolCallParts(decisionSource ?? [])).filter((part) => part.approval?.approved !== void 0).map((part) => [part.toolCallId, part]));
1630
+ if (preserved.size === 0 && decisions.size === 0) return this.preserveToolInteractions(previous, next);
1227
1631
  const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
1228
1632
  const prior = preserved.get(part.toolCallId);
1229
- if (!prior) return part;
1230
- if (!isResolvedToolCall(part) && isResolvedToolCall(prior)) return {
1231
- ...part,
1232
- result: prior.result,
1233
- ...prior.artifact !== void 0 ? { artifact: prior.artifact } : {},
1234
- ...prior.isError !== void 0 ? { isError: prior.isError } : {},
1235
- ...prior.modelContent !== void 0 ? { modelContent: prior.modelContent } : {}
1236
- };
1237
- return part;
1633
+ const result = prior ? preserveToolResult(prior, part) : part;
1634
+ if (result.approval && decisions.has(part.toolCallId)) {
1635
+ const approval = preserveToolApprovalDecision(decisions.get(part.toolCallId), result);
1636
+ if (approval && approval !== result.approval) return {
1637
+ ...result,
1638
+ approval
1639
+ };
1640
+ }
1641
+ return result;
1238
1642
  });
1239
1643
  return this.preserveToolInteractions(previous, changed ? merged : next);
1240
1644
  }
@@ -1252,10 +1656,10 @@ var AgUiThreadController = class {
1252
1656
  });
1253
1657
  return changed ? content : next;
1254
1658
  }
1255
- mergeSnapshotAssistantContent(previous, next, snapshotHasReasoning, snapshotHasActivity) {
1256
- const shouldKeep = (part) => part.type === "reasoning" ? this.showThinking && !snapshotHasReasoning : isActivityPart(part) && !snapshotHasActivity;
1659
+ mergeSnapshotAssistantContent(previous, next, snapshotHasReasoning, snapshotHasActivity, keepMissingToolCalls = /* @__PURE__ */ new Set(), keepPriorTranscripts = false) {
1660
+ const shouldKeep = (part) => part.type === "tool-call" ? keepMissingToolCalls.has(part.toolCallId) && !next.some((incoming) => incoming.type === "tool-call" && incoming.toolCallId === part.toolCallId) : part.type === "reasoning" ? this.showThinking && !snapshotHasReasoning : part.type === "data" && !isActivityPart(part) ? !next.some((incoming) => incoming.type === "data" && incoming.name === part.name) : isActivityPart(part) && !snapshotHasActivity;
1257
1661
  const kept = previous.filter(shouldKeep);
1258
- const merged = this.reconcileSnapshotNestedContent(previous, this.preserveToolInteractions(previous, next));
1662
+ const merged = this.reconcileSnapshotNestedContent(previous, this.preserveToolInteractions(previous, next), keepPriorTranscripts);
1259
1663
  if (kept.length === 0) return merged;
1260
1664
  const mergedByType = /* @__PURE__ */ new Map();
1261
1665
  const mergedTools = /* @__PURE__ */ new Map();
@@ -1287,7 +1691,7 @@ var AgUiThreadController = class {
1287
1691
  result.push(...insertions[merged.length]);
1288
1692
  return result;
1289
1693
  }
1290
- reconcileSnapshotNestedContent(previous, next) {
1694
+ reconcileSnapshotNestedContent(previous, next, keepPriorTranscripts) {
1291
1695
  const nestedIds = /* @__PURE__ */ new Set();
1292
1696
  for (const part of previous) {
1293
1697
  if (part.type !== "tool-call" || !part.messages) continue;
@@ -1317,12 +1721,50 @@ var AgUiThreadController = class {
1317
1721
  if (part.type === "tool-call" && flattened.has(part.toolCallId)) return [];
1318
1722
  if (part.type !== "tool-call") return [part];
1319
1723
  const prior = previousCalls.get(part.toolCallId);
1320
- return prior?.messages === void 0 ? [part] : [{
1724
+ if (prior?.messages === void 0) return [part];
1725
+ if (part.messages === void 0) return [{
1321
1726
  ...part,
1322
1727
  messages: prior.messages
1323
1728
  }];
1729
+ if (part.messages.length === 0) return [part];
1730
+ return [{
1731
+ ...part,
1732
+ messages: this.mergeNestedTranscripts(prior.messages, part.messages, keepPriorTranscripts)
1733
+ }];
1324
1734
  });
1325
1735
  }
1736
+ /**
1737
+ * Merges one tool call's nested transcripts message by message. The incoming
1738
+ * copy is the base for tool calls and reasoning; resolved results and calls
1739
+ * only the prior copy has carry over, and the prior text wins when it extends
1740
+ * the incoming text. Importing a snapshot keeps prior messages it lacks, a
1741
+ * prior status that is running or incomplete, and the prior status when the
1742
+ * snapshot waits on calls the merged content has already resolved.
1743
+ */
1744
+ mergeNestedTranscripts(prior, next, importingSnapshot) {
1745
+ const priorById = new Map(prior.map((message) => [message.id, message]));
1746
+ const merged = next.map((message) => {
1747
+ const previous = priorById.get(message.id);
1748
+ if (previous?.role !== "assistant" || message.role !== "assistant") return message;
1749
+ let content = this.mergeSnapshotAssistantContent(previous.content, this.preserveToolResults(previous.content, message.content), message.content.some((part) => part.type === "reasoning"), message.content.some(isActivityPart), new Set(Array.from(iterateToolCallParts(previous.content), (call) => call.toolCallId)), importingSnapshot);
1750
+ const priorText = textOf(previous.content);
1751
+ const incomingText = textOf(message.content);
1752
+ if (priorText.length > incomingText.length && priorText.startsWith(incomingText)) content = replaceTextParts(content, previous.content);
1753
+ const status = !importingSnapshot ? message.status : previous.status.type === "running" || previous.status.type === "incomplete" || message.status.type === "requires-action" && message.status.reason === "tool-calls" && !Array.from(iterateToolCallParts(content)).some((call) => !isResolvedToolCall(call)) ? previous.status : message.status;
1754
+ return {
1755
+ ...message,
1756
+ status,
1757
+ content,
1758
+ metadata: {
1759
+ ...message.metadata,
1760
+ custom: mergeAgUiNamespace(previous.metadata.custom, message.metadata.custom)
1761
+ }
1762
+ };
1763
+ });
1764
+ if (!importingSnapshot) return merged;
1765
+ const mergedById = new Map(merged.map((message) => [message.id, message]));
1766
+ return [...prior.map((message) => mergedById.get(message.id) ?? message), ...merged.filter((message) => !priorById.has(message.id))];
1767
+ }
1326
1768
  mergeAssistantMetadata(current, incoming) {
1327
1769
  const annotations = incoming.unstable_annotations ? [...current.unstable_annotations, ...incoming.unstable_annotations] : current.unstable_annotations;
1328
1770
  const data = incoming.unstable_data ? [...current.unstable_data, ...incoming.unstable_data] : current.unstable_data;
@@ -1337,7 +1779,8 @@ var AgUiThreadController = class {
1337
1779
  custom: incoming.custom ? mergeAgUiNamespace(current.custom, incoming.custom) : current.custom
1338
1780
  };
1339
1781
  }
1340
- handleEvent(aggregator, event, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1782
+ handleEvent(aggregator, event, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot) {
1783
+ if (event.type === "SUBAGENT_STARTED" && !this.subagentParentToolCallIds.has(event.subagentRunId)) this.subagentParentToolCallIds.set(event.subagentRunId, event.parentToolCallId);
1341
1784
  switch (event.type) {
1342
1785
  case "STATE_SNAPSHOT":
1343
1786
  this.dispatch({
@@ -1360,12 +1803,26 @@ var AgUiThreadController = class {
1360
1803
  }
1361
1804
  break;
1362
1805
  case "MESSAGES_SNAPSHOT": {
1806
+ aggregator.handle(event);
1363
1807
  const previousHeadId = this.session.headId;
1364
1808
  const activeAssistant = activeAssistantId === void 0 ? void 0 : this.session.tryGetMessage(activeAssistantId)?.message;
1365
1809
  const hasActiveText = activeAssistant?.role === "assistant" && activeAssistant.content.some((part) => part.type === "text");
1366
- this.importMessagesSnapshot(event.messages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
1810
+ this.importMessagesSnapshot(event.messages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot);
1367
1811
  const headId = this.session.headId;
1368
- if (hasActiveText && headId !== previousHeadId) activeAssistantId = (headId === null ? void 0 : this.session.tryGetMessage(headId)?.message)?.role === "assistant" ? headId ?? void 0 : void 0;
1812
+ const head = headId === null ? void 0 : this.session.tryGetMessage(headId)?.message;
1813
+ const hasNewPendingToolCallAnswer = head?.role === "assistant" && !runStartMessageIds.has(head.id) && head.status?.type === "requires-action" && head.status.reason === "tool-calls";
1814
+ if (headId !== previousHeadId && (hasActiveText || hasNewPendingToolCallAnswer)) {
1815
+ const nextActiveAssistantId = head?.role === "assistant" ? headId ?? void 0 : void 0;
1816
+ if (activeAssistantId !== nextActiveAssistantId && activeAssistantId) {
1817
+ const previous = this.session.tryGetMessage(activeAssistantId)?.message;
1818
+ if (previous?.role === "assistant" && previous.status?.type === "running") this.updateAssistantMessage(activeAssistantId, {
1819
+ status: FALLBACK_USER_STATUS,
1820
+ content: settleNestedContent(previous.content, FALLBACK_USER_STATUS)
1821
+ });
1822
+ }
1823
+ activeAssistantId = nextActiveAssistantId;
1824
+ if (hasNewPendingToolCallAnswer && head) this.updateAssistantMessage(head.id, { status: { type: "running" } });
1825
+ }
1369
1826
  this.updateActiveAssistantState(activeAssistantId);
1370
1827
  break;
1371
1828
  }
@@ -1427,7 +1884,8 @@ var AgUiThreadController = class {
1427
1884
  text: modelText
1428
1885
  }] } : {},
1429
1886
  ...part.isError === void 0 ? typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {} : {},
1430
- ...event.messageId ? { unstable_toolMessageId: event.messageId } : {}
1887
+ ...event.messageId ? { unstable_toolMessageId: event.messageId } : {},
1888
+ ...event.metadata !== void 0 ? { providerMetadata: withAgUiPartMessageMetadata(part.providerMetadata, event.metadata) } : {}
1431
1889
  };
1432
1890
  const { modelContent: _modelContent, ...partWithoutModelContent } = part;
1433
1891
  return {
@@ -1439,7 +1897,8 @@ var AgUiThreadController = class {
1439
1897
  }] } : {},
1440
1898
  ...typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {},
1441
1899
  ...part.mcp === void 0 && mcpAppUri !== void 0 ? { mcp: { app: { resourceUri: mcpAppUri } } } : {},
1442
- ...event.messageId ? { unstable_toolMessageId: event.messageId } : {}
1900
+ ...event.messageId ? { unstable_toolMessageId: event.messageId } : {},
1901
+ ...event.metadata !== void 0 ? { providerMetadata: withAgUiPartMessageMetadata(part.providerMetadata, event.metadata) } : {}
1443
1902
  };
1444
1903
  });
1445
1904
  if (!matchedToolCall) return message;
@@ -1449,7 +1908,8 @@ var AgUiThreadController = class {
1449
1908
  };
1450
1909
  })) return;
1451
1910
  this.publishUpdate();
1452
- this.maybeCompleteAfterToolResults(messageId);
1911
+ if (this.maybeCompleteAfterToolResults(messageId)) return;
1912
+ this.persistSettledAssistant(messageId);
1453
1913
  }
1454
1914
  applyCrossRunActivitySnapshot(messageId, toolCallId, event) {
1455
1915
  const resourceUri = event.content["resourceUri"];
@@ -1488,18 +1948,21 @@ var AgUiThreadController = class {
1488
1948
  };
1489
1949
  })) return;
1490
1950
  this.publishUpdate();
1491
- this.maybeCompleteAfterToolResults(messageId);
1951
+ if (this.maybeCompleteAfterToolResults(messageId)) return;
1952
+ this.persistSettledAssistant(messageId);
1492
1953
  }
1493
- importMessagesSnapshot(rawMessages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1954
+ importMessagesSnapshot(rawMessages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot) {
1494
1955
  try {
1495
1956
  const activeAssistantItem = activeAssistantId ? this.session.tryGetMessage(activeAssistantId) : void 0;
1496
1957
  const activeMessage = activeAssistantItem?.message;
1497
1958
  const activeAssistantParentId = activeAssistantItem?.parentId;
1498
1959
  const activeAssistant = activeMessage?.role === "assistant" ? activeMessage : void 0;
1499
- const normalized = fromAgUiMessages(rawMessages, { showThinking: this.showThinking });
1960
+ const normalized = fromAgUiSnapshotMessages(rawMessages, { showThinking: this.showThinking }, this.subagentParentToolCallIds);
1500
1961
  const converted = [];
1501
1962
  for (const message of normalized) try {
1502
- converted.push(fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS));
1963
+ const previous = message.id ? this.session.tryGetMessage(message.id)?.message : void 0;
1964
+ const fallbackStatus = previous?.role === "assistant" && previous.status?.type === "running" ? previous.status : FALLBACK_USER_STATUS;
1965
+ converted.push(fromThreadMessageLike(message, generateId(), fallbackStatus));
1503
1966
  } catch (error) {
1504
1967
  this.logger.error?.("[agui] failed to import message from snapshot", error);
1505
1968
  }
@@ -1508,10 +1971,16 @@ var AgUiThreadController = class {
1508
1971
  for (let index = 0; index < converted.length; index++) {
1509
1972
  const convertedMessage = converted[index];
1510
1973
  const existing = this.session.tryGetMessage(convertedMessage.id)?.message;
1511
- if (convertedMessage.role === "assistant" && existing?.role === "assistant") converted[index] = {
1512
- ...convertedMessage,
1513
- content: this.mergeSnapshotAssistantContent(existing.content, convertedMessage.content, snapshotHasReasoning, snapshotHasActivity)
1514
- };
1974
+ if (convertedMessage.role === "assistant") {
1975
+ const previous = existing?.role === "assistant" ? existing : void 0;
1976
+ const snapshot = reconcileSnapshot?.(convertedMessage, previous) ?? convertedMessage;
1977
+ const preservesRunningStatus = convertedMessage.id === activeAssistantId && previous?.status?.type === "running" && snapshot.status?.type === "requires-action" && snapshot.status.reason === "tool-calls";
1978
+ converted[index] = previous ? {
1979
+ ...snapshot,
1980
+ ...preservesRunningStatus ? { status: previous.status } : {},
1981
+ content: this.mergeSnapshotAssistantContent(previous.content, snapshot.content, snapshotHasReasoning, snapshotHasActivity, /* @__PURE__ */ new Set(), true)
1982
+ } : snapshot;
1983
+ }
1515
1984
  }
1516
1985
  if (joinStrategy === "concat-content") {
1517
1986
  const activeIndex = converted.findIndex((message) => message.id === activeAssistantId);
@@ -1594,6 +2063,14 @@ var AgUiThreadController = class {
1594
2063
  if (!this.history) return;
1595
2064
  this.assistantHistoryParents.set(messageId, parentId);
1596
2065
  }
2066
+ persistSettledAssistant(messageId) {
2067
+ const item = this.session.tryGetMessage(messageId);
2068
+ if (!this.history?.update || !item) return;
2069
+ if (!this.isTerminalStatus(item.message.status)) return;
2070
+ if (!this.persistedHistoryIds.has(messageId) && this.historyWrites.pending(messageId) === void 0) return;
2071
+ this.markPendingAssistantHistory(messageId, item.parentId);
2072
+ this.persistAssistantHistory(messageId);
2073
+ }
1597
2074
  persistAssistantHistory(messageId) {
1598
2075
  const history = this.history;
1599
2076
  if (!history) return;
@@ -1604,7 +2081,7 @@ var AgUiThreadController = class {
1604
2081
  if (!this.isPersistableStatus(message.status)) return;
1605
2082
  const wasPersisted = this.persistedHistoryIds.has(messageId);
1606
2083
  const update = history.update;
1607
- if (update !== void 0 && (wasPersisted || this.snapshotHistoryIds.has(messageId))) {
2084
+ if (update !== void 0 && (wasPersisted || this.snapshotHistoryIds.has(messageId) || this.historyWrites.pending(messageId) !== void 0)) {
1608
2085
  const write = this.historyWrites.chain(messageId, () => update.call(history, {
1609
2086
  parentId,
1610
2087
  message
@@ -1647,7 +2124,6 @@ var AgUiThreadController = class {
1647
2124
  return write;
1648
2125
  }
1649
2126
  };
1650
- //#endregion
1651
2127
  export { AgUiThreadController };
1652
2128
 
1653
2129
  //# sourceMappingURL=AgUiThreadController.js.map