@assistant-ui/core 0.2.22 → 0.2.23

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 (214) hide show
  1. package/dist/adapters/thread-history.d.ts +8 -0
  2. package/dist/adapters/thread-history.d.ts.map +1 -1
  3. package/dist/internal.d.ts +3 -2
  4. package/dist/internal.js +3 -2
  5. package/dist/model-context/frame/host.d.ts.map +1 -1
  6. package/dist/model-context/frame/host.js +36 -21
  7. package/dist/model-context/frame/host.js.map +1 -1
  8. package/dist/model-context/types.d.ts.map +1 -1
  9. package/dist/model-context/types.js +13 -4
  10. package/dist/model-context/types.js.map +1 -1
  11. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  12. package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -0
  13. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  14. package/dist/react/client/interactable-model-context.js +1 -0
  15. package/dist/react/client/interactable-model-context.js.map +1 -1
  16. package/dist/react/interactables-legacy/useInteractableState.js +1 -0
  17. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
  18. package/dist/react/model-context/useInteractableState.js +1 -0
  19. package/dist/react/model-context/useInteractableState.js.map +1 -1
  20. package/dist/react/model-context/useInteractableVersions.js +1 -0
  21. package/dist/react/model-context/useInteractableVersions.js.map +1 -1
  22. package/dist/react/model-context/useToolArgsStatus.js +1 -0
  23. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  24. package/dist/react/primitive-hooks/useActionBarCopy.js +1 -0
  25. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  26. package/dist/react/primitive-hooks/useActionBarEdit.js +1 -0
  27. package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
  28. package/dist/react/primitive-hooks/useActionBarFeedback.js +1 -0
  29. package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
  30. package/dist/react/primitive-hooks/useActionBarReload.js +1 -0
  31. package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
  32. package/dist/react/primitive-hooks/useActionBarSpeak.js +1 -0
  33. package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
  34. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +1 -0
  35. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
  36. package/dist/react/primitive-hooks/useBranchPickerNext.js +1 -0
  37. package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
  38. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +1 -0
  39. package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
  40. package/dist/react/primitive-hooks/useComposerAddAttachment.js +1 -0
  41. package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
  42. package/dist/react/primitive-hooks/useComposerCancel.js +1 -0
  43. package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
  44. package/dist/react/primitive-hooks/useComposerDictate.js +1 -0
  45. package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
  46. package/dist/react/primitive-hooks/useComposerSend.js +1 -0
  47. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  48. package/dist/react/primitive-hooks/useEditComposerCancel.js +1 -0
  49. package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
  50. package/dist/react/primitive-hooks/useEditComposerSend.js +1 -0
  51. package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
  52. package/dist/react/primitive-hooks/useMessageBranching.js +1 -0
  53. package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
  54. package/dist/react/primitive-hooks/useMessageReload.js +1 -0
  55. package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
  56. package/dist/react/primitive-hooks/useSuggestionTrigger.js +1 -0
  57. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  58. package/dist/react/primitive-hooks/useThreadListItemArchive.js +1 -0
  59. package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
  60. package/dist/react/primitive-hooks/useThreadListItemDelete.js +1 -0
  61. package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
  62. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +1 -0
  63. package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
  64. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +1 -0
  65. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
  66. package/dist/react/primitive-hooks/useThreadListLoadMore.js +1 -0
  67. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  68. package/dist/react/primitive-hooks/useThreadListNew.js +1 -0
  69. package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
  70. package/dist/react/providers/TextMessagePartProvider.js +1 -0
  71. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  72. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  73. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +6 -4
  74. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  75. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
  76. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  77. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +11 -1
  78. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  79. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
  80. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  81. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +1 -1
  82. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  83. package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
  84. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  85. package/dist/react/runtimes/cloud/auiV0.js +2 -1
  86. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  87. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
  88. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  89. package/dist/react/runtimes/external-message-converter.js +1 -1
  90. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  91. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
  92. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
  93. package/dist/react/runtimes/useLocalRuntime.d.ts +0 -1
  94. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  95. package/dist/runtime/api/assistant-runtime.d.ts +1 -1
  96. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  97. package/dist/runtime/api/assistant-runtime.js +1 -1
  98. package/dist/runtime/api/assistant-runtime.js.map +1 -1
  99. package/dist/runtime/api/attachment-runtime.d.ts +1 -1
  100. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  101. package/dist/runtime/api/attachment-runtime.js +1 -1
  102. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  103. package/dist/runtime/api/composer-runtime.d.ts +2 -2
  104. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  105. package/dist/runtime/api/composer-runtime.js +2 -2
  106. package/dist/runtime/api/composer-runtime.js.map +1 -1
  107. package/dist/runtime/api/message-part-runtime.d.ts +4 -4
  108. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  109. package/dist/runtime/api/message-part-runtime.js +3 -3
  110. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  111. package/dist/runtime/api/message-runtime.d.ts +1 -1
  112. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  113. package/dist/runtime/api/message-runtime.js +2 -2
  114. package/dist/runtime/api/message-runtime.js.map +1 -1
  115. package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
  116. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  117. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  118. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  119. package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
  120. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  121. package/dist/runtime/api/thread-list-runtime.js +1 -1
  122. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  123. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
  124. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  125. package/dist/runtime/base/base-thread-runtime-core.js +1 -1
  126. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  127. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
  128. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  129. package/dist/runtime/base/default-edit-composer-runtime-core.js +2 -2
  130. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  131. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  132. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  133. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  134. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  135. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  136. package/dist/runtime/utils/message-repository.js +4 -2
  137. package/dist/runtime/utils/message-repository.js.map +1 -1
  138. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
  139. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  140. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
  141. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  142. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  143. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -0
  144. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  145. package/dist/runtimes/internal.d.ts +2 -1
  146. package/dist/runtimes/internal.js +2 -1
  147. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
  148. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  149. package/dist/runtimes/local/local-thread-runtime-core.js +44 -13
  150. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  151. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
  152. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  153. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  154. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  155. package/dist/store/clients/no-op-composer-client.js +1 -0
  156. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  157. package/dist/store/runtime-clients/thread-list-runtime-client.js +1 -0
  158. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  159. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  160. package/dist/store/runtime-clients/thread-runtime-client.js +17 -15
  161. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  162. package/dist/store/scopes/thread.d.ts +1 -0
  163. package/dist/store/scopes/thread.d.ts.map +1 -1
  164. package/dist/subscribable/subscribable.d.ts +3 -3
  165. package/dist/subscribable/subscribable.d.ts.map +1 -1
  166. package/dist/subscribable/subscribable.js +3 -3
  167. package/dist/subscribable/subscribable.js.map +1 -1
  168. package/package.json +7 -7
  169. package/src/adapters/thread-history.ts +8 -0
  170. package/src/internal.ts +1 -1
  171. package/src/model-context/frame/host.test.ts +47 -0
  172. package/src/model-context/frame/host.ts +46 -19
  173. package/src/model-context/types.ts +16 -6
  174. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +15 -5
  175. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +20 -6
  176. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
  177. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +26 -5
  178. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +5 -1
  179. package/src/react/runtimes/cloud/auiV0.ts +14 -0
  180. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
  181. package/src/react/runtimes/external-message-converter.test.ts +346 -0
  182. package/src/react/runtimes/external-message-converter.ts +1 -1
  183. package/src/react/runtimes/useLocalRuntime.test.tsx +43 -0
  184. package/src/runtime/api/assistant-runtime.ts +4 -1
  185. package/src/runtime/api/attachment-runtime.ts +8 -2
  186. package/src/runtime/api/composer-runtime.ts +9 -5
  187. package/src/runtime/api/message-part-runtime.ts +10 -3
  188. package/src/runtime/api/message-runtime.ts +7 -2
  189. package/src/runtime/api/thread-list-item-runtime.ts +7 -2
  190. package/src/runtime/api/thread-list-runtime.ts +10 -2
  191. package/src/runtime/base/base-thread-runtime-core.ts +5 -1
  192. package/src/runtime/base/default-edit-composer-runtime-core.ts +14 -2
  193. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  194. package/src/runtime/utils/message-repository.ts +19 -13
  195. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
  196. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
  197. package/src/runtimes/internal.ts +3 -0
  198. package/src/runtimes/local/local-thread-runtime-core.test.ts +596 -0
  199. package/src/runtimes/local/local-thread-runtime-core.ts +85 -21
  200. package/src/runtimes/local/should-continue.test.ts +122 -0
  201. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +9 -10
  202. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +38 -32
  203. package/src/store/clients/model-context-client.test.ts +21 -1
  204. package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
  205. package/src/store/scopes/thread.ts +1 -0
  206. package/src/subscribable/subscribable.ts +16 -8
  207. package/src/tests/MessageRepository.test.ts +12 -0
  208. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
  209. package/src/tests/auiV0Encode.test.ts +74 -0
  210. package/src/tests/base-composer-runtime-core.test.ts +422 -0
  211. package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
  212. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
  213. package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
  214. package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
