@assistant-ui/react-ag-ui 0.1.0 → 0.1.1

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 (40) hide show
  1. package/dist/agUiExtras.js +1 -2
  2. package/dist/hooks.js +1 -2
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadController.js +354 -68
  5. package/dist/runtime/AgUiThreadController.js.map +1 -1
  6. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  7. package/dist/runtime/adapter/conversions.js +10 -3
  8. package/dist/runtime/adapter/conversions.js.map +1 -1
  9. package/dist/runtime/adapter/replay-content.d.ts +9 -0
  10. package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
  11. package/dist/runtime/adapter/replay-content.js +185 -0
  12. package/dist/runtime/adapter/replay-content.js.map +1 -0
  13. package/dist/runtime/adapter/run-aggregator.js +1 -2
  14. package/dist/runtime/adapter/subscriber.js +1 -2
  15. package/dist/runtime/adapter/tool-approval.d.ts +2 -0
  16. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  17. package/dist/runtime/adapter/tool-approval.js +23 -6
  18. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  19. package/dist/runtime/agUiThreadState.d.ts +1 -1
  20. package/dist/runtime/agUiThreadState.d.ts.map +1 -1
  21. package/dist/runtime/agUiThreadState.js +3 -4
  22. package/dist/runtime/agUiThreadState.js.map +1 -1
  23. package/dist/runtime/event-parser.js +1 -2
  24. package/dist/runtime/interrupt-internals.js +1 -2
  25. package/dist/runtime/logger.js +1 -2
  26. package/dist/runtime/mcp-tool-result.js +1 -2
  27. package/dist/runtime/types.js +1 -2
  28. package/dist/useAgUiRuntime.js +1 -2
  29. package/package.json +5 -5
  30. package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
  31. package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
  32. package/src/runtime/AgUiThreadController.test.ts +79 -0
  33. package/src/runtime/AgUiThreadController.ts +725 -109
  34. package/src/runtime/adapter/conversions.content.test.ts +113 -1
  35. package/src/runtime/adapter/conversions.ts +18 -8
  36. package/src/runtime/adapter/replay-content.test.ts +235 -0
  37. package/src/runtime/adapter/replay-content.ts +306 -0
  38. package/src/runtime/adapter/tool-approval.ts +41 -3
  39. package/src/runtime/agUiThreadState.test.ts +21 -2
  40. package/src/runtime/agUiThreadState.ts +12 -3
@@ -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
6
  import { RunAggregator, isPlainObject, tryParseJSON } from "./adapter/run-aggregator.js";
7
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,6 +37,21 @@ 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",
@@ -299,7 +320,7 @@ var AgUiThreadController = class {
299
320
  const historyImport = this.waitForHistoryImport();
300
321
  if (historyImport && !await historyImport) return;
301
322
  this.assertNoPendingInterrupts();
302
- await this.startRun(config.parentId, config.runConfig ?? this.state.lastRunConfig, void 0, config.stream);
323
+ await this.startRun(config.parentId, config.runConfig ?? this.state.lastRunConfig, void 0, config.stream, true);
303
324
  }
304
325
  async resumeInFlightRun(messages) {
305
326
  const historyImport = this.waitForHistoryImport();
@@ -321,9 +342,7 @@ var AgUiThreadController = class {
321
342
  throw new Error("[agui] cannot start a new run while interrupts are pending; resolve them with submitInterruptResponses()");
322
343
  }
323
344
  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;
345
+ return this.getTrailingAssistantTurn().findLast((message) => message.status?.type === "requires-action" && message.status.reason === reason) ?? null;
327
346
  }
