@assistant-ui/core 0.2.21 → 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 (345) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/speech.d.ts.map +1 -1
  3. package/dist/adapters/speech.js +9 -2
  4. package/dist/adapters/speech.js.map +1 -1
  5. package/dist/adapters/thread-history.d.ts +8 -0
  6. package/dist/adapters/thread-history.d.ts.map +1 -1
  7. package/dist/adapters/voice.d.ts.map +1 -1
  8. package/dist/adapters/voice.js +13 -5
  9. package/dist/adapters/voice.js.map +1 -1
  10. package/dist/internal.d.ts +5 -2
  11. package/dist/internal.js +5 -2
  12. package/dist/model-context/frame/host.d.ts +1 -0
  13. package/dist/model-context/frame/host.d.ts.map +1 -1
  14. package/dist/model-context/frame/host.js +41 -21
  15. package/dist/model-context/frame/host.js.map +1 -1
  16. package/dist/model-context/frame/provider.d.ts.map +1 -1
  17. package/dist/model-context/frame/provider.js +1 -0
  18. package/dist/model-context/frame/provider.js.map +1 -1
  19. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  20. package/dist/model-context/types.d.ts.map +1 -1
  21. package/dist/model-context/types.js +13 -4
  22. package/dist/model-context/types.js.map +1 -1
  23. package/dist/react/AssistantProvider.js.map +1 -1
  24. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  25. package/dist/react/adapters/LocalStorageThreadListAdapter.js +78 -50
  26. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  27. package/dist/react/client/Interactables.js.map +1 -1
  28. package/dist/react/client/Tools.d.ts.map +1 -1
  29. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  30. package/dist/react/client/interactable-model-context.js +1 -0
  31. package/dist/react/client/interactable-model-context.js.map +1 -1
  32. package/dist/react/interactables-legacy/useInteractableState.js +1 -0
  33. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
  34. package/dist/react/model-context/useInteractable.js.map +1 -1
  35. package/dist/react/model-context/useInteractableState.js +1 -0
  36. package/dist/react/model-context/useInteractableState.js.map +1 -1
  37. package/dist/react/model-context/useInteractableVersions.js +1 -0
  38. package/dist/react/model-context/useInteractableVersions.js.map +1 -1
  39. package/dist/react/model-context/useToolArgsStatus.js +1 -0
  40. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  41. package/dist/react/primitive-hooks/useActionBarCopy.js +14 -13
  42. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  43. package/dist/react/primitive-hooks/useActionBarEdit.js +1 -0
  44. package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
  45. package/dist/react/primitive-hooks/useActionBarFeedback.js +1 -0
  46. package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
  47. package/dist/react/primitive-hooks/useActionBarReload.js +1 -0
  48. package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
  49. package/dist/react/primitive-hooks/useActionBarSpeak.js +1 -0
  50. package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
  51. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +1 -0
  52. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
  53. package/dist/react/primitive-hooks/useBranchPickerNext.js +1 -0
  54. package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
  55. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +1 -0
  56. package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
  57. package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -1
  58. package/dist/react/primitive-hooks/useComposerAddAttachment.js +1 -0
  59. package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
  60. package/dist/react/primitive-hooks/useComposerCancel.js +1 -0
  61. package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
  62. package/dist/react/primitive-hooks/useComposerDictate.js +1 -0
  63. package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
  64. package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
  65. package/dist/react/primitive-hooks/useComposerSend.js +1 -0
  66. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  67. package/dist/react/primitive-hooks/useEditComposerCancel.js +1 -0
  68. package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
  69. package/dist/react/primitive-hooks/useEditComposerSend.js +1 -0
  70. package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
  71. package/dist/react/primitive-hooks/useMessageBranching.js +1 -0
  72. package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
  73. package/dist/react/primitive-hooks/useMessageReload.js +1 -0
  74. package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
  75. package/dist/react/primitive-hooks/useSuggestionTrigger.js +1 -0
  76. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  77. package/dist/react/primitive-hooks/useThreadListItemArchive.js +1 -0
  78. package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
  79. package/dist/react/primitive-hooks/useThreadListItemDelete.js +1 -0
  80. package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
  81. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +1 -0
  82. package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
  83. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +1 -0
  84. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
  85. package/dist/react/primitive-hooks/useThreadListLoadMore.js +1 -0
  86. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  87. package/dist/react/primitive-hooks/useThreadListNew.js +1 -0
  88. package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
  89. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts.map +1 -1
  90. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  91. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
  92. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  93. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  94. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  95. package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
  96. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  97. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  98. package/dist/react/primitives/message/MessageParts.d.ts +2 -1
  99. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  100. package/dist/react/primitives/message/MessageParts.js +46 -36
  101. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  102. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  103. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  104. package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
  105. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  106. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
  107. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +18 -30
  108. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
  109. package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
  110. package/dist/react/providers/PartByIndexProvider.js +18 -30
  111. package/dist/react/providers/PartByIndexProvider.js.map +1 -1
  112. package/dist/react/providers/TextMessagePartProvider.js +1 -0
  113. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  114. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  115. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -0
  116. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  117. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +9 -2
  118. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  119. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +55 -12
  120. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  121. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
  122. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  123. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +12 -2
  124. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  125. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
  126. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  127. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +3 -2
  128. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  129. package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
  130. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  131. package/dist/react/runtimes/cloud/auiV0.js +2 -1
  132. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  133. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
  134. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  135. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  136. package/dist/react/runtimes/external-message-converter.js +2 -2
  137. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  138. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
  139. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
  140. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  141. package/dist/react/runtimes/useLocalRuntime.js +9 -1
  142. package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
  143. package/dist/runtime/api/assistant-runtime.d.ts +1 -1
  144. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  145. package/dist/runtime/api/assistant-runtime.js +1 -1
  146. package/dist/runtime/api/assistant-runtime.js.map +1 -1
  147. package/dist/runtime/api/attachment-runtime.d.ts +1 -1
  148. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  149. package/dist/runtime/api/attachment-runtime.js +1 -1
  150. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  151. package/dist/runtime/api/composer-runtime.d.ts +2 -2
  152. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  153. package/dist/runtime/api/composer-runtime.js +2 -2
  154. package/dist/runtime/api/composer-runtime.js.map +1 -1
  155. package/dist/runtime/api/message-part-runtime.d.ts +4 -4
  156. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  157. package/dist/runtime/api/message-part-runtime.js +3 -3
  158. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  159. package/dist/runtime/api/message-runtime.d.ts +1 -1
  160. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  161. package/dist/runtime/api/message-runtime.js +6 -6
  162. package/dist/runtime/api/message-runtime.js.map +1 -1
  163. package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
  164. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  165. package/dist/runtime/api/thread-list-item-runtime.js +4 -3
  166. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  167. package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
  168. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  169. package/dist/runtime/api/thread-list-runtime.js +1 -1
  170. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  171. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  172. package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -1
  173. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  174. package/dist/runtime/base/base-composer-runtime-core.js +41 -14
  175. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  176. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
  177. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  178. package/dist/runtime/base/base-thread-runtime-core.js +3 -2
  179. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  180. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
  181. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  182. package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -4
  183. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  184. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  185. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  186. package/dist/runtime/base/default-thread-composer-runtime-core.js +3 -3
  187. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  188. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  189. package/dist/runtime/utils/message-repository.js +4 -2
  190. package/dist/runtime/utils/message-repository.js.map +1 -1
  191. package/dist/runtime/utils/thread-message-like.js +4 -1
  192. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  193. package/dist/runtimes/external-store/external-store-runtime-core.d.ts.map +1 -1
  194. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
  195. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  196. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
  197. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  198. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  199. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -0
  200. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  201. package/dist/runtimes/internal.d.ts +2 -1
  202. package/dist/runtimes/internal.js +2 -1
  203. package/dist/runtimes/local/local-runtime-core.d.ts.map +1 -1
  204. package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
  205. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
  206. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  207. package/dist/runtimes/local/local-thread-runtime-core.js +49 -15
  208. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  209. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  210. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +4 -4
  211. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  212. package/dist/runtimes/remote-thread-list/optimistic-state.js +22 -9
  213. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  214. package/dist/runtimes/remote-thread-list/types.d.ts +1 -1
  215. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
  216. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  217. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +3 -2
  218. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  219. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  220. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  221. package/dist/store/clients/no-op-composer-client.js +1 -0
  222. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  223. package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
  224. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  225. package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
  226. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  227. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  228. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  229. package/dist/store/runtime-clients/thread-list-runtime-client.js +1 -0
  230. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  231. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  232. package/dist/store/runtime-clients/thread-runtime-client.js +17 -15
  233. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  234. package/dist/store/scopes/thread.d.ts +1 -0
  235. package/dist/store/scopes/thread.d.ts.map +1 -1
  236. package/dist/subscribable/subscribable.d.ts +4 -4
  237. package/dist/subscribable/subscribable.d.ts.map +1 -1
  238. package/dist/subscribable/subscribable.js +14 -6
  239. package/dist/subscribable/subscribable.js.map +1 -1
  240. package/dist/tests/remote-thread-list-test-helpers.d.ts +2 -1
  241. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  242. package/dist/tests/remote-thread-list-test-helpers.js +5 -2
  243. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  244. package/dist/types/attachment.d.ts +2 -1
  245. package/dist/types/attachment.d.ts.map +1 -1
  246. package/dist/types/attachment.js +6 -0
  247. package/dist/types/attachment.js.map +1 -0
  248. package/dist/types/message.d.ts +1 -0
  249. package/dist/types/message.d.ts.map +1 -1
  250. package/dist/types/message.js.map +1 -1
  251. package/dist/utils/data-url.d.ts +9 -0
  252. package/dist/utils/data-url.d.ts.map +1 -0
  253. package/dist/utils/data-url.js +14 -0
  254. package/dist/utils/data-url.js.map +1 -0
  255. package/dist/utils/notify-event-listeners.d.ts +6 -0
  256. package/dist/utils/notify-event-listeners.d.ts.map +1 -0
  257. package/dist/utils/notify-event-listeners.js +16 -0
  258. package/dist/utils/notify-event-listeners.js.map +1 -0
  259. package/package.json +10 -9
  260. package/src/adapters/speech.test.ts +88 -0
  261. package/src/adapters/speech.ts +17 -2
  262. package/src/adapters/thread-history.ts +8 -0
  263. package/src/adapters/voice.test.ts +130 -0
  264. package/src/adapters/voice.ts +26 -6
  265. package/src/internal.ts +6 -1
  266. package/src/model-context/frame/host.test.ts +147 -0
  267. package/src/model-context/frame/host.ts +56 -19
  268. package/src/model-context/frame/provider.test.ts +83 -0
  269. package/src/model-context/frame/provider.ts +1 -0
  270. package/src/model-context/types.ts +16 -6
  271. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +82 -0
  272. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +108 -58
  273. package/src/react/primitive-hooks/useActionBarCopy.test.ts +79 -0
  274. package/src/react/primitive-hooks/useActionBarCopy.ts +4 -3
  275. package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -3
  276. package/src/react/primitives/message/MessageParts.tsx +48 -35
  277. package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +19 -2
  278. package/src/react/providers/PartByIndexProvider.tsx +16 -2
  279. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +18 -0
  280. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +1 -0
  281. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +68 -13
  282. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
  283. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +69 -0
  284. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +27 -6
  285. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +16 -0
  286. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +7 -2
  287. package/src/react/runtimes/cloud/auiV0.ts +14 -0
  288. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
  289. package/src/react/runtimes/external-message-converter.test.ts +346 -0
  290. package/src/react/runtimes/external-message-converter.test.tsx +21 -0
  291. package/src/react/runtimes/external-message-converter.ts +2 -2
  292. package/src/react/runtimes/useLocalRuntime.test.tsx +99 -0
  293. package/src/react/runtimes/useLocalRuntime.ts +13 -1
  294. package/src/runtime/api/assistant-runtime.ts +4 -1
  295. package/src/runtime/api/attachment-runtime.ts +8 -2
  296. package/src/runtime/api/composer-runtime.ts +9 -5
  297. package/src/runtime/api/message-part-runtime.ts +10 -3
  298. package/src/runtime/api/message-runtime.test.ts +81 -0
  299. package/src/runtime/api/message-runtime.ts +11 -8
  300. package/src/runtime/api/thread-list-item-runtime.ts +13 -3
  301. package/src/runtime/api/thread-list-runtime.ts +10 -2
  302. package/src/runtime/base/base-composer-runtime-core.ts +77 -31
  303. package/src/runtime/base/base-thread-runtime-core.ts +7 -2
  304. package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -4
  305. package/src/runtime/base/default-thread-composer-runtime-core.ts +13 -3
  306. package/src/runtime/utils/message-repository.ts +19 -13
  307. package/src/runtime/utils/thread-message-like.ts +1 -0
  308. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
  309. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
  310. package/src/runtimes/internal.ts +3 -0
  311. package/src/runtimes/local/local-thread-runtime-core.test.ts +746 -1
  312. package/src/runtimes/local/local-thread-runtime-core.ts +89 -23
  313. package/src/runtimes/local/should-continue.test.ts +122 -0
  314. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +63 -0
  315. package/src/runtimes/remote-thread-list/optimistic-state.ts +49 -17
  316. package/src/runtimes/remote-thread-list/types.ts +1 -1
  317. package/src/runtimes/tool-invocations/EDGE_CASES.md +13 -19
  318. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +128 -8
  319. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +42 -34
  320. package/src/store/clients/model-context-client.test.ts +21 -1
  321. package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
  322. package/src/store/scopes/thread.ts +1 -0
  323. package/src/subscribable/subscribable.test.ts +58 -0
  324. package/src/subscribable/subscribable.ts +29 -11
  325. package/src/tests/MessageRepository.test.ts +12 -0
  326. package/src/tests/OptimisticState-list-race.test.ts +33 -0
  327. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +223 -0
  328. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
  329. package/src/tests/auiV0Encode.test.ts +74 -0
  330. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +44 -0
  331. package/src/tests/base-composer-runtime-core-send.test.ts +433 -5
  332. package/src/tests/base-composer-runtime-core.test.ts +422 -0
  333. package/src/tests/default-edit-composer-runtime-core.test.ts +43 -5
  334. package/src/tests/event-subscription-listener-errors.test.ts +205 -0
  335. package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
  336. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
  337. package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
  338. package/src/tests/remote-thread-list-test-helpers.ts +14 -7
  339. package/src/tests/thread-message-like.test.ts +57 -0
  340. package/src/types/attachment.ts +5 -0
  341. package/src/types/message.ts +1 -0
  342. package/src/utils/data-url.test.ts +50 -0
  343. package/src/utils/data-url.ts +9 -0
  344. package/src/utils/notify-event-listeners.ts +30 -0
  345. package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