@@ -68,6 +68,45 @@ export class LocalThreadRuntimeCore
68
68
  private _queue: MessageQueueController | null = null;
69
69
  private _queueRunInFlight = false;
70
70
 
71
+ private _historyWrites = new Map<string, Promise<void>>();
72
+
73
+ // Writes for one message id must land in issue order; an earlier paused
74
+ // snapshot arriving after the terminal write would resurrect the pause.
75
+ private _chainHistoryWrite(
76
+ id: string,
77
+ write: () => Promise<void>,
78
+ ): Promise<void> {
79
+ const next = (this._historyWrites.get(id) ?? Promise.resolve()).then(
80
+ write,
81
+ write,
82
+ );
83
+ const stored = next.then(
84
+ () => {},
85
+ () => {},
86
+ );
87
+ this._historyWrites.set(id, stored);
88
+ void stored.then(() => {
89
+ if (this._historyWrites.get(id) === stored) {
90
+ this._historyWrites.delete(id);
91
+ }
92
+ });
93
+ return next;
94
+ }
95
+
96
+ // A decision recorded on a still-paused message must reach history before
97
+ // the run resumes, or a refresh would restore the message without it.
98
+ private _persistPausedMessage(
99
+ parentId: string | null,
100
+ message: ThreadAssistantMessage,
101
+ ) {
102
+ if (message.status?.type !== "requires-action") return;
103
+ const history = this._options.adapters.history;
104
+ if (!history?.update) return;
105
+ const update = history.update.bind(history);
106
+ const item = { parentId, message, runConfig: this._lastRunConfig };
107
+ this._chainHistoryWrite(message.id, () => update(item)).catch(() => {});
108
+ }
109
+
71
110
  public readonly isDisabled = false;