328
347
  getTrailingAssistantTurn() {
329
348
  const messages = this.getMessages();
@@ -383,8 +402,9 @@ var AgUiThreadController = class {
383
402
  * unretryable.
384
403
  */
385
404
  async resumeWithResponses(messageId, resume) {
405
+ const parentId = this.session.headId ?? messageId;
386
406
  this.clearPendingInterrupts(messageId, resume);
387
- await this.startRun(messageId, this.state.lastRunConfig, {
407
+ await this.startRun(parentId, this.state.lastRunConfig, {
388
408
  entries: resume,
389
409
  interruptedMessageId: messageId
390
410
  });
@@ -657,23 +677,27 @@ var AgUiThreadController = class {
657
677
  }
658
678
  this.startResumeRun(parentId);
659
679
  }
660
- maybeResumeAfterToolResults(messageId) {
680
+ maybeResumeAfterToolResults(messageId, accountedResultIds) {
661
681
  if (!this.maybeCompleteAfterToolResults(messageId)) return;
682
+ if (accountedResultIds) {
683
+ const message = this.session.tryGetMessage(messageId)?.message;
684
+ if (message?.role !== "assistant" || !Array.from(iterateToolCallParts(message.content)).some((part) => isResolvedToolCall(part) && !accountedResultIds.has(part.toolCallId))) return;
685
+ }
662
686
  if (this.getPendingInterrupts()) return;
663
687
  const turn = this.getTrailingAssistantTurn();
664
688
  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
689
  const owner = this.abortController;
669
690
  if (owner) {
670
691
  this.dispatch({
671
692
  type: "resumeDeferred",
672
693
  owner,
673
- messageId: headId
694
+ messageId
674
695
  });
675
696
  return;
676
697
  }
698
+ if (this.getPendingToolCalls()) return;
699
+ const headId = turn.at(-1)?.id;
700
+ if (headId === void 0) return;
677
701
  this.startResumeRun(headId);
678
702
  }
679
703
  maybeCompleteAfterToolResults(messageId) {
@@ -729,6 +753,10 @@ var AgUiThreadController = class {
729
753
  let lastAppliedId = null;
730
754
  let hardReplace = false;
731
755
  const seen = /* @__PURE__ */ new Set();
756
+ const retainToolResults = (existing, message) => existing?.role === "assistant" && message.role === "assistant" ? {
757
+ ...message,
758
+ content: this.preserveToolResults(existing.content, message.content)
759
+ } : message;
732
760
  for (const message of messages) {
733
761
  if (seen.has(message.id)) continue;
734
762
  seen.add(message.id);
@@ -737,11 +765,12 @@ var AgUiThreadController = class {
737
765
  hardReplace = true;
738
766
  break;
739
767
  }
740
- this.session.addOrUpdateMessage(expectedParentId, message);
768
+ this.session.addOrUpdateMessage(expectedParentId, retainToolResults(existing?.message, message));
741
769
  expectedParentId = message.id;
742
770
  lastAppliedId = message.id;
743
771
  }
744
772
  if (hardReplace) {
773
+ const previous = new Map(this.getMessageRepository().messages.map(({ message }) => [message.id, message]));
745
774
  this.session.clear();
746
775
  expectedParentId = null;
747
776
  lastAppliedId = null;
@@ -749,17 +778,29 @@ var AgUiThreadController = class {
749
778
  for (const message of messages) {
750
779
  if (seen.has(message.id)) continue;
751
780
  seen.add(message.id);
752
- this.session.addOrUpdateMessage(expectedParentId, message);
781
+ this.session.addOrUpdateMessage(expectedParentId, retainToolResults(previous.get(message.id), message));
753
782
  expectedParentId = message.id;
754
783
  lastAppliedId = message.id;
755
784
  }
756
785
  }
757
786
  this.session.resetHead(lastAppliedId);
787
+ for (const messageId of seen) this.maybeCompleteAfterToolResults(messageId);
758
788
  }
759
789
  this.snapshotHistoryIds.clear();
760
790
  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" });
791
+ const pendingResume = this.state.pendingResume;
792
+ if (pendingResume) {
793
+ const selectedResultOwnerIds = new Set(this.session.getMessages().filter(hasResolvedToolCalls).map((message) => message.id));
794
+ const retained = pendingResume.messageIds.filter((id) => selectedResultOwnerIds.has(id));
795
+ if (retained.length !== pendingResume.messageIds.length) {
796
+ this.dispatch({ type: "resumeCleared" });
797
+ for (const messageId of retained) this.dispatch({
798
+ type: "resumeDeferred",
799
+ owner: pendingResume.owner,
800
+ messageId
801
+ });
802
+ }
803
+ }
763
804
  this.publishUpdate();
764
805
  }
765
806
  loadExternalState(state) {
@@ -799,9 +840,10 @@ var AgUiThreadController = class {
799
840
  }
800
841
  } else this.publishUpdate();
801
842
  }
802
- async startRun(parentId, runConfig, resume, resumeStream) {
843
+ async startRun(parentId, runConfig, resume, resumeStream, replayExistingMessages = false) {
803
844
  if (this.isThreadSwitching?.()) return;
804
845
  if (!this.supersedeActiveRun()) return;
846
+ const generation = this.state.historyMutationGeneration;
805
847
  const normalizedRunConfig = runConfig ?? {};
806
848
  this.dispatch({
807
849
  type: "runConfigRecorded",
@@ -811,13 +853,43 @@ var AgUiThreadController = class {
811
853
  const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.session.headId;
812
854
  const historicalMessages = [...shouldEagerlyInsertAssistant ? this.session.tryGetMessages(parentId) ?? this.session.getMessages() : this.session.getMessages()];
813
855
  const runStartMessageIds = new Set(this.getMessageRepository().messages.map(({ message }) => message.id));
856
+ const selectedMessages = this.session.getMessages();
857
+ const parentIndex = selectedMessages.findIndex((message) => message.id === parentId);
858
+ let replayStart = parentIndex + 1;
859
+ if (selectedMessages[parentIndex]?.role === "assistant") {
860
+ replayStart = parentIndex;
861
+ while (replayStart > 0 && selectedMessages[replayStart - 1]?.role === "assistant") replayStart--;
862
+ }
863
+ const replayMessageIds = [];
864
+ if (replayExistingMessages && !resumeStream && parentIndex >= 0) for (const message of selectedMessages.slice(replayStart)) {
865
+ if (message.role !== "assistant") break;
866
+ replayMessageIds.push(message.id);
867
+ }
868
+ const accountedResultIds = new Set(historicalMessages.flatMap((message) => message.role === "assistant" ? Array.from(iterateToolCallParts(message.content)).filter(isResolvedToolCall).map((part) => part.toolCallId) : []));
869
+ const candidateContexts = /* @__PURE__ */ new Map();
870
+ for (const message of selectedMessages) {
871
+ if (message.role !== "assistant" || !replayMessageIds.includes(message.id)) continue;
872
+ candidateContexts.set(message.id, {
873
+ originalContent: message.content,
874
+ accountedResultIds,
875
+ admittedToolCallIds: actionableToolCallIds(message),
876
+ merge: createReplayContentMerger()
877
+ });
878
+ }
879
+ let replayContexts = /* @__PURE__ */ new Map();
880
+ let lastReplayedIndex = -1;
881
+ let replayEnded = replayMessageIds.length === 0;
882
+ const replayTail = replayMessageIds.at(-1);
814
883
  const joinStrategy = this.joinStrategy;
815
884
  const hasRunUserBoundary = parent?.message.role === "user" && !resume;
816
885
  let pendingError = null;
817
886
  const assistantParentId = parent ? parentId : this.session.headId;
818
887
  let assistantMessageId;
888
+ let snapshotAssistantId;
889
+ let replayInterruptMessageId;
819
890
  let assistantCollided = false;
820
891
  const heldMessages = /* @__PURE__ */ new Map();
892
+ let frameOwnerIds = /* @__PURE__ */ new Set();
821
893
  let restrictedPendingToolCallIds;
822
894
  let settlingRun = false;
823
895
  const ensureAssistant = (allowRecreate = false) => {
@@ -825,22 +897,33 @@ var AgUiThreadController = class {
825
897
  if (cached !== void 0 && this.session.tryGetMessage(cached)) return cached;
826
898
  if (cached !== void 0 && (assistantCollided || !allowRecreate)) return cached;
827
899
  const repositoryHeadId = this.session.headId;
828
- const parentId = (cached === void 0 || shouldEagerlyInsertAssistant && (repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId))) && assistantParentId && this.session.hasMessage(assistantParentId) ? assistantParentId : repositoryHeadId;
900
+ 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
901
  const created = this.insertAssistantPlaceholder(parentId, cached);
830
902
  assistantMessageId = created;
831
903
  this.markPendingAssistantHistory(created, parentId);
832
904
  return created;
833
905
  };
834
- if (shouldEagerlyInsertAssistant) ensureAssistant();
906
+ if (shouldEagerlyInsertAssistant && replayEnded) ensureAssistant();
835
907
  const settleHeldMessages = (terminalStatus) => {
836
908
  const pendingMessages = [];
909
+ const producerPending = restrictedPendingToolCallIds;
910
+ const canWaitForTools = terminalStatus.type === "complete" || terminalStatus.type === "requires-action";
911
+ if (producerPending && canWaitForTools) for (const message of this.getTrailingAssistantTurn()) {
912
+ const replay = replayContexts.get(message.id);
913
+ if (message.id === assistantMessageId || heldMessages.has(message.id) || !replay || message.status?.type === "requires-action" && message.status.reason === "interrupt") continue;
914
+ 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());
915
+ }
837
916
  for (const [messageId, openToolCallIds] of heldMessages) {
917
+ const existing = this.session.tryGetMessage(messageId);
918
+ if (existing) this.markPendingAssistantHistory(messageId, existing.parentId);
838
919
  let requiresToolResult = false;
839
920
  if (this.session.updateMessage(messageId, (message) => {
840
921
  if (message.role !== "assistant") return message;
841
922
  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 ? {
923
+ 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);
924
+ const replay = replayContexts.get(messageId);
925
+ if (replay) replay.admittedToolCallIds = new Set(pendingToolCallIds);
926
+ const status = canWaitForTools ? pendingToolCallIds.length > 0 ? {
844
927
  type: "requires-action",
845
928
  reason: "tool-calls"
846
929
  } : {
@@ -852,16 +935,7 @@ var AgUiThreadController = class {
852
935
  return {
853
936
  ...assistant,
854
937
  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
938
+ metadata: status.type === "requires-action" && (restrictedPendingToolCallIds || replayContexts.has(messageId)) ? withPendingToolCallIds(metadata, pendingToolCallIds) : metadata
865
939
  };
866
940
  })) {
867
941
  this.publishUpdate();
@@ -872,29 +946,139 @@ var AgUiThreadController = class {
872
946
  heldMessages.clear();
873
947
  return pendingMessages;
874
948
  };
949
+ const accountProducerResults = (content) => {
950
+ for (const part of iterateToolCallParts(content)) if (isResolvedToolCall(part)) accountedResultIds.add(part.toolCallId);
951
+ };
952
+ const reconcileReplaySnapshot = (snapshot, previous) => {
953
+ accountProducerResults(snapshot.content);
954
+ const replay = candidateContexts.get(snapshot.id);
955
+ if (!replay) return snapshot;
956
+ replay.admittedToolCallIds = actionableToolCallIds(snapshot);
957
+ if (heldMessages.has(snapshot.id)) heldMessages.set(snapshot.id, new Set(replay.admittedToolCallIds));
958
+ if (!replayContexts.has(snapshot.id)) return snapshot;
959
+ return previous ? {
960
+ ...snapshot,
961
+ content: this.preserveToolResults(previous.content, snapshot.content, replay.originalContent)
962
+ } : snapshot;
963
+ };
875
964
  const applyUpdate = (update) => {
965
+ if (!assistantCollided && replayContexts.size && update.content !== void 0) accountProducerResults(update.content);
966
+ let ownerParts;
967
+ let contentOwnerIds;
968
+ const hasStreamContent = Array.isArray(update.content) && update.content.some((part) => part.type !== "data");
969
+ if (!replayEnded && assistantMessageId === void 0) {
970
+ if (!update.status || update.status.type === "running") return;
971
+ replayEnded = true;
972
+ if (replayContexts.size === 0 && snapshotAssistantId !== void 0 && this.session.headId === snapshotAssistantId && !hasStreamContent) {
973
+ if (update.status.type !== "complete") this.updateAssistantMessage(snapshotAssistantId, {
974
+ status: update.status,
975
+ ...update.metadata ? { metadata: update.metadata } : {}
976
+ });
977
+ return;
978
+ }
979
+ }
980
+ const streamMessageId = assistantMessageId;
981
+ let primaryMessageId = streamMessageId;
876
982
  const terminalStatus = update.status;
983
+ const replayInterrupt = replayContexts.size > 0 && terminalStatus?.type === "requires-action" && terminalStatus.reason === "interrupt";
984
+ if (streamMessageId !== void 0 && !assistantCollided && replayContexts.size > 0 && (update.content !== void 0 || replayInterrupt)) {
985
+ const owners = this.getTrailingAssistantTurn().filter((message) => replayContexts.has(message.id));
986
+ const grouped = update.content !== void 0 ? groupReplayToolContent(owners, update.content, streamMessageId) : /* @__PURE__ */ new Map([[streamMessageId, [...update.content ?? []]]]);
987
+ if (update.content !== void 0) {
988
+ const currentOwnerIds = new Set(grouped.keys());
989
+ for (const ownerId of frameOwnerIds) grouped.set(ownerId, grouped.get(ownerId) ?? []);
990
+ contentOwnerIds = new Set(grouped.keys());
991
+ frameOwnerIds = currentOwnerIds;
992
+ }
993
+ ownerParts = grouped;
994
+ if (replayInterrupt) {
995
+ const interrupts = (update.metadata?.custom?.[AG_UI_METADATA_NAMESPACE])?.interrupts;
996
+ const approvals = projectAgUiToolApprovals(interrupts);
997
+ 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) ?? []);
998
+ const anchor = owners.find((message) => projectAgUiToolApprovals(interrupts, new Set(Array.from(iterateToolCallParts([...message.content, ...grouped.get(message.id) ?? []])).map((part) => part.toolCallId))).size > 0);
999
+ if (anchor) {
1000
+ primaryMessageId = anchor.id;
1001
+ replayInterruptMessageId = anchor.id;
1002
+ grouped.set(anchor.id, grouped.get(anchor.id) ?? []);
1003
+ }
1004
+ }
1005
+ }
877
1006
  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);
1007
+ const frameMessageId = ensureAssistant(hasStreamContent);
1008
+ const messageId = primaryMessageId ?? frameMessageId;
1009
+ let primaryUpdate = update;
1010
+ if (ownerParts) {
1011
+ const interrupt = terminalStatus?.type === "requires-action" && terminalStatus.reason === "interrupt";
1012
+ const ownerStatus = terminalStatus?.type === "requires-action" ? {
1013
+ type: "complete",
1014
+ reason: "unknown"
1015
+ } : terminalStatus;
1016
+ for (const [ownerId, content] of ownerParts) {
1017
+ const existing = this.session.tryGetMessage(ownerId);
1018
+ if (existing) this.markPendingAssistantHistory(ownerId, existing.parentId);
1019
+ if (ownerId === messageId) continue;
1020
+ this.updateAssistantMessage(ownerId, {
1021
+ ...contentOwnerIds?.has(ownerId) ? { content } : {},
1022
+ ...ownerStatus ? { status: ownerStatus } : {}
1023
+ }, replayContexts.get(ownerId), interrupt ? /* @__PURE__ */ new Set() : restrictedPendingToolCallIds);
1024
+ }
1025
+ const { content: _content, ...controlUpdate } = update;
1026
+ const content = ownerParts.get(messageId) ?? [];
1027
+ primaryUpdate = {
1028
+ ...controlUpdate,
1029
+ ...contentOwnerIds?.has(messageId) ? { content } : {},
1030
+ ...ownerStatus && !interrupt && replayContexts.has(messageId) ? { status: ownerStatus } : {}
1031
+ };
1032
+ }
1033
+ const resolved = this.updateAssistantMessage(messageId, primaryUpdate, replayContexts.get(messageId), restrictedPendingToolCallIds);
1034
+ if (messageId === frameMessageId && resolved !== assistantMessageId) assistantMessageId = resolved;
1035
+ for (const messageId of pendingMessages) this.maybeResumeAfterToolResults(messageId, replayMessageIds.includes(messageId) ? accountedResultIds : void 0);
882
1036
  };
883
1037
  const adoptServerMessageId = (serverId, startNewMessage, openToolCallIds = []) => {
1038
+ const existing = this.session.tryGetMessage(serverId);
1039
+ const replayIndex = replayMessageIds.indexOf(serverId);
1040
+ const adoptsReplay = !replayEnded && !assistantCollided && replayIndex > lastReplayedIndex && existing?.message.role === "assistant" && this.session.getMessages().some((message) => message.id === serverId);
1041
+ if (adoptsReplay && replayContexts.size === 0) {
1042
+ replayContexts = candidateContexts;
1043
+ const selected = this.session.getMessages();
1044
+ let tailId = serverId;
1045
+ for (const message of selected.slice(selected.findIndex((message) => message.id === serverId) + 1)) {
1046
+ if (message.role !== "assistant") break;
1047
+ tailId = message.id;
1048
+ }
1049
+ this.session.selectHead(tailId);
1050
+ }
884
1051
  if (startNewMessage && assistantMessageId !== void 0) {
885
- if (openToolCallIds.length > 0) heldMessages.set(assistantMessageId, new Set(openToolCallIds));
1052
+ let hasReplayCallsToHold = false;
1053
+ const replay = replayContexts.get(assistantMessageId);
1054
+ if (replay) {
1055
+ const current = this.session.tryGetMessage(assistantMessageId)?.message;
1056
+ if (current?.role === "assistant") {
1057
+ const retainedPending = Array.from(iterateToolCallParts(current.content)).filter((part) => !isResolvedToolCall(part) || replay.admittedToolCallIds.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId));
1058
+ hasReplayCallsToHold = retainedPending.length > 0;
1059
+ openToolCallIds = [.../* @__PURE__ */ new Set([...openToolCallIds, ...retainedPending.filter((part) => replay.admittedToolCallIds.has(part.toolCallId)).map((part) => part.toolCallId)])];
1060
+ }
1061
+ }
1062
+ if (openToolCallIds.length > 0 || hasReplayCallsToHold) heldMessages.set(assistantMessageId, new Set(openToolCallIds));
886
1063
  else applyUpdate({ status: {
887
1064
  type: "complete",
888
1065
  reason: "unknown"
889
1066
  } });
890
1067
  const previousId = assistantMessageId;
891
1068
  assistantMessageId = void 0;
892
- if (this.session.tryGetMessage(previousId)) {
1069
+ if (!adoptsReplay && this.session.tryGetMessage(previousId)) {
893
1070
  const created = this.insertAssistantPlaceholder(previousId);
894
1071
  assistantMessageId = created;
895
1072
  this.markPendingAssistantHistory(created, previousId);
896
1073
  }
897
1074
  }
1075
+ if (adoptsReplay) {
1076
+ assistantMessageId = serverId;
1077
+ lastReplayedIndex = replayIndex;
1078
+ this.markPendingAssistantHistory(serverId, existing.parentId);
1079
+ return;
1080
+ }
1081
+ replayEnded = true;
898
1082
  const placeholder = ensureAssistant(true);
899
1083
  if (placeholder === serverId) return;
900
1084
  const reassigned = this.reassignAssistantId(placeholder, serverId);
@@ -917,21 +1101,57 @@ var AgUiThreadController = class {
917
1101
  onServerMessageId: (serverId) => {
918
1102
  adoptServerMessageId(serverId, false);
919
1103
  },
920
- onTextMessageStart: (serverId, openToolCallIds) => adoptServerMessageId(serverId, true, openToolCallIds)
1104
+ onTextMessageStart: (serverId, openToolCallIds) => {
1105
+ const owners = this.getTrailingAssistantTurn().filter((message) => replayContexts.has(message.id));
1106
+ const toolOwners = indexReplayToolOwners(owners);
1107
+ const frameOpenIds = openToolCallIds.filter((id) => {
1108
+ const ownerId = toolOwners.get(id);
1109
+ if (!ownerId || ownerId === assistantMessageId || assistantCollided) return true;
1110
+ const held = heldMessages.get(ownerId) ?? /* @__PURE__ */ new Set();
1111
+ held.add(id);
1112
+ heldMessages.set(ownerId, held);
1113
+ return false;
1114
+ });
1115
+ for (const ownerId of frameOwnerIds) {
1116
+ const replay = replayContexts.get(ownerId);
1117
+ if (replay) replay.merge = createReplayContentMerger();
1118
+ }
1119
+ frameOwnerIds.clear();
1120
+ adoptServerMessageId(serverId, true, frameOpenIds);
1121
+ }
921
1122
  });
922
1123
  let runFinished = false;
923
1124
  const dispatch = (event) => {
924
1125
  if (this.abortController !== abortController) return;
925
1126
  if (event.type === "RUN_FINISHED") {
926
1127
  runFinished = true;
1128
+ if (event.outcome?.type === "interrupt") replayInterruptMessageId = assistantMessageId ?? replayTail;
927
1129
  const ids = event.outcome?.type === "success" ? event.outcome.pendingToolCallIds : void 0;
928
- restrictedPendingToolCallIds = ids?.length ? new Set(ids) : void 0;
1130
+ restrictedPendingToolCallIds = ids === void 0 ? void 0 : new Set(ids);
1131
+ }
1132
+ if (event.type === "TOOL_CALL_RESULT") {
1133
+ if (!assistantCollided && candidateContexts.size) accountedResultIds.add(event.toolCallId);
1134
+ for (const openToolCallIds of heldMessages.values()) openToolCallIds.delete(event.toolCallId);
929
1135
  }
930
- if (event.type === "TOOL_CALL_RESULT") for (const openToolCallIds of heldMessages.values()) openToolCallIds.delete(event.toolCallId);
931
1136
  settlingRun = event.type === "RUN_FINISHED" || event.type === "RUN_ERROR" || event.type === "RUN_CANCELLED";
1137
+ const beforeSnapshot = event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
932
1138
  try {
933
- const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
934
- if (event.type === "MESSAGES_SNAPSHOT") assistantMessageId = nextAssistantMessageId;
1139
+ const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId, joinStrategy, runStartMessageIds, hasRunUserBoundary, candidateContexts.size ? reconcileReplaySnapshot : void 0);
1140
+ if (event.type === "MESSAGES_SNAPSHOT") {
1141
+ for (const message of beforeSnapshot) {
1142
+ const context = replayContexts.get(message.id);
1143
+ const current = this.session.tryGetMessage(message.id)?.message;
1144
+ if (context && current?.content !== message.content) {
1145
+ context.merge = createReplayContentMerger();
1146
+ frameOwnerIds.delete(message.id);
1147
+ }
1148
+ }
1149
+ assistantMessageId = nextAssistantMessageId;
1150
+ if (!replayEnded && assistantMessageId === void 0) {
1151
+ const tail = this.session.getMessages().at(-1);
1152
+ snapshotAssistantId = tail?.role === "assistant" ? tail.id : void 0;
1153
+ }
1154
+ }
935
1155
  } finally {
936
1156
  settlingRun = false;
937
1157
  }
@@ -1008,10 +1228,40 @@ var AgUiThreadController = class {
1008
1228
  this.clearDeferredContinuations(abortController);
1009
1229
  throw err;
1010
1230
  }
1231
+ const pending = replayInterruptMessageId ? this.getPendingInterrupts() : null;
1232
+ if (pending && pending.messageId === replayInterruptMessageId && replayContexts.has(pending.messageId) && !abortSignal.aborted && this.abortController === null && generation === this.state.historyMutationGeneration && !this.isThreadSwitching?.()) {
1233
+ const message = this.session.tryGetMessage(pending.messageId)?.message;
1234
+ const responses = message?.role === "assistant" ? buildToolApprovalResume(message.content, pending.interrupts) : null;
1235
+ if (responses) {
1236
+ this.clearDeferredContinuations(abortController);
1237
+ try {
1238
+ await this.submitInterruptResponses(responses);
1239
+ } catch (error) {
1240
+ const err = error instanceof Error ? error : new Error(String(error));
1241
+ if (!this.reportedErrors.has(err)) {
1242
+ this.reportError(err);
1243
+ this.reportedErrors.add(err);
1244
+ }
1245
+ throw err;
1246
+ }
1247
+ return;
1248
+ }
1249
+ }
1011
1250
  if (this.state.pendingResume?.owner === abortController) {
1012
- const { messageId } = this.state.pendingResume;
1251
+ const { messageIds } = this.state.pendingResume;
1013
1252
  this.dispatch({ type: "resumeCleared" });
1014
- if (!abortSignal.aborted) this.startResumeRun(messageId);
1253
+ if (!abortSignal.aborted && !this.getPendingInterrupts() && !this.getPendingToolCalls()) {
1254
+ const turn = this.getTrailingAssistantTurn();
1255
+ if (turn.some((message) => {
1256
+ if (!messageIds.includes(message.id)) return false;
1257
+ const replay = replayContexts.get(message.id);
1258
+ if (!replay) return hasResolvedToolCalls(message);
1259
+ return Array.from(iterateToolCallParts(message.content)).some((part) => isResolvedToolCall(part) && replay.admittedToolCallIds.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId));
1260
+ })) {
1261
+ const headId = turn.at(-1)?.id;
1262
+ if (headId !== void 0) this.startResumeRun(headId);
1263
+ }
1264
+ }
1015
1265
  }
1016
1266
  if (this.state.pendingA2uiResumeOwner === abortController) {
1017
1267
  this.dispatch({ type: "a2uiResumeCleared" });
@@ -1194,15 +1444,47 @@ var AgUiThreadController = class {
1194
1444
  this.publishUpdate();
1195
1445
  return !collidesWithExisting;
1196
1446
  }
1197
- updateAssistantMessage(messageId, update) {
1447
+ updateAssistantMessage(messageId, update, replay, pendingToolCallIds) {
1198
1448
  let latestStatus;
1199
1449
  if (!this.session.updateMessage(messageId, (message) => {
1200
1450
  if (message.role !== "assistant") return message;
1201
1451
  const assistant = message;
1202
1452
  const mergedMetadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
1203
1453
  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;
1454
+ let content = update.content !== void 0 ? replay ? replay.merge(assistant.content, update.content) : this.preserveToolResults(assistant.content, update.content) : assistant.content;
1455
+ const calls = replay ? Array.from(iterateToolCallParts(content)) : [];
1456
+ const pending = pendingToolCallIds ?? (replay ? /* @__PURE__ */ new Set([...replay.admittedToolCallIds, ...Array.from(iterateToolCallParts(update.content ?? [])).filter((part) => !isResolvedToolCall(part)).map((part) => part.toolCallId)]) : void 0);
1457
+ if (replay && latestStatus?.type !== "running" && pending) replay.admittedToolCallIds = pending;
1458
+ if (replay) {
1459
+ const interrupts = latestStatus?.type === "requires-action" && latestStatus.reason === "interrupt" ? mergedMetadata.custom["agui"]?.interrupts : void 0;
1460
+ const approvals = projectAgUiToolApprovals(interrupts, new Set(calls.map((part) => part.toolCallId)));
1461
+ const originalCalls = new Map(Array.from(iterateToolCallParts(replay.originalContent), (part) => [part.toolCallId, part]));
1462
+ content = mapToolCallPartsDeep(content, (part) => {
1463
+ const approval = approvals.get(part.toolCallId);
1464
+ if (approval) return {
1465
+ ...part,
1466
+ approval: preserveToolApprovalDecision(originalCalls.get(part.toolCallId), {
1467
+ ...part,
1468
+ approval
1469
+ }) ?? approval
1470
+ };
1471
+ if (!part.approval || part.approval.approved !== void 0 || part.approval.resolution) return part;
1472
+ const { approval: _approval, ...withoutApproval } = part;
1473
+ return withoutApproval;
1474
+ }).content;
1475
+ }
1476
+ if (replay && latestStatus?.type === "complete") {
1477
+ const listed = pending ?? listedPendingToolCallIds({
1478
+ ...assistant,
1479
+ metadata: mergedMetadata
1480
+ });
1481
+ if (calls.some((part) => (!isResolvedToolCall(part) || !replay.accountedResultIds.has(part.toolCallId)) && (!listed || listed.has(part.toolCallId)))) latestStatus = {
1482
+ type: "requires-action",
1483
+ reason: "tool-calls"
1484
+ };
1485
+ }
1486
+ const admission = pending && replay ? withPendingToolCallIds(mergedMetadata, pending) : mergedMetadata;
1487
+ const metadata = latestStatus?.type === "requires-action" && latestStatus.reason === "tool-calls" ? admission : replay && latestStatus?.type === "running" ? mergedMetadata : withoutPendingToolCallIds(admission);
1206
1488
  return {
1207
1489
  ...assistant,
1208
1490
  content,
@@ -1217,24 +1499,25 @@ var AgUiThreadController = class {
1217
1499
  resolvedMessageId = stableId;
1218
1500
  } else this.publishUpdate();
1219
1501
  if (this.isPersistableStatus(latestStatus)) this.persistAssistantHistory(resolvedMessageId);
1220
- this.maybeResumeAfterToolResults(resolvedMessageId);
1502
+ this.maybeResumeAfterToolResults(resolvedMessageId, replay?.accountedResultIds);
1221
1503
  return resolvedMessageId;
1222
1504
  }
1223
- preserveToolResults(previous, next) {
1505
+ preserveToolResults(previous, next, decisionSource) {
1224
1506
  const preserved = /* @__PURE__ */ new Map();
1225
1507
  for (const part of iterateToolCallParts(previous)) if (isResolvedToolCall(part)) preserved.set(part.toolCallId, part);
1226
- if (preserved.size === 0) return this.preserveToolInteractions(previous, next);
1508
+ const decisions = new Map(Array.from(iterateToolCallParts(decisionSource ?? [])).filter((part) => part.approval?.approved !== void 0).map((part) => [part.toolCallId, part]));
1509
+ if (preserved.size === 0 && decisions.size === 0) return this.preserveToolInteractions(previous, next);
1227
1510
  const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
1228
1511
  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;
1512
+ const result = prior ? preserveToolResult(prior, part) : part;
1513
+ if (result.approval && decisions.has(part.toolCallId)) {
1514
+ const approval = preserveToolApprovalDecision(decisions.get(part.toolCallId), result);
1515
+ if (approval && approval !== result.approval) return {
1516
+ ...result,
1517
+ approval
1518
+ };
1519
+ }
1520
+ return result;
1238
1521
  });
1239
1522
  return this.preserveToolInteractions(previous, changed ? merged : next);
1240
1523
  }
@@ -1337,7 +1620,7 @@ var AgUiThreadController = class {
1337
1620
  custom: incoming.custom ? mergeAgUiNamespace(current.custom, incoming.custom) : current.custom
1338
1621
  };
1339
1622
  }
1340
- handleEvent(aggregator, event, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1623
+ handleEvent(aggregator, event, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot) {
1341
1624
  switch (event.type) {
1342
1625
  case "STATE_SNAPSHOT":
1343
1626
  this.dispatch({
@@ -1363,7 +1646,7 @@ var AgUiThreadController = class {
1363
1646
  const previousHeadId = this.session.headId;
1364
1647
  const activeAssistant = activeAssistantId === void 0 ? void 0 : this.session.tryGetMessage(activeAssistantId)?.message;
1365
1648
  const hasActiveText = activeAssistant?.role === "assistant" && activeAssistant.content.some((part) => part.type === "text");
1366
- this.importMessagesSnapshot(event.messages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
1649
+ this.importMessagesSnapshot(event.messages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot);
1367
1650
  const headId = this.session.headId;
1368
1651
  if (hasActiveText && headId !== previousHeadId) activeAssistantId = (headId === null ? void 0 : this.session.tryGetMessage(headId)?.message)?.role === "assistant" ? headId ?? void 0 : void 0;
1369
1652
  this.updateActiveAssistantState(activeAssistantId);
@@ -1490,7 +1773,7 @@ var AgUiThreadController = class {
1490
1773
  this.publishUpdate();
1491
1774
  this.maybeCompleteAfterToolResults(messageId);
1492
1775
  }
1493
- importMessagesSnapshot(rawMessages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1776
+ importMessagesSnapshot(rawMessages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot) {
1494
1777
  try {
1495
1778
  const activeAssistantItem = activeAssistantId ? this.session.tryGetMessage(activeAssistantId) : void 0;
1496
1779
  const activeMessage = activeAssistantItem?.message;
@@ -1508,10 +1791,14 @@ var AgUiThreadController = class {
1508
1791
  for (let index = 0; index < converted.length; index++) {
1509
1792
  const convertedMessage = converted[index];
1510
1793
  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
- };
1794
+ if (convertedMessage.role === "assistant") {
1795
+ const previous = existing?.role === "assistant" ? existing : void 0;
1796
+ const snapshot = reconcileSnapshot?.(convertedMessage, previous) ?? convertedMessage;
1797
+ converted[index] = previous ? {
1798
+ ...snapshot,
1799
+ content: this.mergeSnapshotAssistantContent(previous.content, snapshot.content, snapshotHasReasoning, snapshotHasActivity)
1800
+ } : snapshot;
1801
+ }
1515
1802
  }
1516
1803
  if (joinStrategy === "concat-content") {
1517
1804
  const activeIndex = converted.findIndex((message) => message.id === activeAssistantId);
@@ -1647,7 +1934,6 @@ var AgUiThreadController = class {
1647
1934
  return write;
1648
1935
  }
1649
1936
  };
1650
- //#endregion
1651
1937
  export { AgUiThreadController };
1652
1938
 
1653
1939
  //# sourceMappingURL=AgUiThreadController.js.map