@@ -1,4 +1,4 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { LocalRuntimeCore } from "./local-runtime-core";
3
3
  import type {
4
4
  ChatModelAdapter,
@@ -7,13 +7,20 @@ import type {
7
7
  } from "../../runtime/utils/chat-model-adapter";
8
8
  import type { AppendMessage } from "../../types/message";
9
9
  import type { LocalRuntimeOptionsBase } from "./local-runtime-options";
10
+ import type { ExportedMessageRepositoryItem } from "../../runtime/utils/message-repository";
10
11
 
11
12
  const flush = () => new Promise((resolve) => setTimeout(resolve, 10));
12
13
 
14
+ afterEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+
13
18
  const createThread = (
14
19
  adapter: ChatModelAdapter,
15
20
  options?: {
16
21
  suggestion?: LocalRuntimeOptionsBase["adapters"]["suggestion"];
22
+ history?: LocalRuntimeOptionsBase["adapters"]["history"];
23
+ maxSteps?: number;
17
24
  },
18
25
  ) => {
19
26
  const core = new LocalRuntimeCore(
@@ -23,8 +30,12 @@ const createThread = (
23
30
  ...(options?.suggestion !== undefined && {
24
31
  suggestion: options.suggestion,
25
32
  }),
33
+ ...(options?.history !== undefined && {
34
+ history: options.history,
35
+ }),
26
36
  },
27
37
  unstable_humanToolNames: ["send_email"],
38
+ ...(options?.maxSteps !== undefined && { maxSteps: options.maxSteps }),
28
39
  },
29
40
  undefined,
30
41
  );
@@ -74,6 +85,64 @@ const createApprovalThread = (firstResult: ChatModelRunResult) => {
74
85
  return { thread, runs };
75
86
  };
76
87
 
88
+ describe("LocalThreadRuntimeCore events", () => {
89
+ it("isolates runEnd listener errors", async () => {
90
+ const listenerError = new Error("telemetry failed");
91
+ const consoleError = vi
92
+ .spyOn(console, "error")
93
+ .mockImplementation(() => {});
94
+ const laterListener = vi.fn();
95
+ const thread = createThread({
96
+ async run() {
97
+ return { content: [{ type: "text", text: "done" }] };
98
+ },
99
+ });
100
+
101
+ thread.unstable_on("runEnd", () => {
102
+ throw listenerError;
103
+ });
104
+ thread.unstable_on("runEnd", laterListener);
105
+
106
+ await expect(thread.append(userMessage("hello"))).resolves.toBeUndefined();
107
+
108
+ expect(laterListener).toHaveBeenCalledOnce();
109
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
110
+ expect(consoleError).toHaveBeenCalledWith(
111
+ '[assistant-ui] Thread runtime "runEnd" listener threw an error',
112
+ listenerError,
113
+ );
114
+ });
115
+
116
+ it("isolates async runEnd listener rejections", async () => {
117
+ const listenerError = new Error("async telemetry failed");
118
+ const consoleError = vi
119
+ .spyOn(console, "error")
120
+ .mockImplementation(() => {});
121
+ const laterListener = vi.fn();
122
+ const thread = createThread({
123
+ async run() {
124
+ return { content: [{ type: "text", text: "done" }] };
125
+ },
126
+ });
127
+
128
+ thread.unstable_on("runEnd", async () => {
129
+ throw listenerError;
130
+ });
131
+ thread.unstable_on("runEnd", laterListener);
132
+
133
+ await expect(thread.append(userMessage("hello"))).resolves.toBeUndefined();
134
+
135
+ expect(laterListener).toHaveBeenCalledOnce();
136
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
137
+ await vi.waitFor(() => {
138
+ expect(consoleError).toHaveBeenCalledWith(
139
+ '[assistant-ui] Thread runtime "runEnd" listener threw an error',
140
+ listenerError,
141
+ );
142
+ });
143
+ });
144
+ });
145
+
77
146
  describe("LocalThreadRuntimeCore human-in-the-loop tools", () => {
78
147
  it("pauses on requires-action while a listed tool call has no result", async () => {
79
148
  const { thread, runs } = createApprovalThread(toolCallResult("send_email"));
@@ -158,6 +227,25 @@ describe("LocalThreadRuntimeCore human-in-the-loop tools", () => {
158
227
  });
159
228
  });
160
229
 
230
+ describe("LocalThreadRuntimeCore state", () => {
231
+ it.each([
232
+ ["false", false],
233
+ ["zero", 0],
234
+ ["an empty string", ""],
235
+ ])("preserves %s model state", async (_label, state) => {
236
+ const thread = createThread({
237
+ async run() {
238
+ return { metadata: { unstable_state: state } };
239
+ },
240
+ });
241
+
242
+ await thread.append(userMessage("update state"));
243
+ await flush();
244
+
245
+ expect(thread.messages.at(-1)?.metadata.unstable_state).toBe(state);
246
+ });
247
+ });
248
+
161
249
  describe("LocalThreadRuntimeCore tool approvals", () => {
162
250
  it("pauses the run while an approval is pending, even for unlisted tools", async () => {
163
251
  const { thread, runs } = createApprovalThread(
@@ -408,6 +496,74 @@ describe("LocalThreadRuntimeCore tool approvals", () => {
408
496
  });
409
497
  });
410
498
 
499
+ describe("LocalThreadRuntimeCore cancellation", () => {
500
+ it("marks the message cancelled when a streaming adapter returns after abort", async () => {
501
+ let released!: () => void;
502
+ const streaming = new Promise<void>((resolve) => {
503
+ released = resolve;
504
+ });
505
+
506
+ const thread = createThread({
507
+ async *run({ abortSignal }) {
508
+ yield { content: [{ type: "text", text: "partial" }] };
509
+ await streaming;
510
+ if (abortSignal.aborted) return;
511
+ yield { content: [{ type: "text", text: "partial answer" }] };
512
+ },
513
+ });
514
+
515
+ const appendPromise = thread.append(userMessage("hi"));
516
+ await flush();
517
+
518
+ thread.cancelRun();
519
+ released();
520
+ await appendPromise;
521
+
522
+ expect(thread.messages.at(-1)?.status).toEqual({
523
+ type: "incomplete",
524
+ reason: "cancelled",
525
+ });
526
+ });
527
+
528
+ it("marks the message cancelled when a non-streaming adapter resolves after abort", async () => {
529
+ let released!: () => void;
530
+ const pending = new Promise<void>((resolve) => {
531
+ released = resolve;
532
+ });
533
+
534
+ const thread = createThread({
535
+ async run() {
536
+ await pending;
537
+ return { content: [{ type: "text", text: "hello" }] };
538
+ },
539
+ });
540
+
541
+ const appendPromise = thread.append(userMessage("hi"));
542
+ await flush();
543
+
544
+ thread.cancelRun();
545
+ released();
546
+ await appendPromise;
547
+
548
+ expect(thread.messages.at(-1)?.status).toEqual({
549
+ type: "incomplete",
550
+ reason: "cancelled",
551
+ });
552
+ });
553
+
554
+ it("keeps a completed run complete", async () => {
555
+ const thread = createThread({
556
+ async *run() {
557
+ yield { content: [{ type: "text", text: "hello" }] };
558
+ },
559
+ });
560
+
561
+ await thread.append(userMessage("hi"));
562
+
563
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
564
+ });
565
+ });
566
+
411
567
  describe("LocalThreadRuntimeCore suggestions", () => {
412
568
  it("cancelRun aborts pending suggestion generation", async () => {
413
569
  const generate = vi.fn().mockImplementation(
@@ -492,3 +648,592 @@ describe("LocalThreadRuntimeCore suggestions", () => {
492
648
  expect(thread.suggestions).toEqual([{ prompt: "follow up" }]);
493
649
  });
494
650
  });
651
+
652
+ describe("LocalThreadRuntimeCore tool approval persistence", () => {
653
+ const createHistory = (options?: { update?: boolean }) => {
654
+ const appended: ExportedMessageRepositoryItem[] = [];
655
+ const updated: ExportedMessageRepositoryItem[] = [];
656
+ const history = {
657
+ async load() {
658
+ return { messages: [] };
659
+ },
660
+ async append(item: ExportedMessageRepositoryItem) {
661
+ appended.push(item);
662
+ },
663
+ ...(options?.update !== false && {
664
+ async update(item: ExportedMessageRepositoryItem) {
665
+ updated.push(item);
666
+ },
667
+ }),
668
+ };
669
+ return { history, appended, updated };
670
+ };
671
+
672
+ const createApprovalThreadWithHistory = (
673
+ history: LocalRuntimeOptionsBase["adapters"]["history"],
674
+ ) => {
675
+ const runs: ChatModelRunOptions[] = [];
676
+ return createThread(
677
+ {
678
+ async run(options) {
679
+ runs.push(options);
680
+ if (runs.length === 1)
681
+ return toolCallResult("send_email", { id: "a1" });
682
+ return { content: [{ type: "text", text: "done" }] };
683
+ },
684
+ },
685
+ { history },
686
+ );
687
+ };
688
+
689
+ it("persists a run paused for approval and rewrites it once the run finishes", async () => {
690
+ const { history, appended, updated } = createHistory();
691
+ const thread = createApprovalThreadWithHistory(history);
692
+
693
+ await thread.append(userMessage("send an email"));
694
+ await flush();
695
+
696
+ const assistant = appended.find((i) => i.message.role === "assistant");
697
+ expect(assistant?.message.status?.type).toBe("requires-action");
698
+
699
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
700
+ await flush();
701
+
702
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
703
+ expect(
704
+ appended.filter((i) => i.message.id === assistant?.message.id),
705
+ ).toHaveLength(1);
706
+ expect(updated.at(-1)?.message.id).toBe(assistant?.message.id);
707
+ expect(updated.at(-1)?.message.status?.type).toBe("complete");
708
+ });
709
+
710
+ it("keeps the append-only behavior for adapters without update", async () => {
711
+ const { history, appended } = createHistory({ update: false });
712
+ const thread = createApprovalThreadWithHistory(history);
713
+
714
+ await thread.append(userMessage("send an email"));
715
+ await flush();
716
+
717
+ expect(appended.some((i) => i.message.role === "assistant")).toBe(false);
718
+
719
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
720
+ await flush();
721
+
722
+ const assistants = appended.filter((i) => i.message.role === "assistant");
723
+ expect(assistants).toHaveLength(1);
724
+ expect(assistants[0]?.message.status?.type).toBe("complete");
725
+ });
726
+
727
+ it("rewrites a restored paused message instead of appending a duplicate", async () => {
728
+ const { history, appended, updated } = createHistory();
729
+ const runs: ChatModelRunOptions[] = [];
730
+ const paused: ExportedMessageRepositoryItem = {
731
+ parentId: null,
732
+ message: {
733
+ id: "restored",
734
+ role: "assistant",
735
+ content: [toolCallPart("send_email", { id: "a1" })],
736
+ status: { type: "requires-action", reason: "tool-calls" },
737
+ createdAt: new Date(),
738
+ metadata: {
739
+ unstable_state: null,
740
+ unstable_annotations: [],
741
+ unstable_data: [],
742
+ steps: [],
743
+ custom: {},
744
+ },
745
+ },
746
+ };
747
+ const thread = createThread(
748
+ {
749
+ async run(options) {
750
+ runs.push(options);
751
+ return { content: [{ type: "text", text: "done" }] };
752
+ },
753
+ },
754
+ {
755
+ history: {
756
+ ...history,
757
+ async load() {
758
+ return { headId: "restored", messages: [paused] };
759
+ },
760
+ },
761
+ },
762
+ );
763
+
764
+ thread.__internal_load();
765
+ await flush();
766
+
767
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
768
+ await flush();
769
+
770
+ expect(runs).toHaveLength(1);
771
+ expect(appended).toHaveLength(0);
772
+ expect(updated.at(-1)?.message.id).toBe("restored");
773
+ expect(updated.at(-1)?.message.status?.type).toBe("complete");
774
+ });
775
+
776
+ it("still appends a restored paused message when the adapter cannot update", async () => {
777
+ const { history, appended } = createHistory({ update: false });
778
+ const paused: ExportedMessageRepositoryItem = {
779
+ parentId: null,
780
+ message: {
781
+ id: "restored",
782
+ role: "assistant",
783
+ content: [toolCallPart("send_email", { id: "a1" })],
784
+ status: { type: "requires-action", reason: "tool-calls" },
785
+ createdAt: new Date(),
786
+ metadata: {
787
+ unstable_state: null,
788
+ unstable_annotations: [],
789
+ unstable_data: [],
790
+ steps: [],
791
+ custom: {},
792
+ },
793
+ },
794
+ };
795
+ const thread = createThread(
796
+ {
797
+ async run() {
798
+ return { content: [{ type: "text", text: "done" }] };
799
+ },
800
+ },
801
+ {
802
+ history: {
803
+ ...history,
804
+ async load() {
805
+ return { headId: "restored", messages: [paused] };
806
+ },
807
+ },
808
+ },
809
+ );
810
+
811
+ thread.__internal_load();
812
+ await flush();
813
+
814
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
815
+ await flush();
816
+
817
+ expect(appended).toHaveLength(1);
818
+ expect(appended[0]?.message.id).toBe("restored");
819
+ expect(appended[0]?.message.status?.type).toBe("complete");
820
+ });
821
+
822
+ it("persists a partial approval decision while another tool call is still pending", async () => {
823
+ const { history, updated } = createHistory();
824
+ const runs: ChatModelRunOptions[] = [];
825
+ const twoPendingApprovals: ChatModelRunResult = {
826
+ content: [
827
+ { ...toolCallPart("send_email", { id: "a1" }), toolCallId: "call-1" },
828
+ { ...toolCallPart("send_email", { id: "a2" }), toolCallId: "call-2" },
829
+ ],
830
+ status: { type: "requires-action", reason: "tool-calls" },
831
+ };
832
+ const thread = createThread(
833
+ {
834
+ async run(options) {
835
+ runs.push(options);
836
+ if (runs.length === 1) return twoPendingApprovals;
837
+ return { content: [{ type: "text", text: "done" }] };
838
+ },
839
+ },
840
+ { history },
841
+ );
842
+
843
+ await thread.append(userMessage("send two emails"));
844
+ await flush();
845
+
846
+ const assistant = thread.messages.at(-1)!;
847
+ expect(assistant.status?.type).toBe("requires-action");
848
+
849
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
850
+ await flush();
851
+
852
+ // The second approval is still pending, so the run must not resume yet —
853
+ // but the first decision has to survive a refresh in the meantime.
854
+ expect(runs).toHaveLength(1);
855
+ const persisted = updated
856
+ .at(-1)
857
+ ?.message.content.find(
858
+ (c) => c.type === "tool-call" && c.toolCallId === "call-1",
859
+ );
860
+ expect(
861
+ persisted?.type === "tool-call" && persisted.approval?.approved,
862
+ ).toBe(true);
863
+ });
864
+
865
+ it("persists a partial tool result while another human tool call is still pending", async () => {
866
+ const { history, updated } = createHistory();
867
+ const runs: ChatModelRunOptions[] = [];
868
+ const twoHumanTools: ChatModelRunResult = {
869
+ content: [
870
+ {
871
+ type: "tool-call",
872
+ toolCallId: "call-1",
873
+ toolName: "send_email",
874
+ args: {},
875
+ argsText: "{}",
876
+ },
877
+ {
878
+ type: "tool-call",
879
+ toolCallId: "call-2",
880
+ toolName: "send_email",
881
+ args: {},
882
+ argsText: "{}",
883
+ },
884
+ ],
885
+ status: { type: "requires-action", reason: "tool-calls" },
886
+ };
887
+ const thread = createThread(
888
+ {
889
+ async run(options) {
890
+ runs.push(options);
891
+ if (runs.length === 1) return twoHumanTools;
892
+ return { content: [{ type: "text", text: "done" }] };
893
+ },
894
+ },
895
+ { history },
896
+ );
897
+
898
+ await thread.append(userMessage("send two emails"));
899
+ await flush();
900
+
901
+ const assistant = thread.messages.at(-1)!;
902
+
903
+ thread.addToolResult({
904
+ messageId: assistant.id,
905
+ toolName: "send_email",
906
+ toolCallId: "call-1",
907
+ result: { ok: true },
908
+ isError: false,
909
+ });
910
+ await flush();
911
+
912
+ expect(runs).toHaveLength(1);
913
+ const persisted = updated
914
+ .at(-1)
915
+ ?.message.content.find(
916
+ (c) => c.type === "tool-call" && c.toolCallId === "call-1",
917
+ );
918
+ expect(persisted?.type === "tool-call" && persisted.result).toEqual({
919
+ ok: true,
920
+ });
921
+ });
922
+
923
+ it("persists a multi-step run once instead of writing intermediate steps", async () => {
924
+ const { history, appended, updated } = createHistory();
925
+ const runs: ChatModelRunOptions[] = [];
926
+ const thread = createThread(
927
+ {
928
+ async run(options) {
929
+ runs.push(options);
930
+ if (runs.length === 1)
931
+ return {
932
+ content: [
933
+ { ...toolCallPart("lookup_weather"), result: { ok: true } },
934
+ ],
935
+ status: { type: "requires-action", reason: "tool-calls" },
936
+ };
937
+ return { content: [{ type: "text", text: "done" }] };
938
+ },
939
+ },
940
+ { history },
941
+ );
942
+
943
+ await thread.append(userMessage("what is the weather"));
944
+ await flush();
945
+
946
+ expect(runs).toHaveLength(2);
947
+ const assistants = appended.filter((i) => i.message.role === "assistant");
948
+ expect(assistants).toHaveLength(1);
949
+ expect(assistants[0]?.message.status?.type).toBe("complete");
950
+ expect(updated).toHaveLength(0);
951
+ });
952
+
953
+ it("rewrites the same entry when a resumed run pauses again", async () => {
954
+ const { history, appended, updated } = createHistory();
955
+ const runs: ChatModelRunOptions[] = [];
956
+ const thread = createThread(
957
+ {
958
+ async run(options) {
959
+ runs.push(options);
960
+ if (runs.length === 1)
961
+ return toolCallResult("send_email", { id: "a1" });
962
+ if (runs.length === 2)
963
+ return {
964
+ content: [
965
+ {
966
+ ...toolCallPart("send_email", { id: "a2" }),
967
+ toolCallId: "call-2",
968
+ },
969
+ ],
970
+ status: { type: "requires-action", reason: "tool-calls" },
971
+ };
972
+ return { content: [{ type: "text", text: "done" }] };
973
+ },
974
+ },
975
+ { history },
976
+ );
977
+
978
+ await thread.append(userMessage("send two emails"));
979
+ await flush();
980
+
981
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
982
+ await flush();
983
+
984
+ expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
985
+
986
+ thread.respondToToolApproval({ approvalId: "a2", approved: true });
987
+ await flush();
988
+
989
+ expect(runs).toHaveLength(3);
990
+ const assistants = appended.filter((i) => i.message.role === "assistant");
991
+ expect(assistants).toHaveLength(1);
992
+ expect(assistants[0]?.message.status?.type).toBe("requires-action");
993
+ expect(updated).toHaveLength(2);
994
+ expect(
995
+ updated.every((i) => i.message.id === assistants[0]?.message.id),
996
+ ).toBe(true);
997
+ expect(updated.at(-1)?.message.status?.type).toBe("complete");
998
+ });
999
+
1000
+ it("finalizes the history entry when a resumed run hits max steps", async () => {
1001
+ const { history, appended, updated } = createHistory();
1002
+ const runs: ChatModelRunOptions[] = [];
1003
+ const thread = createThread(
1004
+ {
1005
+ async run(options) {
1006
+ runs.push(options);
1007
+ return {
1008
+ content: [toolCallPart("send_email", { id: "a1" })],
1009
+ status: { type: "requires-action", reason: "tool-calls" },
1010
+ metadata: { steps: [{}] },
1011
+ };
1012
+ },
1013
+ },
1014
+ { history, maxSteps: 1 },
1015
+ );
1016
+
1017
+ await thread.append(userMessage("send an email"));
1018
+ await flush();
1019
+
1020
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
1021
+ await flush();
1022
+
1023
+ expect(runs).toHaveLength(1);
1024
+ expect(thread.messages.at(-1)?.status?.type).toBe("incomplete");
1025
+ const assistants = appended.filter((i) => i.message.role === "assistant");
1026
+ expect(assistants).toHaveLength(1);
1027
+ expect(updated).toHaveLength(1);
1028
+ expect(updated.at(-1)?.message.status?.type).toBe("incomplete");
1029
+ });
1030
+
1031
+ it("orders the terminal rewrite after an in-flight partial decision write", async () => {
1032
+ const order: string[] = [];
1033
+ let releasePartial!: () => void;
1034
+ const gate = new Promise<void>((resolve) => {
1035
+ releasePartial = resolve;
1036
+ });
1037
+ const history = {
1038
+ async load() {
1039
+ return { messages: [] };
1040
+ },
1041
+ async append() {},
1042
+ async update(item: ExportedMessageRepositoryItem) {
1043
+ const kind =
1044
+ item.message.status?.type === "requires-action"
1045
+ ? "partial"
1046
+ : "terminal";
1047
+ order.push(`${kind}:start`);
1048
+ if (kind === "partial") await gate;
1049
+ order.push(`${kind}:end`);
1050
+ },
1051
+ };
1052
+ const runs: ChatModelRunOptions[] = [];
1053
+ const twoPendingApprovals: ChatModelRunResult = {
1054
+ content: [
1055
+ { ...toolCallPart("send_email", { id: "a1" }), toolCallId: "call-1" },
1056
+ { ...toolCallPart("send_email", { id: "a2" }), toolCallId: "call-2" },
1057
+ ],
1058
+ status: { type: "requires-action", reason: "tool-calls" },
1059
+ };
1060
+ const thread = createThread(
1061
+ {
1062
+ async run(options) {
1063
+ runs.push(options);
1064
+ if (runs.length === 1) return twoPendingApprovals;
1065
+ return { content: [{ type: "text", text: "done" }] };
1066
+ },
1067
+ },
1068
+ { history },
1069
+ );
1070
+
1071
+ await thread.append(userMessage("send two emails"));
1072
+ await flush();
1073
+
1074
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
1075
+ thread.respondToToolApproval({ approvalId: "a2", approved: true });
1076
+ await flush();
1077
+
1078
+ expect(order).toEqual(["partial:start"]);
1079
+
1080
+ releasePartial();
1081
+ await flush();
1082
+
1083
+ expect(order).toEqual([
1084
+ "partial:start",
1085
+ "partial:end",
1086
+ "terminal:start",
1087
+ "terminal:end",
1088
+ ]);
1089
+ });
1090
+ });
1091
+
1092
+ describe("LocalThreadRuntimeCore runs", () => {
1093
+ const createPlainThread = (
1094
+ adapter: ChatModelAdapter,
1095
+ options?: { maxSteps?: number },
1096
+ ) => {
1097
+ const core = new LocalRuntimeCore(
1098
+ {
1099
+ adapters: { chatModel: adapter },
1100
+ ...(options?.maxSteps !== undefined && { maxSteps: options.maxSteps }),
1101
+ },
1102
+ undefined,
1103
+ );
1104
+ return core.threads.getMainThreadRuntimeCore();
1105
+ };
1106
+
1107
+ it("appends the user message and the adapter result", async () => {
1108
+ const run = vi.fn(
1109
+ async (): Promise<ChatModelRunResult> => ({
1110
+ content: [{ type: "text", text: "Hello!" }],
1111
+ status: { type: "complete", reason: "stop" },
1112
+ }),
1113
+ );
1114
+ const thread = createPlainThread({ run });
1115
+
1116
+ await thread.append(userMessage("Hi"));
1117
+
1118
+ expect(run).toHaveBeenCalledOnce();
1119
+ expect(thread.messages).toHaveLength(2);
1120
+ expect(thread.messages[0]?.role).toBe("user");
1121
+ const assistant = thread.messages[1]!;
1122
+ expect(assistant.role).toBe("assistant");
1123
+ expect(assistant.content).toContainEqual(
1124
+ expect.objectContaining({ type: "text", text: "Hello!" }),
1125
+ );
1126
+ expect(assistant.status).toEqual({ type: "complete", reason: "stop" });
1127
+ });
1128
+
1129
+ it("streams the assistant response via an async generator", async () => {
1130
+ const thread = createPlainThread({
1131
+ async *run() {
1132
+ yield { content: [{ type: "text" as const, text: "Hel" }] };
1133
+ yield {
1134
+ content: [{ type: "text" as const, text: "Hello world" }],
1135
+ status: { type: "complete" as const, reason: "stop" as const },
1136
+ };
1137
+ },
1138
+ });
1139
+
1140
+ await thread.append(userMessage("Stream test"));
1141
+
1142
+ const assistant = thread.messages.at(-1)!;
1143
+ expect(assistant.content).toContainEqual(
1144
+ expect.objectContaining({ type: "text", text: "Hello world" }),
1145
+ );
1146
+ expect(assistant.status).toEqual({ type: "complete", reason: "stop" });
1147
+ });
1148
+
1149
+ it("marks the message errored when the adapter rejects", async () => {
1150
+ const thread = createPlainThread({
1151
+ async run() {
1152
+ throw new Error("Model unavailable");
1153
+ },
1154
+ });
1155
+
1156
+ await expect(thread.append(userMessage("Error test"))).rejects.toThrow(
1157
+ "Model unavailable",
1158
+ );
1159
+
1160
+ const assistant = thread.messages.at(-1)!;
1161
+ expect(assistant.status).toEqual({
1162
+ type: "incomplete",
1163
+ reason: "error",
1164
+ error: { code: "unknown", message: "Model unavailable" },
1165
+ });
1166
+ });
1167
+
1168
+ it("does not run again after a tool result once maxSteps is reached", async () => {
1169
+ const run = vi.fn(
1170
+ async (): Promise<ChatModelRunResult> => ({
1171
+ content: [
1172
+ {
1173
+ type: "tool-call",
1174
+ toolCallId: "tc1",
1175
+ toolName: "myTool",
1176
+ args: {},
1177
+ argsText: "{}",
1178
+ },
1179
+ ],
1180
+ status: { type: "requires-action", reason: "tool-calls" },
1181
+ metadata: {
1182
+ steps: [{ usage: { promptTokens: 10, completionTokens: 5 } }],
1183
+ },
1184
+ }),
1185
+ );
1186
+ const thread = createPlainThread({ run }, { maxSteps: 1 });
1187
+
1188
+ await thread.append(userMessage("Tool call"));
1189
+
1190
+ thread.addToolResult({
1191
+ messageId: thread.messages.at(-1)!.id,
1192
+ toolName: "myTool",
1193
+ toolCallId: "tc1",
1194
+ result: "result",
1195
+ isError: false,
1196
+ });
1197
+ await flush();
1198
+
1199
+ expect(run).toHaveBeenCalledTimes(1);
1200
+ expect(thread.messages.at(-1)?.status).toMatchObject({
1201
+ type: "incomplete",
1202
+ reason: "tool-calls",
1203
+ });
1204
+ });
1205
+
1206
+ it("derives capabilities from the configured adapters", () => {
1207
+ const adapter: ChatModelAdapter = {
1208
+ run: async () => ({ content: [] }),
1209
+ };
1210
+ const thread = createPlainThread(adapter);
1211
+
1212
+ expect(thread.capabilities.speech).toBe(false);
1213
+ expect(thread.capabilities.dictation).toBe(false);
1214
+ expect(thread.capabilities.attachments).toBe(false);
1215
+ expect(thread.capabilities.feedback).toBe(false);
1216
+
1217
+ thread.__internal_setOptions({
1218
+ adapters: {
1219
+ chatModel: adapter,
1220
+ speech: {} as any,
1221
+ dictation: {} as any,
1222
+ attachments: {} as any,
1223
+ feedback: {} as any,
1224
+ },
1225
+ });
1226
+
1227
+ expect(thread.capabilities.speech).toBe(true);
1228
+ expect(thread.capabilities.dictation).toBe(true);
1229
+ expect(thread.capabilities.attachments).toBe(true);
1230
+ expect(thread.capabilities.feedback).toBe(true);
1231
+
1232
+ thread.__internal_setOptions({ adapters: { chatModel: adapter } });
1233
+
1234
+ expect(thread.capabilities.speech).toBe(false);
1235
+ expect(thread.capabilities.dictation).toBe(false);
1236
+ expect(thread.capabilities.attachments).toBe(false);
1237
+ expect(thread.capabilities.feedback).toBe(false);
1238
+ });
1239
+ });