72
111
  public readonly isSendDisabled = false;
73
112
 
@@ -351,6 +390,12 @@ export class LocalThreadRuntimeCore
351
390
  ): Promise<void> {
352
391
  this._notifyEventSubscribers("runStart", {});
353
392
 
393
+ // A run entered on a requires-action message resumes a pause an
394
+ // update-capable adapter already holds (written at pause time or loaded).
395
+ const alreadyPersisted =
396
+ message.status?.type === "requires-action" &&
397
+ this._options.adapters.history?.update !== undefined;
398
+
354
399
  try {
355
400
  // mark busy for runs not started through the queue (regenerate, resume)
356
401
  this._queue?.notifyBusy();
@@ -364,6 +409,7 @@ export class LocalThreadRuntimeCore
364
409
  parentId,
365
410
  message,
366
411
  runConfig,
412
+ alreadyPersisted,
367
413
  runCallback,
368
414
  );
369
415
  runCallback = undefined;
@@ -406,6 +452,7 @@ export class LocalThreadRuntimeCore
406
452
  parentId: string | null,
407
453
  message: ThreadAssistantMessage,
408
454
  runConfig: RunConfig | undefined,
455
+ alreadyPersisted: boolean,
409
456
  runCallback?: ChatModelAdapter["run"],
410
457
  ) {
411
458
  const messages = parentId ? this.repository.getMessages(parentId) : [];
@@ -471,17 +518,18 @@ export class LocalThreadRuntimeCore
471
518
 
472
519
  const maxSteps = this._options.maxSteps ?? 2;
473
520
 
474
- const steps = message.metadata?.steps?.length ?? 0;
475
- if (steps >= maxSteps) {
476
- // reached max tool steps
477
- updateMessage({
478
- status: {
479
- type: "incomplete",
480
- reason: "tool-calls",
481
- },
482
- });
483
- return message;
484
- } else {
521
+ try {
522
+ const steps = message.metadata?.steps?.length ?? 0;
523
+ if (steps >= maxSteps) {
524
+ updateMessage({
525
+ status: {
526
+ type: "incomplete",
527
+ reason: "tool-calls",
528
+ },
529
+ });
530
+ return message;
531
+ }
532
+
485
533
  updateMessage({
486
534
  status: {
487
535
  type: "running",
@@ -491,9 +539,7 @@ export class LocalThreadRuntimeCore
491
539
  // Switch to the new message branch right after adding it for the first time
492
540
  this.repository.resetHead(message.id);
493
541
  this._notifySubscribers();
494
- }
495
542
 
496
- try {
497
543
  this._lastRunConfig = runConfig ?? {};
498
544
  // unstable_composerMetadata is composer-only (stamped onto the outgoing
499
545
  // message); never expose it to the chat-model adapter's run context.
@@ -565,15 +611,29 @@ export class LocalThreadRuntimeCore
565
611
  } finally {
566
612
  this.abortController = null;
567
613
 
568
- if (
614
+ const history = this._options.adapters.history;
615
+ const item = {
616
+ parentId,
617
+ message,
618
+ runConfig: this._lastRunConfig,
619
+ };
620
+ const isTerminal =
569
621
  message.status.type === "complete" ||
570
- message.status.type === "incomplete"
571
- ) {
572
- await this._options.adapters.history?.append({
573
- parentId,
574
- message: message,
575
- runConfig: this._lastRunConfig,
576
- });
622
+ message.status.type === "incomplete";
623
+ const isPausing =
624
+ message.status.type === "requires-action" &&
625
+ !shouldContinue(message, this._options.unstable_humanToolNames);
626
+
627
+ // Pauses are written only for adapters that can rewrite the entry later;
628
+ // an append-only adapter would strand a half-finished run in history.
629
+ if (isTerminal || (isPausing && history?.update)) {
630
+ const write =
631
+ alreadyPersisted && history?.update
632
+ ? history.update.bind(history)
633
+ : history?.append.bind(history);
634
+ if (write) {
635
+ await this._chainHistoryWrite(message.id, () => write(item));
636
+ }
577
637
  }
578
638
  }
579
639
  return message;
@@ -643,6 +703,8 @@ export class LocalThreadRuntimeCore
643
703
  shouldContinue(message, this._options.unstable_humanToolNames)
644
704
  ) {
645
705
  this._runLoop(parentId, message, this._lastRunConfig).catch(() => {});
706
+ } else if (added) {
707
+ this._persistPausedMessage(parentId, message);
646
708
  }
647
709
  }
648
710
 
@@ -721,6 +783,8 @@ export class LocalThreadRuntimeCore
721
783
  shouldContinue(message, this._options.unstable_humanToolNames)
722
784
  ) {
723
785
  this._runLoop(parentId, message, this._lastRunConfig).catch(() => {});
786
+ } else {
787
+ this._persistPausedMessage(parentId, message);
724
788
  }
725
789
  }
726
790
  }
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ThreadAssistantMessage } from "../../types/message";
3
+ import { shouldContinue } from "./should-continue";
4
+
5
+ const makeMessage = (
6
+ overrides: Partial<ThreadAssistantMessage>,
7
+ ): ThreadAssistantMessage => ({
8
+ id: "msg-1",
9
+ role: "assistant",
10
+ createdAt: new Date(),
11
+ content: [],
12
+ status: { type: "complete", reason: "stop" },
13
+ metadata: {
14
+ unstable_state: null,
15
+ unstable_annotations: [],
16
+ unstable_data: [],
17
+ steps: [],
18
+ custom: {},
19
+ },
20
+ ...overrides,
21
+ });
22
+
23
+ const toolCall = (toolName: string, result?: unknown) => ({
24
+ type: "tool-call" as const,
25
+ toolCallId: `call-${toolName}`,
26
+ toolName,
27
+ args: {},
28
+ argsText: "{}",
29
+ ...(result !== undefined ? { result } : {}),
30
+ });
31
+
32
+ describe("shouldContinue", () => {
33
+ it("returns false when status is not requires-action", () => {
34
+ const msg = makeMessage({
35
+ status: { type: "complete", reason: "stop" },
36
+ });
37
+ expect(shouldContinue(msg, undefined)).toBe(false);
38
+ });
39
+
40
+ it("returns false when reason is not tool-calls", () => {
41
+ const msg = makeMessage({
42
+ status: { type: "requires-action", reason: "interrupt" },
43
+ });
44
+ expect(shouldContinue(msg, undefined)).toBe(false);
45
+ });
46
+
47
+ it("returns true when all tool calls have results (no humanToolNames)", () => {
48
+ const msg = makeMessage({
49
+ status: { type: "requires-action", reason: "tool-calls" },
50
+ content: [toolCall("search", "found it"), toolCall("calculate", 42)],
51
+ });
52
+ expect(shouldContinue(msg, undefined)).toBe(true);
53
+ });
54
+
55
+ it("returns false when some tool calls have no result (no humanToolNames)", () => {
56
+ const msg = makeMessage({
57
+ status: { type: "requires-action", reason: "tool-calls" },
58
+ content: [toolCall("search", "found it"), toolCall("calculate")],
59
+ });
60
+ expect(shouldContinue(msg, undefined)).toBe(false);
61
+ });
62
+
63
+ it("returns true when unresolved tool call is not a human tool", () => {
64
+ const msg = makeMessage({
65
+ status: { type: "requires-action", reason: "tool-calls" },
66
+ content: [toolCall("auto-tool")],
67
+ });
68
+ expect(shouldContinue(msg, ["human-approval"])).toBe(true);
69
+ });
70
+
71
+ it("returns false when unresolved tool call IS a human tool", () => {
72
+ const msg = makeMessage({
73
+ status: { type: "requires-action", reason: "tool-calls" },
74
+ content: [toolCall("human-approval")],
75
+ });
76
+ expect(shouldContinue(msg, ["human-approval"])).toBe(false);
77
+ });
78
+
79
+ it("returns true for non-tool-call content parts", () => {
80
+ const msg = makeMessage({
81
+ status: { type: "requires-action", reason: "tool-calls" },
82
+ content: [
83
+ { type: "text", text: "Hello" } as any,
84
+ toolCall("search", "done"),
85
+ ],
86
+ });
87
+ expect(shouldContinue(msg, undefined)).toBe(true);
88
+ });
89
+
90
+ it("returns false while a tool call has a pending approval", () => {
91
+ const msg = makeMessage({
92
+ status: { type: "requires-action", reason: "tool-calls" },
93
+ content: [{ ...toolCall("deploy"), approval: { id: "a1" } }],
94
+ });
95
+ expect(shouldContinue(msg, undefined)).toBe(false);
96
+ expect(shouldContinue(msg, ["human-approval"])).toBe(false);
97
+ });
98
+
99
+ it("returns true when a decided approval has no result", () => {
100
+ const msg = makeMessage({
101
+ status: { type: "requires-action", reason: "tool-calls" },
102
+ content: [
103
+ { ...toolCall("deploy"), approval: { id: "a1", approved: true } },
104
+ ],
105
+ });
106
+ expect(shouldContinue(msg, undefined)).toBe(true);
107
+ expect(shouldContinue(msg, ["human-approval"])).toBe(true);
108
+ });
109
+
110
+ it("exempts approval-gated tool calls from the human tool result requirement", () => {
111
+ const msg = makeMessage({
112
+ status: { type: "requires-action", reason: "tool-calls" },
113
+ content: [
114
+ {
115
+ ...toolCall("human-approval"),
116
+ approval: { id: "a1", approved: true },
117
+ },
118
+ ],
119
+ });
120
+ expect(shouldContinue(msg, ["human-approval"])).toBe(true);
121
+ });
122
+ });
@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from "vitest";
3
3
  import {
4
4
  ToolInvocationTracker,
5
5
  type ToolExecutionStatus,
6
- type ToolInvocationTrackerSnapshot,
7
6
  } from "./ToolInvocationTracker";
8
7
  import type {
9
8
  ThreadAssistantMessage,
@@ -34,7 +33,7 @@ async function waitFor(
34
33
  const createState = (
35
34
  messages: ThreadAssistantMessage[],
36
35
  isRunning: boolean = true,
37
- ): ToolInvocationTrackerSnapshot => ({
36
+ ): ToolInvocationTracker.Snapshot => ({
38
37
  messages: messages as readonly ThreadMessage[],
39
38
  isRunning,
40
39
  });
@@ -152,13 +151,13 @@ describe("ToolInvocationTracker", () => {
152
151
  }
153
152
  });
154
153
 
155
- it("does not auto-submit a parse-error result when divergent argsText closes without a backend result (#5130)", async () => {
156
- // Mirrors the #5098 production shape: a human-in-the-loop tool whose
157
- // argsText diverges mid-stream and never re-converges, with no backend
158
- // result at close time. The args stream must not close on the divergent
159
- // snapshot (which holds a stale prefix the execution path would parse),
160
- // so no bogus parse-error result is auto-submitted to resume the host
161
- // graph and abandon the pending interrupt.
154
+ it("does not auto-submit a parse-error result when divergent argsText closes without a backend result", async () => {
155
+ // A human-in-the-loop tool whose argsText diverges mid-stream and never
156
+ // re-converges, with no backend result at close time. The args stream must
157
+ // not close on the divergent snapshot (which holds a stale prefix the
158
+ // execution path would parse), so no bogus parse-error result is
159
+ // auto-submitted to resume the host graph and abandon the pending
160
+ // interrupt.
162
161
  const execute = vi.fn(async () => ({ forecast: "ok" }));
163
162
  const streamCall = vi.fn((_reader, { human }) => {
164
163
  // Request human input immediately — sets up the pending interrupt.
@@ -236,7 +235,7 @@ describe("ToolInvocationTracker", () => {
236
235
  }
237
236
  });
238
237
 
239
- it("does not auto-submit a parse-error result for a non-executable tool whose divergent argsText closes (#5130)", async () => {
238
+ it("does not auto-submit a parse-error result for a non-executable tool whose divergent argsText closes", async () => {
240
239
  // Same close-gating mismatch as the executable case, but for a tool with
241
240
  // no frontend execute. Closing on the divergent complete snapshot would
242
241
  // still parse the incomplete stale prefix and fabricate a parse-error
@@ -35,34 +35,6 @@ export type AddToolResultCommand = {
35
35
  readonly modelContent?: readonly ToolModelContentPart[];
36
36
  };
37
37
 
38
- export type ToolInvocationTrackerSnapshot = {
39
- readonly messages: readonly ThreadMessage[];
40
- /** Whether the producing runtime is currently streaming new output. */
41
- readonly isRunning: boolean;
42
- /**
43
- * Whether the producing runtime is still loading historical state.
44
- * When `true`, every snapshot is treated as historical (no `streamCall` /
45
- * `execute` fires). When `false`, processing resumes as live.
46
- */
47
- readonly isLoading?: boolean;
48
- };
49
-
50
- export type ToolInvocationTrackerCallbacks = {
51
- /**
52
- * Invoked when a client-side `execute()` returns a result and the runtime
53
- * needs to feed it back into the conversation.
54
- */
55
- onResult: (command: AddToolResultCommand) => void;
56
- /**
57
- * Invoked whenever the per-tool-call status map changes (executing /
58
- * interrupt / cleared). The callback receives a fresh map; mutating the
59
- * argument is not supported.
60
- */
61
- onStatusesChange: (
62
- statuses: ReadonlyMap<string, ToolExecutionStatus>,
63
- ) => void;
64
- };
65
-
66
38
  type ToolCallEntry = {
67
39
  toolName: string;
68
40
  argsText: string;
@@ -123,10 +95,12 @@ const isEquivalentCompleteArgsText = (previous: string, next: string) => {
123
95
  * the hot message-processing path, so a malformed snapshot must never crash
124
96
  * the host runtime. See ./EDGE_CASES.md for the known non-trivial state
125
97
  * transitions and what each does today.
98
+ *
99
+ * @deprecated Internal — for framework bindings; not a public API. May change without notice.
126
100
  */
127
101
  export class ToolInvocationTracker {
128
102
  private readonly _getTools: () => Record<string, Tool> | undefined;
129
- private readonly _callbacks: ToolInvocationTrackerCallbacks;
103
+ private readonly _callbacks: ToolInvocationTracker.Callbacks;
130
104
 
131
105
  private readonly _entries = new Map<string, ToolCallEntry>();
132
106
  /**
@@ -157,7 +131,7 @@ export class ToolInvocationTracker {
157
131
  private _pendingRestore = true;
158
132
 
159
133
  /** Cached last snapshot, used to skip processing on identical re-renders. */
160
- private _lastSnapshot: ToolInvocationTrackerSnapshot | null = null;
134
+ private _lastSnapshot: ToolInvocationTracker.Snapshot | null = null;
161
135
  private _isRunning = false;
162
136
 
163
137
  private _controller!: ReturnType<typeof createAssistantStreamController>[1];
@@ -175,7 +149,7 @@ export class ToolInvocationTracker {
175
149
 
176
150
  constructor(
177
151
  getTools: () => Record<string, Tool> | undefined,
178
- callbacks: ToolInvocationTrackerCallbacks,
152
+ callbacks: ToolInvocationTracker.Callbacks,
179
153
  ) {
180
154
  this._getTools = getTools;
181
155
  this._callbacks = callbacks;
@@ -235,7 +209,7 @@ export class ToolInvocationTracker {
235
209
  * Feed the next observed snapshot into the tracker. Called from the host
236
210
  * runtime whenever its message list / running state changes.
237
211
  */
238
- public setState(snapshot: ToolInvocationTrackerSnapshot): void {
212
+ public setState(snapshot: ToolInvocationTracker.Snapshot): void {
239
213
  try {
240
214
  // Recover from a dead pipeline before processing anything. We demote
241
215
  // all active entries to "restored" so the rebuilt pipeline does not
@@ -782,3 +756,35 @@ export class ToolInvocationTracker {
782
756
  }
783
757
  }
784
758
  }
759
+
760
+ export namespace ToolInvocationTracker {
761
+ export type ExecutionStatus = ToolExecutionStatus;
762
+
763
+ export type Snapshot = {
764
+ readonly messages: readonly ThreadMessage[];
765
+ /** Whether the producing runtime is currently streaming new output. */
766
+ readonly isRunning: boolean;
767
+ /**
768
+ * Whether the producing runtime is still loading historical state.
769
+ * When `true`, every snapshot is treated as historical (no `streamCall` /
770
+ * `execute` fires). When `false`, processing resumes as live.
771
+ */
772
+ readonly isLoading?: boolean;
773
+ };
774
+
775
+ export type Callbacks = {
776
+ /**
777
+ * Invoked when a client-side `execute()` returns a result and the runtime
778
+ * needs to feed it back into the conversation.
779
+ */
780
+ onResult: (command: AddToolResultCommand) => void;
781
+ /**
782
+ * Invoked whenever the per-tool-call status map changes (executing /
783
+ * interrupt / cleared). The callback receives a fresh map; mutating the
784
+ * argument is not supported.
785
+ */
786
+ onStatusesChange: (
787
+ statuses: ReadonlyMap<string, ToolExecutionStatus>,
788
+ ) => void;
789
+ };
790
+ }
@@ -149,7 +149,7 @@ describe("mergeModelContexts", () => {
149
149
  });
150
150
  });
151
151
 
152
- it("still rejects duplicate tools at the same priority", () => {
152
+ it("rejects duplicate tools at the same priority", () => {
153
153
  expect(() =>
154
154
  mergeModelContexts(
155
155
  new Set([
@@ -160,6 +160,26 @@ describe("mergeModelContexts", () => {
160
160
  ).toThrow(/already exists/);
161
161
  });
162
162
 
163
+ it("silently replaces a same-priority tool when the later registration sets overwrite", () => {
164
+ const merged = mergeModelContexts(
165
+ new Set([
166
+ provider({ tools: { duplicate: toolFixture() } }),
167
+ provider({
168
+ tools: {
169
+ duplicate: {
170
+ ...toolFixture(),
171
+ description: "latest",
172
+ overwrite: true,
173
+ } as Tool<any, any>,
174
+ },
175
+ }),
176
+ ]),
177
+ );
178
+
179
+ expect(merged.tools?.duplicate?.description).toBe("latest");
180
+ expect(merged.tools?.duplicate?.overwrite).toBeUndefined();
181
+ });
182
+
163
183
  it("preserves the highest priority when a lower-priority provider reuses the same tool object", () => {
164
184
  const shared = {
165
185
  ...toolFixture(),
@@ -1,7 +1,7 @@
1
1
  import type { Unsubscribe } from "../../types/unsubscribe";
2
2
  import type { ThreadRuntimeEventType } from "../../runtime/interfaces/thread-runtime-core";
3
3
  import type { ThreadRuntime } from "../../runtime/api/thread-runtime";
4
- import { useMemo, useEffect, RefObject } from "react";
4
+ import { useMemo, useEffect, type RefObject } from "react";
5
5
  import { useResource, resource, withKey } from "@assistant-ui/tap";
6
6
  import { liveRef } from "./liveRef";
7
7
  import {
@@ -113,6 +113,7 @@ const useThreadClient = ({
113
113
  deleteMessage: runtime.deleteMessage,
114
114
  startRun: runtime.startRun,
115
115
  resumeRun: runtime.resumeRun,
116
+ importExternalState: runtime.importExternalState,
116
117
  cancelRun: runtime.cancelRun,
117
118
  getModelContext: runtime.getModelContext,
118
119
  export: runtime.export,
@@ -108,6 +108,7 @@ export type ThreadMethods = {
108
108
  * @param initialMessages - Optional array of initial messages to populate the thread
109
109
  */
110
110
  reset(initialMessages?: readonly ThreadMessageLike[]): void;
111
+ importExternalState?(state: unknown): void;
111
112
  message(selector: { id: string } | { index: number }): MessageMethods;
112
113
  /** @deprecated This API is still under active development and might change without notice. */
113
114
  stopSpeaking(): void;
@@ -141,10 +141,11 @@ export class ShallowMemoizeSubject<TState extends object, TPath>
141
141
  return this.binding.path;
142
142
  }
143
143
 
144
- constructor(
145
- private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>,
146
- ) {
144
+ private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>;
145
+
146
+ constructor(binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>) {
147
147
  super();
148
+ this.binding = binding;
148
149
  const state = binding.getState();
149
150
  if (state === SKIP_UPDATE)
150
151
  throw new Error("Entry not available in the store");
@@ -184,10 +185,11 @@ export class LazyMemoizeSubject<TState extends object, TPath>
184
185
  return this.binding.path;
185
186
  }
186
187
 
187
- constructor(
188
- private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>,
189
- ) {
188
+ private binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>;
189
+
190
+ constructor(binding: SubscribableWithState<TState | SKIP_UPDATE, TPath>) {
190
191
  super();
192
+ this.binding = binding;
191
193
  }
192
194
 
193
195
  private _previousStateDirty = true;
@@ -228,8 +230,11 @@ export class NestedSubscriptionSubject<
228
230
  return this.binding.path;
229
231
  }
230
232
 
231
- constructor(private binding: NestedSubscribable<TState, TPath>) {
233
+ private binding: NestedSubscribable<TState, TPath>;
234
+
235
+ constructor(binding: NestedSubscribable<TState, TPath>) {
232
236
  super();
237
+ this.binding = binding;
233
238
  }
234
239
 
235
240
  public getState() {
@@ -269,8 +274,11 @@ export class NestedSubscriptionSubject<
269
274
  export class EventSubscriptionSubject<
270
275
  TEvent extends string,
271
276
  > extends BaseSubject {
272
- constructor(private config: EventSubscribable<TEvent>) {
277
+ private config: EventSubscribable<TEvent>;
278
+
279
+ constructor(config: EventSubscribable<TEvent>) {
273
280
  super();
281
+ this.config = config;
274
282
  }
275
283
 
276
284
  public getState() {
@@ -110,6 +110,18 @@ describe("MessageRepository", () => {
110
110
  }).toThrow(/Parent message not found/);
111
111
  });
112
112
 
113
+ it("should throw when a message would be linked under an ancestor with the same id", () => {
114
+ const message = createTestMessage({ id: "message-id" });
115
+ repository.addOrUpdateMessage(null, message);
116
+
117
+ expect(() => {
118
+ repository.addOrUpdateMessage(
119
+ "message-id",
120
+ createTestMessage({ id: "message-id" }),
121
+ );
122
+ }).toThrow(/same id already exists in the parent tree/);
123
+ });
124
+
113
125
  it("should retrieve all messages in the current branch", () => {
114
126
  const parent = createTestMessage({ id: "parent-id" });
115
127
  const child = createTestMessage({ id: "child-id" });