@assistant-ui/core 0.3.18 → 0.3.20

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 (804) hide show
  1. package/dist/adapters/attachment.d.ts +9 -10
  2. package/dist/adapters/attachment.d.ts.map +1 -1
  3. package/dist/adapters/attachment.js +2 -1
  4. package/dist/adapters/attachment.js.map +1 -1
  5. package/dist/adapters/directive-formatter.d.ts +1 -2
  6. package/dist/adapters/directive-formatter.d.ts.map +1 -1
  7. package/dist/adapters/feedback.d.ts +2 -2
  8. package/dist/adapters/feedback.d.ts.map +1 -1
  9. package/dist/adapters/speech.d.ts +6 -7
  10. package/dist/adapters/speech.d.ts.map +1 -1
  11. package/dist/adapters/speech.js +13 -3
  12. package/dist/adapters/speech.js.map +1 -1
  13. package/dist/adapters/suggestion.d.ts +5 -6
  14. package/dist/adapters/suggestion.d.ts.map +1 -1
  15. package/dist/adapters/thread-history.d.ts +10 -7
  16. package/dist/adapters/thread-history.d.ts.map +1 -1
  17. package/dist/adapters/trigger.d.ts +1 -2
  18. package/dist/adapters/trigger.d.ts.map +1 -1
  19. package/dist/adapters/voice.d.ts +5 -6
  20. package/dist/adapters/voice.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal/duplicate-detection.d.ts +1 -2
  24. package/dist/internal/duplicate-detection.d.ts.map +1 -1
  25. package/dist/internal.d.ts +1 -1
  26. package/dist/model-context/frame/host.d.ts +1 -2
  27. package/dist/model-context/frame/host.d.ts.map +1 -1
  28. package/dist/model-context/frame/host.js +26 -11
  29. package/dist/model-context/frame/host.js.map +1 -1
  30. package/dist/model-context/frame/provider.d.ts +1 -2
  31. package/dist/model-context/frame/provider.d.ts.map +1 -1
  32. package/dist/model-context/frame/types.d.ts +5 -6
  33. package/dist/model-context/frame/types.d.ts.map +1 -1
  34. package/dist/model-context/interactable-composer-metadata.d.ts +9 -10
  35. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
  36. package/dist/model-context/interactable-composer-metadata.js +11 -9
  37. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  38. package/dist/model-context/registry-handles.d.ts +3 -4
  39. package/dist/model-context/registry-handles.d.ts.map +1 -1
  40. package/dist/model-context/registry.d.ts +1 -2
  41. package/dist/model-context/registry.d.ts.map +1 -1
  42. package/dist/model-context/tool-call-text.d.ts +2 -3
  43. package/dist/model-context/tool-call-text.d.ts.map +1 -1
  44. package/dist/model-context/tool.d.ts +2 -3
  45. package/dist/model-context/tool.d.ts.map +1 -1
  46. package/dist/model-context/types.d.ts +8 -9
  47. package/dist/model-context/types.d.ts.map +1 -1
  48. package/dist/react/AssistantProvider.d.ts +3 -4
  49. package/dist/react/AssistantProvider.d.ts.map +1 -1
  50. package/dist/react/AssistantRuntimeProvider.d.ts +1 -2
  51. package/dist/react/AssistantRuntimeProvider.d.ts.map +1 -1
  52. package/dist/react/RuntimeAdapter.d.ts +2 -3
  53. package/dist/react/RuntimeAdapter.d.ts.map +1 -1
  54. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts +4 -5
  55. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  56. package/dist/react/adapters/LocalStorageThreadListAdapter.js +43 -19
  57. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  58. package/dist/react/adapters/TitleGenerationAdapter.d.ts +2 -3
  59. package/dist/react/adapters/TitleGenerationAdapter.d.ts.map +1 -1
  60. package/dist/react/client/AdaptedRemoteThread.d.ts +1 -2
  61. package/dist/react/client/AdaptedRemoteThread.d.ts.map +1 -1
  62. package/dist/react/client/DataRenderers.d.ts +1 -2
  63. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  64. package/dist/react/client/DataRenderers.js +20 -15
  65. package/dist/react/client/DataRenderers.js.map +1 -1
  66. package/dist/react/client/InMemoryThreadList.d.ts +3 -4
  67. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  68. package/dist/react/client/Interactables.d.ts +1 -2
  69. package/dist/react/client/Interactables.d.ts.map +1 -1
  70. package/dist/react/client/Interactables.js +11 -5
  71. package/dist/react/client/Interactables.js.map +1 -1
  72. package/dist/react/client/RemoteThreadList.d.ts +2 -3
  73. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  74. package/dist/react/client/RemoteThreadList.js +3 -2
  75. package/dist/react/client/RemoteThreadList.js.map +1 -1
  76. package/dist/react/client/Tools.d.ts +2 -2
  77. package/dist/react/client/Tools.d.ts.map +1 -1
  78. package/dist/react/client/interactable-model-context.d.ts +2 -3
  79. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  80. package/dist/react/client/interactable-model-context.js +3 -2
  81. package/dist/react/client/interactable-model-context.js.map +1 -1
  82. package/dist/react/index.d.ts +2 -2
  83. package/dist/react/index.js +2 -2
  84. package/dist/react/interactables-legacy/Interactables.d.ts +1 -2
  85. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  86. package/dist/react/interactables-legacy/interactable-model-context.d.ts +2 -3
  87. package/dist/react/interactables-legacy/interactable-model-context.d.ts.map +1 -1
  88. package/dist/react/interactables-legacy/scopes.d.ts +9 -10
  89. package/dist/react/interactables-legacy/scopes.d.ts.map +1 -1
  90. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts +2 -3
  91. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
  92. package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
  93. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
  94. package/dist/react/interactables-legacy/useInteractableState.d.ts +1 -2
  95. package/dist/react/interactables-legacy/useInteractableState.d.ts.map +1 -1
  96. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +1 -2
  97. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
  98. package/dist/react/model-context/define-mcp-toolkit.d.ts +4 -5
  99. package/dist/react/model-context/define-mcp-toolkit.d.ts.map +1 -1
  100. package/dist/react/model-context/define-toolkit.d.ts +2 -3
  101. package/dist/react/model-context/define-toolkit.d.ts.map +1 -1
  102. package/dist/react/model-context/external-tool.d.ts +1 -2
  103. package/dist/react/model-context/external-tool.d.ts.map +1 -1
  104. package/dist/react/model-context/human-tool.d.ts +3 -4
  105. package/dist/react/model-context/human-tool.d.ts.map +1 -1
  106. package/dist/react/model-context/interactableTool.d.ts +3 -4
  107. package/dist/react/model-context/interactableTool.d.ts.map +1 -1
  108. package/dist/react/model-context/makeAssistantDataUI.d.ts +2 -3
  109. package/dist/react/model-context/makeAssistantDataUI.d.ts.map +1 -1
  110. package/dist/react/model-context/makeAssistantTool.d.ts +2 -3
  111. package/dist/react/model-context/makeAssistantTool.d.ts.map +1 -1
  112. package/dist/react/model-context/makeAssistantToolUI.d.ts +2 -3
  113. package/dist/react/model-context/makeAssistantToolUI.d.ts.map +1 -1
  114. package/dist/react/model-context/provider-tool.d.ts +2 -3
  115. package/dist/react/model-context/provider-tool.d.ts.map +1 -1
  116. package/dist/react/model-context/stub-tool.d.ts +1 -2
  117. package/dist/react/model-context/stub-tool.d.ts.map +1 -1
  118. package/dist/react/model-context/toolbox.d.ts +9 -10
  119. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  120. package/dist/react/model-context/useAssistantContext.d.ts +2 -2
  121. package/dist/react/model-context/useAssistantContext.d.ts.map +1 -1
  122. package/dist/react/model-context/useAssistantDataUI.d.ts +2 -3
  123. package/dist/react/model-context/useAssistantDataUI.d.ts.map +1 -1
  124. package/dist/react/model-context/useAssistantInstructions.d.ts +2 -2
  125. package/dist/react/model-context/useAssistantInstructions.d.ts.map +1 -1
  126. package/dist/react/model-context/useAssistantTool.d.ts +2 -3
  127. package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
  128. package/dist/react/model-context/useAssistantToolUI.d.ts +2 -3
  129. package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
  130. package/dist/react/model-context/useAuiToolOverrides.d.ts +1 -2
  131. package/dist/react/model-context/useAuiToolOverrides.d.ts.map +1 -1
  132. package/dist/react/model-context/useInlineRender.d.ts +1 -2
  133. package/dist/react/model-context/useInlineRender.d.ts.map +1 -1
  134. package/dist/react/model-context/useInteractable.d.ts +4 -5
  135. package/dist/react/model-context/useInteractable.d.ts.map +1 -1
  136. package/dist/react/model-context/useInteractable.js +4 -1
  137. package/dist/react/model-context/useInteractable.js.map +1 -1
  138. package/dist/react/model-context/useInteractableState.d.ts +1 -2
  139. package/dist/react/model-context/useInteractableState.d.ts.map +1 -1
  140. package/dist/react/model-context/useInteractableVersions.d.ts +1 -2
  141. package/dist/react/model-context/useInteractableVersions.d.ts.map +1 -1
  142. package/dist/react/model-context/useInteractableVersions.js +1 -1
  143. package/dist/react/model-context/useToolArgsStatus.d.ts +2 -3
  144. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
  145. package/dist/react/primitive-hooks/useActionBarCopy.d.ts +2 -3
  146. package/dist/react/primitive-hooks/useActionBarCopy.d.ts.map +1 -1
  147. package/dist/react/primitive-hooks/useActionBarEdit.d.ts +1 -2
  148. package/dist/react/primitive-hooks/useActionBarEdit.d.ts.map +1 -1
  149. package/dist/react/primitive-hooks/useActionBarFeedback.d.ts +2 -3
  150. package/dist/react/primitive-hooks/useActionBarFeedback.d.ts.map +1 -1
  151. package/dist/react/primitive-hooks/useActionBarReload.d.ts +1 -2
  152. package/dist/react/primitive-hooks/useActionBarReload.d.ts.map +1 -1
  153. package/dist/react/primitive-hooks/useActionBarSpeak.d.ts +1 -2
  154. package/dist/react/primitive-hooks/useActionBarSpeak.d.ts.map +1 -1
  155. package/dist/react/primitive-hooks/useActionBarStopSpeaking.d.ts +1 -2
  156. package/dist/react/primitive-hooks/useActionBarStopSpeaking.d.ts.map +1 -1
  157. package/dist/react/primitive-hooks/useBranchPickerNext.d.ts +1 -2
  158. package/dist/react/primitive-hooks/useBranchPickerNext.d.ts.map +1 -1
  159. package/dist/react/primitive-hooks/useBranchPickerPrevious.d.ts +1 -2
  160. package/dist/react/primitive-hooks/useBranchPickerPrevious.d.ts.map +1 -1
  161. package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts +1 -2
  162. package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -1
  163. package/dist/react/primitive-hooks/useComposerCancel.d.ts +1 -2
  164. package/dist/react/primitive-hooks/useComposerCancel.d.ts.map +1 -1
  165. package/dist/react/primitive-hooks/useComposerDictate.d.ts +1 -2
  166. package/dist/react/primitive-hooks/useComposerDictate.d.ts.map +1 -1
  167. package/dist/react/primitive-hooks/useComposerSend.d.ts +1 -2
  168. package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
  169. package/dist/react/primitive-hooks/useEditComposerCancel.d.ts +1 -2
  170. package/dist/react/primitive-hooks/useEditComposerCancel.d.ts.map +1 -1
  171. package/dist/react/primitive-hooks/useEditComposerSend.d.ts +1 -2
  172. package/dist/react/primitive-hooks/useEditComposerSend.d.ts.map +1 -1
  173. package/dist/react/primitive-hooks/useMessageBranching.d.ts +1 -2
  174. package/dist/react/primitive-hooks/useMessageBranching.d.ts.map +1 -1
  175. package/dist/react/primitive-hooks/useMessageError.d.ts +1 -2
  176. package/dist/react/primitive-hooks/useMessageError.d.ts.map +1 -1
  177. package/dist/react/primitive-hooks/useMessageReload.d.ts +1 -2
  178. package/dist/react/primitive-hooks/useMessageReload.d.ts.map +1 -1
  179. package/dist/react/primitive-hooks/useSuggestionTrigger.d.ts +2 -3
  180. package/dist/react/primitive-hooks/useSuggestionTrigger.d.ts.map +1 -1
  181. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +1 -2
  182. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
  183. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +1 -2
  184. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
  185. package/dist/react/primitive-hooks/useThreadListItemArchive.d.ts +1 -2
  186. package/dist/react/primitive-hooks/useThreadListItemArchive.d.ts.map +1 -1
  187. package/dist/react/primitive-hooks/useThreadListItemDelete.d.ts +1 -2
  188. package/dist/react/primitive-hooks/useThreadListItemDelete.d.ts.map +1 -1
  189. package/dist/react/primitive-hooks/useThreadListItemTrigger.d.ts +1 -2
  190. package/dist/react/primitive-hooks/useThreadListItemTrigger.d.ts.map +1 -1
  191. package/dist/react/primitive-hooks/useThreadListItemUnarchive.d.ts +1 -2
  192. package/dist/react/primitive-hooks/useThreadListItemUnarchive.d.ts.map +1 -1
  193. package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts +1 -2
  194. package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts.map +1 -1
  195. package/dist/react/primitive-hooks/useThreadListNew.d.ts +1 -2
  196. package/dist/react/primitive-hooks/useThreadListNew.d.ts.map +1 -1
  197. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts +1 -2
  198. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts.map +1 -1
  199. package/dist/react/primitive-hooks/useThreadMessages.d.ts +1 -2
  200. package/dist/react/primitive-hooks/useThreadMessages.d.ts.map +1 -1
  201. package/dist/react/primitive-hooks/useVoice.d.ts +3 -4
  202. package/dist/react/primitive-hooks/useVoice.d.ts.map +1 -1
  203. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts +2 -3
  204. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts.map +1 -1
  205. package/dist/react/primitives/composer/ComposerAttachments.d.ts +4 -5
  206. package/dist/react/primitives/composer/ComposerAttachments.d.ts.map +1 -1
  207. package/dist/react/primitives/composer/ComposerIf.d.ts +4 -5
  208. package/dist/react/primitives/composer/ComposerIf.d.ts.map +1 -1
  209. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -3
  210. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  211. package/dist/react/primitives/generativeUI/GenerativeUI.d.ts +4 -5
  212. package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
  213. package/dist/react/primitives/generativeUI/GenerativeUI.js +27 -28
  214. package/dist/react/primitives/generativeUI/GenerativeUI.js.map +1 -1
  215. package/dist/react/primitives/message/MessageAttachments.d.ts +4 -5
  216. package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
  217. package/dist/react/primitives/message/MessageGroupedParts.d.ts +13 -5
  218. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  219. package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
  220. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  221. package/dist/react/primitives/message/MessageParts.d.ts +10 -10
  222. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  223. package/dist/react/primitives/message/MessageQuote.d.ts +2 -3
  224. package/dist/react/primitives/message/MessageQuote.d.ts.map +1 -1
  225. package/dist/react/primitives/messagePart/MessagePartInProgress.d.ts +2 -3
  226. package/dist/react/primitives/messagePart/MessagePartInProgress.d.ts.map +1 -1
  227. package/dist/react/primitives/part/PartMessages.d.ts +4 -4
  228. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  229. package/dist/react/primitives/part/PartMessages.js +1 -0
  230. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  231. package/dist/react/primitives/thread/ThreadMessages.d.ts +7 -8
  232. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  233. package/dist/react/primitives/thread/ThreadMessages.js +6 -5
  234. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  235. package/dist/react/primitives/thread/ThreadSuggestions.d.ts +5 -6
  236. package/dist/react/primitives/thread/ThreadSuggestions.d.ts.map +1 -1
  237. package/dist/react/primitives/threadList/ThreadListItems.d.ts +4 -5
  238. package/dist/react/primitives/threadList/ThreadListItems.d.ts.map +1 -1
  239. package/dist/react/primitives/threadListItem/ThreadListItemTitle.d.ts +2 -3
  240. package/dist/react/primitives/threadListItem/ThreadListItemTitle.d.ts.map +1 -1
  241. package/dist/react/providers/AttachmentByIndexProvider.d.ts +2 -3
  242. package/dist/react/providers/AttachmentByIndexProvider.d.ts.map +1 -1
  243. package/dist/react/providers/ChainOfThoughtByIndicesProvider.d.ts +1 -2
  244. package/dist/react/providers/ChainOfThoughtByIndicesProvider.d.ts.map +1 -1
  245. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts +1 -2
  246. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
  247. package/dist/react/providers/MessageByIdProvider.d.ts +1 -2
  248. package/dist/react/providers/MessageByIdProvider.d.ts.map +1 -1
  249. package/dist/react/providers/MessageByIndexProvider.d.ts +1 -2
  250. package/dist/react/providers/MessageByIndexProvider.d.ts.map +1 -1
  251. package/dist/react/providers/PartByIndexProvider.d.ts +1 -2
  252. package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
  253. package/dist/react/providers/QueueItemByIndexProvider.d.ts +2 -3
  254. package/dist/react/providers/QueueItemByIndexProvider.d.ts.map +1 -1
  255. package/dist/react/providers/ReadonlyThreadProvider.d.ts +2 -3
  256. package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
  257. package/dist/react/providers/SuggestionByIndexProvider.d.ts +2 -3
  258. package/dist/react/providers/SuggestionByIndexProvider.d.ts.map +1 -1
  259. package/dist/react/providers/TextMessagePartProvider.d.ts +1 -2
  260. package/dist/react/providers/TextMessagePartProvider.d.ts.map +1 -1
  261. package/dist/react/providers/ThreadListItemByIndexProvider.d.ts +1 -2
  262. package/dist/react/providers/ThreadListItemByIndexProvider.d.ts.map +1 -1
  263. package/dist/react/providers/ThreadListItemRuntimeProvider.d.ts +1 -2
  264. package/dist/react/providers/ThreadListItemRuntimeProvider.d.ts.map +1 -1
  265. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -2
  266. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  267. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +17 -7
  268. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  269. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +1 -2
  270. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  271. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +2 -1
  272. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  273. package/dist/react/runtimes/RemoteThreadResource.d.ts +4 -5
  274. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  275. package/dist/react/runtimes/RuntimeAdapterProvider.d.ts +3 -3
  276. package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -1
  277. package/dist/react/runtimes/assistant-transport/commandQueue.d.ts +2 -3
  278. package/dist/react/runtimes/assistant-transport/commandQueue.d.ts.map +1 -1
  279. package/dist/react/runtimes/assistant-transport/replayBoundaryStream.d.ts +3 -4
  280. package/dist/react/runtimes/assistant-transport/replayBoundaryStream.d.ts.map +1 -1
  281. package/dist/react/runtimes/assistant-transport/runManager.d.ts +2 -3
  282. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  283. package/dist/react/runtimes/assistant-transport/types.d.ts +17 -18
  284. package/dist/react/runtimes/assistant-transport/types.d.ts.map +1 -1
  285. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts +4 -5
  286. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  287. package/dist/react/runtimes/assistant-transport/useConvertedState.d.ts +1 -2
  288. package/dist/react/runtimes/assistant-transport/useConvertedState.d.ts.map +1 -1
  289. package/dist/react/runtimes/assistant-transport/useLatestRef.d.ts +1 -2
  290. package/dist/react/runtimes/assistant-transport/useLatestRef.d.ts.map +1 -1
  291. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -26
  292. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  293. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +152 -378
  294. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  295. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +2 -2
  296. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  297. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
  298. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  299. package/dist/react/runtimes/cloud/auiV0.d.ts +51 -18
  300. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  301. package/dist/react/runtimes/cloud/auiV0.js +125 -17
  302. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  303. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +6 -6
  304. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  305. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +4 -0
  306. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  307. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +1 -2
  308. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -1
  309. package/dist/react/runtimes/cloud/sdkIdentity.d.ts +5 -0
  310. package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
  311. package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
  312. package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
  313. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +1 -2
  314. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  315. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.d.ts +1 -2
  316. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.d.ts.map +1 -1
  317. package/dist/react/runtimes/createMessageConverter.d.ts +1 -2
  318. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  319. package/dist/react/runtimes/createRuntimeExtras.d.ts +3 -4
  320. package/dist/react/runtimes/createRuntimeExtras.d.ts.map +1 -1
  321. package/dist/react/runtimes/external-message-converter.d.ts +14 -4
  322. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  323. package/dist/react/runtimes/external-message-converter.js +26 -61
  324. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  325. package/dist/react/runtimes/useExternalStoreRuntime.d.ts +1 -2
  326. package/dist/react/runtimes/useExternalStoreRuntime.d.ts.map +1 -1
  327. package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts +1 -2
  328. package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts.map +1 -1
  329. package/dist/react/runtimes/useLocalRuntime.d.ts +3 -4
  330. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  331. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts +1 -2
  332. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  333. package/dist/react/runtimes/useRemoteThreadListRuntime.js +3 -3
  334. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  335. package/dist/react/runtimes/useRuntimeAdapters.d.ts +4 -4
  336. package/dist/react/runtimes/useRuntimeAdapters.d.ts.map +1 -1
  337. package/dist/react/runtimes/useStreamingTiming.d.ts +2 -2
  338. package/dist/react/runtimes/useStreamingTiming.d.ts.map +1 -1
  339. package/dist/react/types/MessagePartComponentTypes.d.ts +26 -27
  340. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  341. package/dist/react/types/scopes/dataRenderers.d.ts +3 -4
  342. package/dist/react/types/scopes/dataRenderers.d.ts.map +1 -1
  343. package/dist/react/types/scopes/interactables.d.ts +10 -11
  344. package/dist/react/types/scopes/interactables.d.ts.map +1 -1
  345. package/dist/react/types/scopes/tools.d.ts +4 -5
  346. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  347. package/dist/react/utils/getMessageQuote.d.ts +1 -2
  348. package/dist/react/utils/getMessageQuote.d.ts.map +1 -1
  349. package/dist/react/utils/groupParts.d.ts +13 -9
  350. package/dist/react/utils/groupParts.d.ts.map +1 -1
  351. package/dist/react/utils/groupParts.js +6 -0
  352. package/dist/react/utils/groupParts.js.map +1 -1
  353. package/dist/react/utils/useJSONEqual.d.ts +3 -2
  354. package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
  355. package/dist/react/utils/useJSONEqual.js +6 -1
  356. package/dist/react/utils/useJSONEqual.js.map +1 -1
  357. package/dist/react/utils/useJSONSchemaDependency.d.ts +6 -0
  358. package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
  359. package/dist/react/utils/useJSONSchemaDependency.js +20 -0
  360. package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
  361. package/dist/runtime/api/assistant-runtime.d.ts +2 -3
  362. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  363. package/dist/runtime/api/attachment-runtime.d.ts +6 -7
  364. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  365. package/dist/runtime/api/bindings.d.ts +5 -6
  366. package/dist/runtime/api/bindings.d.ts.map +1 -1
  367. package/dist/runtime/api/composer-runtime.d.ts +10 -10
  368. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  369. package/dist/runtime/api/message-part-runtime.d.ts +3 -4
  370. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  371. package/dist/runtime/api/message-part-runtime.js +3 -2
  372. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  373. package/dist/runtime/api/message-runtime.d.ts +8 -6
  374. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  375. package/dist/runtime/api/message-runtime.js +3 -2
  376. package/dist/runtime/api/message-runtime.js.map +1 -1
  377. package/dist/runtime/api/paths.d.ts +6 -7
  378. package/dist/runtime/api/paths.d.ts.map +1 -1
  379. package/dist/runtime/api/thread-list-item-runtime.d.ts +8 -8
  380. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  381. package/dist/runtime/api/thread-list-runtime.d.ts +4 -5
  382. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  383. package/dist/runtime/api/thread-runtime.d.ts +10 -11
  384. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  385. package/dist/runtime/api/thread-runtime.js +2 -5
  386. package/dist/runtime/api/thread-runtime.js.map +1 -1
  387. package/dist/runtime/base/base-assistant-runtime-core.d.ts +1 -2
  388. package/dist/runtime/base/base-assistant-runtime-core.d.ts.map +1 -1
  389. package/dist/runtime/base/base-composer-runtime-core.d.ts +2 -2
  390. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  391. package/dist/runtime/base/base-composer-runtime-core.js +46 -10
  392. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  393. package/dist/runtime/base/base-thread-runtime-core.d.ts +10 -3
  394. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  395. package/dist/runtime/base/base-thread-runtime-core.js +212 -77
  396. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  397. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +1 -2
  398. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  399. package/dist/runtime/base/default-edit-composer-runtime-core.js +12 -2
  400. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  401. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -2
  402. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  403. package/dist/runtime/base/default-thread-composer-runtime-core.js +7 -1
  404. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  405. package/dist/runtime/branch/external-thread-branch-adapter.d.ts +1 -2
  406. package/dist/runtime/branch/external-thread-branch-adapter.d.ts.map +1 -1
  407. package/dist/runtime/interfaces/assistant-runtime-core.d.ts +1 -2
  408. package/dist/runtime/interfaces/assistant-runtime-core.d.ts.map +1 -1
  409. package/dist/runtime/interfaces/composer-runtime-core.d.ts +10 -11
  410. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  411. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -5
  412. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  413. package/dist/runtime/interfaces/thread-runtime-core.d.ts +23 -16
  414. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  415. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +2 -3
  416. package/dist/runtime/queue/external-thread-queue-adapter.d.ts.map +1 -1
  417. package/dist/runtime/queue/message-queue.d.ts +10 -4
  418. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  419. package/dist/runtime/queue/message-queue.js +70 -7
  420. package/dist/runtime/queue/message-queue.js.map +1 -1
  421. package/dist/runtime/queue/queue-item.d.ts +2 -3
  422. package/dist/runtime/queue/queue-item.d.ts.map +1 -1
  423. package/dist/runtime/utils/abortable-thread-load.d.ts +2 -3
  424. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -1
  425. package/dist/runtime/utils/attachment-add-operations.d.ts +3 -4
  426. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -1
  427. package/dist/runtime/utils/auto-status.d.ts +9 -6
  428. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  429. package/dist/runtime/utils/auto-status.js +12 -4
  430. package/dist/runtime/utils/auto-status.js.map +1 -1
  431. package/dist/runtime/utils/chat-model-adapter.d.ts +5 -6
  432. package/dist/runtime/utils/chat-model-adapter.d.ts.map +1 -1
  433. package/dist/runtime/utils/external-message-conversion.d.ts +26 -15
  434. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  435. package/dist/runtime/utils/external-message-conversion.js +51 -14
  436. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  437. package/dist/runtime/utils/external-store-message.d.ts +4 -5
  438. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  439. package/dist/runtime/utils/message-repository-session.d.ts +3 -4
  440. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -1
  441. package/dist/runtime/utils/message-repository.d.ts +4 -5
  442. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  443. package/dist/runtime/utils/message-repository.js +3 -3
  444. package/dist/runtime/utils/message-repository.js.map +1 -1
  445. package/dist/runtime/utils/pending-tool-calls.d.ts +2 -3
  446. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -1
  447. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts +1 -2
  448. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts.map +1 -1
  449. package/dist/runtime/utils/runtime-extras-brand.d.ts +2 -3
  450. package/dist/runtime/utils/runtime-extras-brand.d.ts.map +1 -1
  451. package/dist/runtime/utils/stored-message-parts.d.ts +21 -0
  452. package/dist/runtime/utils/stored-message-parts.d.ts.map +1 -0
  453. package/dist/runtime/utils/stored-message-parts.js +83 -0
  454. package/dist/runtime/utils/stored-message-parts.js.map +1 -0
  455. package/dist/runtime/utils/streaming-timing.d.ts +4 -5
  456. package/dist/runtime/utils/streaming-timing.d.ts.map +1 -1
  457. package/dist/runtime/utils/thread-message-like.d.ts +7 -6
  458. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  459. package/dist/runtime/utils/thread-message-like.js +4 -2
  460. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  461. package/dist/runtime/utils/thread-runtime-lifecycle.d.ts +3 -4
  462. package/dist/runtime/utils/thread-runtime-lifecycle.d.ts.map +1 -1
  463. package/dist/runtime/utils/tool-call-tree.d.ts +4 -5
  464. package/dist/runtime/utils/tool-call-tree.d.ts.map +1 -1
  465. package/dist/runtimes/assistant-transport/utils.d.ts +1 -2
  466. package/dist/runtimes/assistant-transport/utils.d.ts.map +1 -1
  467. package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -6
  468. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  469. package/dist/runtimes/external-store/external-store-runtime-core.d.ts +1 -2
  470. package/dist/runtimes/external-store/external-store-runtime-core.d.ts.map +1 -1
  471. package/dist/runtimes/external-store/external-store-shared-options.d.ts +2 -3
  472. package/dist/runtimes/external-store/external-store-shared-options.d.ts.map +1 -1
  473. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -3
  474. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  475. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +4 -3
  476. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  477. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +36 -6
  478. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  479. package/dist/runtimes/external-store/thread-message-converter.d.ts +2 -3
  480. package/dist/runtimes/external-store/thread-message-converter.d.ts.map +1 -1
  481. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +2 -3
  482. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -1
  483. package/dist/runtimes/internal.d.ts +1 -1
  484. package/dist/runtimes/local/local-runtime-core.d.ts +1 -2
  485. package/dist/runtimes/local/local-runtime-core.d.ts.map +1 -1
  486. package/dist/runtimes/local/local-runtime-options.d.ts +1 -2
  487. package/dist/runtimes/local/local-runtime-options.d.ts.map +1 -1
  488. package/dist/runtimes/local/local-thread-list-runtime-core.d.ts +2 -3
  489. package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
  490. package/dist/runtimes/local/local-thread-runtime-core.d.ts +10 -5
  491. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  492. package/dist/runtimes/local/local-thread-runtime-core.js +57 -10
  493. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  494. package/dist/runtimes/local/should-continue.d.ts +1 -2
  495. package/dist/runtimes/local/should-continue.d.ts.map +1 -1
  496. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -2
  497. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  498. package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts +1 -2
  499. package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts.map +1 -1
  500. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +1 -2
  501. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -1
  502. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts +1 -2
  503. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  504. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +1 -2
  505. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  506. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +16 -17
  507. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  508. package/dist/runtimes/remote-thread-list/title-generation.d.ts +7 -8
  509. package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -1
  510. package/dist/runtimes/remote-thread-list/title.d.ts +2 -3
  511. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
  512. package/dist/runtimes/remote-thread-list/types.d.ts +8 -9
  513. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  514. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +4 -5
  515. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  516. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
  517. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  518. package/dist/store/clients/chain-of-thought-client.d.ts +1 -2
  519. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  520. package/dist/store/clients/external-thread.d.ts +3 -4
  521. package/dist/store/clients/external-thread.d.ts.map +1 -1
  522. package/dist/store/clients/external-thread.js +256 -211
  523. package/dist/store/clients/external-thread.js.map +1 -1
  524. package/dist/store/clients/model-context-client.d.ts +1 -2
  525. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  526. package/dist/store/clients/no-op-composer-client.d.ts +1 -2
  527. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  528. package/dist/store/clients/runtime-adapter.d.ts +1 -2
  529. package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
  530. package/dist/store/clients/single-thread-list.d.ts +1 -2
  531. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  532. package/dist/store/clients/suggestions.d.ts +3 -4
  533. package/dist/store/clients/suggestions.d.ts.map +1 -1
  534. package/dist/store/clients/thread-message-client.d.ts +2 -3
  535. package/dist/store/clients/thread-message-client.d.ts.map +1 -1
  536. package/dist/store/clients/thread-selection-events.d.ts +1 -2
  537. package/dist/store/clients/thread-selection-events.d.ts.map +1 -1
  538. package/dist/store/clients/thread-tasks.d.ts +12 -0
  539. package/dist/store/clients/thread-tasks.d.ts.map +1 -0
  540. package/dist/store/clients/thread-tasks.js +69 -0
  541. package/dist/store/clients/thread-tasks.js.map +1 -0
  542. package/dist/store/env.d.ts +1 -2
  543. package/dist/store/env.d.ts.map +1 -1
  544. package/dist/store/index.d.ts +3 -2
  545. package/dist/store/index.js +3 -3
  546. package/dist/store/primitive-predicates.d.ts +11 -12
  547. package/dist/store/primitive-predicates.d.ts.map +1 -1
  548. package/dist/store/primitive-predicates.js +4 -4
  549. package/dist/store/primitive-predicates.js.map +1 -1
  550. package/dist/store/runtime-clients/attachment-runtime-client.d.ts +1 -2
  551. package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
  552. package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -2
  553. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  554. package/dist/store/runtime-clients/handle-runtime-action.d.ts +1 -2
  555. package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
  556. package/dist/store/runtime-clients/handle-thread-list-action.d.ts +1 -2
  557. package/dist/store/runtime-clients/handle-thread-list-action.d.ts.map +1 -1
  558. package/dist/store/runtime-clients/liveRef.d.ts +1 -2
  559. package/dist/store/runtime-clients/liveRef.d.ts.map +1 -1
  560. package/dist/store/runtime-clients/message-part-runtime-client.d.ts +1 -2
  561. package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
  562. package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -2
  563. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  564. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -2
  565. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  566. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts +1 -2
  567. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  568. package/dist/store/runtime-clients/thread-runtime-client.d.ts +1 -2
  569. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  570. package/dist/store/runtime-clients/thread-runtime-client.js +176 -128
  571. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  572. package/dist/store/runtime-clients/useSubscribable.d.ts +1 -2
  573. package/dist/store/runtime-clients/useSubscribable.d.ts.map +1 -1
  574. package/dist/store/scope-registration.d.ts +2 -0
  575. package/dist/store/scope-registration.d.ts.map +1 -1
  576. package/dist/store/scopes/attachment.d.ts +4 -5
  577. package/dist/store/scopes/attachment.d.ts.map +1 -1
  578. package/dist/store/scopes/chain-of-thought.d.ts +5 -6
  579. package/dist/store/scopes/chain-of-thought.d.ts.map +1 -1
  580. package/dist/store/scopes/composer.d.ts +6 -7
  581. package/dist/store/scopes/composer.d.ts.map +1 -1
  582. package/dist/store/scopes/message.d.ts +6 -6
  583. package/dist/store/scopes/message.d.ts.map +1 -1
  584. package/dist/store/scopes/model-context.d.ts +3 -4
  585. package/dist/store/scopes/model-context.d.ts.map +1 -1
  586. package/dist/store/scopes/part.d.ts +4 -5
  587. package/dist/store/scopes/part.d.ts.map +1 -1
  588. package/dist/store/scopes/queue-item.d.ts +4 -4
  589. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  590. package/dist/store/scopes/suggestion.d.ts +4 -5
  591. package/dist/store/scopes/suggestion.d.ts.map +1 -1
  592. package/dist/store/scopes/suggestions.d.ts +5 -6
  593. package/dist/store/scopes/suggestions.d.ts.map +1 -1
  594. package/dist/store/scopes/task.d.ts +35 -0
  595. package/dist/store/scopes/task.d.ts.map +1 -0
  596. package/dist/store/scopes/task.js +0 -0
  597. package/dist/store/scopes/thread-list-item.d.ts +5 -6
  598. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  599. package/dist/store/scopes/thread.d.ts +25 -6
  600. package/dist/store/scopes/thread.d.ts.map +1 -1
  601. package/dist/store/scopes/threads.d.ts +4 -5
  602. package/dist/store/scopes/threads.d.ts.map +1 -1
  603. package/dist/subscribable/subscribable.d.ts +15 -15
  604. package/dist/subscribable/subscribable.d.ts.map +1 -1
  605. package/dist/subscribable/subscribable.js +23 -15
  606. package/dist/subscribable/subscribable.js.map +1 -1
  607. package/dist/tests/remote-thread-list-test-helpers.d.ts +5 -6
  608. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  609. package/dist/types/attachment.d.ts +9 -10
  610. package/dist/types/attachment.d.ts.map +1 -1
  611. package/dist/types/augmentations.d.ts +3 -4
  612. package/dist/types/augmentations.d.ts.map +1 -1
  613. package/dist/types/directive.d.ts +2 -3
  614. package/dist/types/directive.d.ts.map +1 -1
  615. package/dist/types/error.d.ts +8 -9
  616. package/dist/types/error.d.ts.map +1 -1
  617. package/dist/types/index.d.ts +2 -2
  618. package/dist/types/message.d.ts +53 -44
  619. package/dist/types/message.d.ts.map +1 -1
  620. package/dist/types/message.js.map +1 -1
  621. package/dist/types/quote.d.ts +1 -2
  622. package/dist/types/quote.d.ts.map +1 -1
  623. package/dist/types/trigger.d.ts +2 -3
  624. package/dist/types/trigger.d.ts.map +1 -1
  625. package/dist/types/unsubscribe.d.ts +1 -2
  626. package/dist/types/unsubscribe.d.ts.map +1 -1
  627. package/dist/utils/abortable-iterable.d.ts +2 -3
  628. package/dist/utils/abortable-iterable.d.ts.map +1 -1
  629. package/dist/utils/abortable-iterable.js +30 -14
  630. package/dist/utils/abortable-iterable.js.map +1 -1
  631. package/dist/utils/composite-context-provider.d.ts +1 -2
  632. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  633. package/dist/utils/data-url.d.ts +6 -7
  634. package/dist/utils/data-url.d.ts.map +1 -1
  635. package/dist/utils/getGroupStatus.d.ts +17 -2
  636. package/dist/utils/getGroupStatus.d.ts.map +1 -1
  637. package/dist/utils/getGroupStatus.js +32 -7
  638. package/dist/utils/getGroupStatus.js.map +1 -1
  639. package/dist/utils/id.d.ts +3 -4
  640. package/dist/utils/id.d.ts.map +1 -1
  641. package/dist/utils/image-media-type.d.ts +1 -2
  642. package/dist/utils/image-media-type.d.ts.map +1 -1
  643. package/dist/utils/invoke-user-callback.d.ts +1 -2
  644. package/dist/utils/invoke-user-callback.d.ts.map +1 -1
  645. package/dist/utils/json/is-json-equal.d.ts +1 -2
  646. package/dist/utils/json/is-json-equal.d.ts.map +1 -1
  647. package/dist/utils/json/is-json.d.ts +4 -5
  648. package/dist/utils/json/is-json.d.ts.map +1 -1
  649. package/dist/utils/json/stable-stringify-tool-args.d.ts +2 -3
  650. package/dist/utils/json/stable-stringify-tool-args.d.ts.map +1 -1
  651. package/dist/utils/normalizePartStatus.d.ts +4 -5
  652. package/dist/utils/normalizePartStatus.d.ts.map +1 -1
  653. package/dist/utils/notify-event-listeners.d.ts +1 -2
  654. package/dist/utils/notify-event-listeners.d.ts.map +1 -1
  655. package/dist/utils/record.d.ts +1 -2
  656. package/dist/utils/record.d.ts.map +1 -1
  657. package/dist/utils/silent-runtime-action.d.ts +2 -3
  658. package/dist/utils/silent-runtime-action.d.ts.map +1 -1
  659. package/dist/utils/text.d.ts +1 -2
  660. package/dist/utils/text.d.ts.map +1 -1
  661. package/dist/utils/wire-media.d.ts +3 -4
  662. package/dist/utils/wire-media.d.ts.map +1 -1
  663. package/dist/utils/wire-media.js +2 -2
  664. package/dist/utils/wire-media.js.map +1 -1
  665. package/package.json +15 -14
  666. package/src/adapters/attachment.ts +8 -1
  667. package/src/adapters/feedback.ts +1 -0
  668. package/src/adapters/speech.test.ts +236 -0
  669. package/src/adapters/speech.ts +17 -10
  670. package/src/adapters/thread-history.ts +3 -0
  671. package/src/index.ts +1 -0
  672. package/src/model-context/frame/host.test.ts +113 -1
  673. package/src/model-context/frame/host.ts +39 -12
  674. package/src/model-context/frame/provider.test.ts +45 -20
  675. package/src/model-context/interactable-composer-metadata.test.ts +30 -0
  676. package/src/model-context/interactable-composer-metadata.ts +10 -10
  677. package/src/react/RuntimeAdapter.test.ts +4 -1
  678. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +361 -0
  679. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +69 -19
  680. package/src/react/client/DataRenderers.test.tsx +45 -0
  681. package/src/react/client/DataRenderers.ts +13 -9
  682. package/src/react/client/Interactables.test.ts +52 -4
  683. package/src/react/client/Interactables.ts +7 -1
  684. package/src/react/client/RemoteThreadList.concurrent.test.tsx +9 -3
  685. package/src/react/client/RemoteThreadList.test.ts +46 -7
  686. package/src/react/client/RemoteThreadList.ts +15 -2
  687. package/src/react/client/interactable-model-context.test.ts +44 -0
  688. package/src/react/client/interactable-model-context.ts +3 -2
  689. package/src/react/index.ts +5 -1
  690. package/src/react/interactables-legacy/Interactables.test.ts +9 -3
  691. package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +150 -0
  692. package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
  693. package/src/react/model-context/useInteractable.test.tsx +204 -0
  694. package/src/react/model-context/useInteractable.ts +3 -0
  695. package/src/react/primitive-hooks/useActionBarCopy.test.ts +5 -6
  696. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
  697. package/src/react/primitives/generativeUI/GenerativeUI.test.tsx +54 -0
  698. package/src/react/primitives/generativeUI/GenerativeUI.tsx +13 -18
  699. package/src/react/primitives/message/MessageGroupedParts.test.tsx +99 -0
  700. package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
  701. package/src/react/primitives/part/PartMessages.test.tsx +104 -1
  702. package/src/react/primitives/part/PartMessages.tsx +1 -0
  703. package/src/react/primitives/thread/ThreadMessages.test.tsx +160 -0
  704. package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
  705. package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
  706. package/src/react/runtimes/RemoteThreadListHookInstanceManager.layout.test.tsx +62 -0
  707. package/src/react/runtimes/RemoteThreadListHookInstanceManager.retry.test.tsx +2 -2
  708. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +23 -20
  709. package/src/react/runtimes/RemoteThreadListHookInstanceManager.title.test.tsx +24 -17
  710. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +39 -24
  711. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +7 -1
  712. package/src/react/runtimes/assistant-transport/transport-scheduling.test.ts +73 -46
  713. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +66 -1
  714. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
  715. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +323 -1
  716. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +190 -523
  717. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +81 -0
  718. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
  719. package/src/react/runtimes/cloud/auiV0.ts +281 -26
  720. package/src/react/runtimes/cloud/auiV0DecodeSafely.test.ts +522 -0
  721. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +21 -0
  722. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +7 -1
  723. package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
  724. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
  725. package/src/react/runtimes/external-message-converter.test.ts +143 -4
  726. package/src/react/runtimes/external-message-converter.ts +39 -93
  727. package/src/react/runtimes/remote-thread-hook-on-tap.test.ts +2 -1
  728. package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +8 -2
  729. package/src/react/runtimes/useExternalStoreRuntime.suggestions.test.tsx +6 -2
  730. package/src/react/runtimes/useRemoteThreadListRuntime.concurrent.test.tsx +84 -4
  731. package/src/react/runtimes/useRemoteThreadListRuntime.ts +6 -2
  732. package/src/react/utils/groupParts.ts +12 -1
  733. package/src/react/utils/useJSONEqual.ts +9 -0
  734. package/src/react/utils/useJSONSchemaDependency.ts +22 -0
  735. package/src/runtime/api/message-part-runtime.ts +4 -1
  736. package/src/runtime/api/message-runtime.ts +15 -2
  737. package/src/runtime/api/thread-runtime.test.ts +58 -0
  738. package/src/runtime/api/thread-runtime.ts +2 -4
  739. package/src/runtime/base/base-composer-runtime-core.ts +69 -16
  740. package/src/runtime/base/base-thread-runtime-core.test.ts +1799 -49
  741. package/src/runtime/base/base-thread-runtime-core.ts +296 -94
  742. package/src/runtime/base/default-edit-composer-runtime-core.ts +12 -2
  743. package/src/runtime/base/default-thread-composer-runtime-core.ts +12 -1
  744. package/src/runtime/interfaces/thread-runtime-core.ts +8 -0
  745. package/src/runtime/queue/message-queue.ts +79 -7
  746. package/src/runtime/utils/auto-status.test.ts +112 -1
  747. package/src/runtime/utils/auto-status.ts +31 -5
  748. package/src/runtime/utils/external-message-conversion.test.ts +177 -0
  749. package/src/runtime/utils/external-message-conversion.ts +128 -33
  750. package/src/runtime/utils/message-repository.test.ts +49 -0
  751. package/src/runtime/utils/message-repository.ts +3 -3
  752. package/src/runtime/utils/stored-message-parts.ts +174 -0
  753. package/src/runtime/utils/thread-message-like.test.ts +52 -0
  754. package/src/runtime/utils/thread-message-like.ts +11 -2
  755. package/src/runtimes/external-store/external-store-adapter.ts +4 -0
  756. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +74 -9
  757. package/src/runtimes/local/local-thread-runtime-core.test.ts +189 -12
  758. package/src/runtimes/local/local-thread-runtime-core.ts +91 -10
  759. package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
  760. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
  761. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
  762. package/src/store/clients/external-thread.ts +53 -14
  763. package/src/store/clients/thread-tasks.test.ts +274 -0
  764. package/src/store/clients/thread-tasks.ts +138 -0
  765. package/src/store/index.ts +8 -0
  766. package/src/store/primitive-predicates.test.ts +64 -5
  767. package/src/store/primitive-predicates.ts +10 -2
  768. package/src/store/runtime-clients/thread-runtime-client.test.tsx +110 -0
  769. package/src/store/runtime-clients/thread-runtime-client.ts +31 -1
  770. package/src/store/scope-registration.ts +2 -0
  771. package/src/store/scopes/message.ts +4 -1
  772. package/src/store/scopes/task.ts +34 -0
  773. package/src/store/scopes/thread.ts +16 -0
  774. package/src/store/thread-tasks.test.tsx +209 -0
  775. package/src/subscribable/subscribable.test.ts +161 -1
  776. package/src/subscribable/subscribable.ts +25 -15
  777. package/src/tests/MessageRepository.test.ts +1 -5
  778. package/src/tests/attachment-adapters.test.ts +16 -1
  779. package/src/tests/auiV0Encode.test.ts +304 -0
  780. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +6 -6
  781. package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
  782. package/src/tests/base-composer-runtime-core.test.ts +329 -6
  783. package/src/tests/composer-can-cancel.test.ts +35 -6
  784. package/src/tests/composer-can-send.test.ts +22 -0
  785. package/src/tests/default-edit-composer-runtime-core.test.ts +47 -0
  786. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +369 -16
  787. package/src/tests/external-store-thread-runtime-core.test.ts +31 -0
  788. package/src/tests/external-thread-attachments.test.tsx +91 -23
  789. package/src/tests/external-thread-feedback.test.tsx +20 -7
  790. package/src/tests/external-thread-parity.test.tsx +35 -1
  791. package/src/tests/groupParts.test.ts +71 -0
  792. package/src/tests/message-queue.test.ts +290 -2
  793. package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
  794. package/src/tests/thread-switch-events.test.tsx +11 -7
  795. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +45 -1
  796. package/src/types/index.ts +1 -0
  797. package/src/types/message.test.ts +19 -0
  798. package/src/types/message.ts +23 -7
  799. package/src/utils/abortable-iterable.test.ts +115 -0
  800. package/src/utils/abortable-iterable.ts +34 -23
  801. package/src/utils/getGroupStatus.test.ts +70 -4
  802. package/src/utils/getGroupStatus.ts +35 -12
  803. package/src/utils/wire-media.test.ts +10 -0
  804. package/src/utils/wire-media.ts +6 -2
@@ -1,7 +1,10 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import type { FeedbackAdapter } from "../../adapters/feedback";
3
+ import type { SpeechSynthesisAdapter } from "../../adapters/speech";
2
4
  import type { RealtimeVoiceAdapter } from "../../adapters/voice";
3
5
  import type { ModelContextProvider } from "../../model-context/types";
4
- import type { AppendMessage } from "../../types/message";
6
+ import type { AppendMessage, ThreadMessage } from "../../types/message";
7
+ import type { ChatModelRunResult } from "../../runtime/utils/chat-model-adapter";
5
8
  import { CompositeContextProvider } from "../../utils/composite-context-provider";
6
9
  import type {
7
10
  AddToolResultOptions,
@@ -13,9 +16,13 @@ import type {
13
16
  ThreadSuggestion,
14
17
  } from "../interfaces/thread-runtime-core";
15
18
  import { BaseThreadRuntimeCore } from "./base-thread-runtime-core";
19
+ import { LocalRuntimeCore } from "../../runtimes/local/local-runtime-core";
16
20
 
17
21
  const createVoiceAdapter = () => {
18
22
  let volumeCallback: ((volume: number) => void) | undefined;
23
+ let statusCallback:
24
+ | ((status: RealtimeVoiceAdapter.Status) => void)
25
+ | undefined;
19
26
  let transcriptCallback:
20
27
  | ((transcript: RealtimeVoiceAdapter.TranscriptItem) => void)
21
28
  | undefined;
@@ -25,7 +32,12 @@ const createVoiceAdapter = () => {
25
32
  disconnect: vi.fn(),
26
33
  mute: vi.fn(),
27
34
  unmute: vi.fn(),
28
- onStatusChange: () => () => {},
35
+ onStatusChange: (callback) => {
36
+ statusCallback = callback;
37
+ return () => {
38
+ statusCallback = undefined;
39
+ };
40
+ },
29
41
  onTranscript: (callback) => {
30
42
  transcriptCallback = callback;
31
43
  return () => {
@@ -44,12 +56,15 @@ const createVoiceAdapter = () => {
44
56
  return {
45
57
  adapter: { connect: () => session },
46
58
  emitVolume: (volume: number) => volumeCallback?.(volume),
59
+ emitStatus: (status: RealtimeVoiceAdapter.Status) =>
60
+ statusCallback?.(status),
47
61
  emitTranscript: (transcript: RealtimeVoiceAdapter.TranscriptItem) =>
48
62
  transcriptCallback?.(transcript),
49
63
  session,
50
64
  } satisfies {
51
65
  adapter: RealtimeVoiceAdapter;
52
66
  emitVolume: (volume: number) => void;
67
+ emitStatus: (status: RealtimeVoiceAdapter.Status) => void;
53
68
  emitTranscript: (transcript: RealtimeVoiceAdapter.TranscriptItem) => void;
54
69
  session: RealtimeVoiceAdapter.Session;
55
70
  };
@@ -57,17 +72,26 @@ const createVoiceAdapter = () => {
57
72
 
58
73
  class TestRuntime extends BaseThreadRuntimeCore {
59
74
  private readonly voiceAdapter: ReturnType<typeof createVoiceAdapter>;
75
+ private readonly actionAdapters: {
76
+ speech?: SpeechSynthesisAdapter;
77
+ feedback?: FeedbackAdapter;
78
+ };
60
79
 
61
80
  constructor(
62
81
  voiceAdapter: ReturnType<typeof createVoiceAdapter>,
63
82
  contextProvider: ModelContextProvider = { getModelContext: () => ({}) },
83
+ actionAdapters: {
84
+ speech?: SpeechSynthesisAdapter;
85
+ feedback?: FeedbackAdapter;
86
+ } = {},
64
87
  ) {
65
88
  super(contextProvider);
66
89
  this.voiceAdapter = voiceAdapter;
90
+ this.actionAdapters = actionAdapters;
67
91
  }
68
92
 
69
93
  get adapters() {
70
- return { voice: this.voiceAdapter.adapter };
94
+ return { voice: this.voiceAdapter.adapter, ...this.actionAdapters };
71
95
  }
72
96
 
73
97
  get isDisabled() {
@@ -128,6 +152,476 @@ afterEach(() => {
128
152
  vi.restoreAllMocks();
129
153
  });
130
154
 
155
+ describe("BaseThreadRuntimeCore speech lifecycle", () => {
156
+ const createUtterance = () => {
157
+ let notify = () => {};
158
+ const unsubscribe = vi.fn();
159
+ const utterance: SpeechSynthesisAdapter.Utterance = {
160
+ status: { type: "running" },
161
+ cancel: vi.fn(),
162
+ subscribe: (callback) => {
163
+ notify = callback;
164
+ return unsubscribe;
165
+ },
166
+ };
167
+ return { utterance, unsubscribe, notify: () => notify() };
168
+ };
169
+
170
+ const createThread = (speech: SpeechSynthesisAdapter) => {
171
+ const runtime = new LocalRuntimeCore(
172
+ {
173
+ adapters: {
174
+ chatModel: {
175
+ async run() {
176
+ return {};
177
+ },
178
+ },
179
+ speech,
180
+ },
181
+ },
182
+ undefined,
183
+ );
184
+ const thread = runtime.threads.getMainThreadRuntimeCore();
185
+ thread.reset([
186
+ {
187
+ id: "first",
188
+ role: "assistant",
189
+ content: [{ type: "text", text: "Hello" }],
190
+ },
191
+ {
192
+ id: "second",
193
+ role: "assistant",
194
+ content: [{ type: "text", text: "Again" }],
195
+ },
196
+ ]);
197
+ return thread;
198
+ };
199
+
200
+ it("releases a session that completes during subscription", () => {
201
+ const { utterance, unsubscribe } = createUtterance();
202
+ utterance.subscribe = (callback) => {
203
+ utterance.status = { type: "ended", reason: "finished" };
204
+ callback();
205
+ return unsubscribe;
206
+ };
207
+ const thread = createThread({ speak: () => utterance });
208
+
209
+ thread.speak("first");
210
+
211
+ expect(thread.speech).toBeUndefined();
212
+ expect(unsubscribe).toHaveBeenCalledOnce();
213
+ expect(utterance.cancel).not.toHaveBeenCalled();
214
+ expect(() => thread.stopSpeaking()).toThrow("No message is being spoken");
215
+ });
216
+
217
+ it("does not install an already-ended utterance", () => {
218
+ const { utterance, unsubscribe } = createUtterance();
219
+ utterance.status = { type: "ended", reason: "finished" };
220
+ const thread = createThread({ speak: () => utterance });
221
+
222
+ thread.speak("first");
223
+
224
+ expect(thread.speech).toBeUndefined();
225
+ expect(unsubscribe).toHaveBeenCalledOnce();
226
+ });
227
+
228
+ it.each(["finished", "error", "cancelled"] as const)(
229
+ "releases the listener after %s completion",
230
+ (reason) => {
231
+ const { utterance, unsubscribe, notify } = createUtterance();
232
+ const thread = createThread({ speak: () => utterance });
233
+ thread.speak("first");
234
+ const subscriber = vi.fn();
235
+ thread.subscribe(subscriber);
236
+
237
+ utterance.status = { type: "ended", reason };
238
+ notify();
239
+ notify();
240
+
241
+ expect(thread.speech).toBeUndefined();
242
+ expect(unsubscribe).toHaveBeenCalledOnce();
243
+ expect(subscriber).toHaveBeenCalledOnce();
244
+ expect(utterance.cancel).not.toHaveBeenCalled();
245
+ },
246
+ );
247
+
248
+ it("ignores old running and ended callbacks after replacement", () => {
249
+ const first = createUtterance();
250
+ const second = createUtterance();
251
+ const speak = vi
252
+ .fn()
253
+ .mockReturnValueOnce(first.utterance)
254
+ .mockReturnValueOnce(second.utterance);
255
+ const thread = createThread({ speak });
256
+ thread.speak("first");
257
+ thread.speak("second");
258
+ const subscriber = vi.fn();
259
+ thread.subscribe(subscriber);
260
+
261
+ first.notify();
262
+ first.utterance.status = { type: "ended", reason: "cancelled" };
263
+ first.notify();
264
+
265
+ expect(thread.speech).toEqual({
266
+ messageId: "second",
267
+ status: { type: "running" },
268
+ });
269
+ expect(subscriber).not.toHaveBeenCalled();
270
+ expect(first.utterance.cancel).toHaveBeenCalledOnce();
271
+ expect(first.unsubscribe).toHaveBeenCalledOnce();
272
+ thread.stopSpeaking();
273
+ expect(second.utterance.cancel).toHaveBeenCalledOnce();
274
+ expect(second.unsubscribe).toHaveBeenCalledOnce();
275
+ });
276
+
277
+ it("clears state before synchronous cancellation callbacks", () => {
278
+ const { utterance, unsubscribe, notify } = createUtterance();
279
+ utterance.cancel = vi.fn(() => {
280
+ utterance.status = { type: "ended", reason: "cancelled" };
281
+ notify();
282
+ });
283
+ const thread = createThread({ speak: () => utterance });
284
+ thread.speak("first");
285
+ const subscriber = vi.fn();
286
+ thread.subscribe(subscriber);
287
+
288
+ thread.stopSpeaking();
289
+
290
+ expect(thread.speech).toBeUndefined();
291
+ expect(utterance.cancel).toHaveBeenCalledOnce();
292
+ expect(unsubscribe).toHaveBeenCalledOnce();
293
+ expect(subscriber).toHaveBeenCalledOnce();
294
+ });
295
+
296
+ it.each(["cancellation", "unsubscribe"] as const)(
297
+ "notifies stopped state when %s throws",
298
+ (failure) => {
299
+ const { utterance, unsubscribe } = createUtterance();
300
+ const error = new Error("cleanup failed");
301
+ const thread = createThread({ speak: () => utterance });
302
+ thread.speak("first");
303
+ let observedSpeech = thread.speech;
304
+ const subscriber = vi.fn(() => {
305
+ observedSpeech = thread.speech;
306
+ });
307
+ thread.subscribe(subscriber);
308
+ if (failure === "cancellation") {
309
+ utterance.cancel = vi.fn(() => {
310
+ throw error;
311
+ });
312
+ } else {
313
+ unsubscribe.mockImplementation(() => {
314
+ throw error;
315
+ });
316
+ }
317
+
318
+ expect(() => thread.stopSpeaking()).toThrow(error);
319
+
320
+ expect(observedSpeech).toBeUndefined();
321
+ expect(thread.speech).toBeUndefined();
322
+ expect(subscriber).toHaveBeenCalledOnce();
323
+ expect(unsubscribe).toHaveBeenCalledOnce();
324
+ expect(utterance.cancel).toHaveBeenCalledOnce();
325
+ expect(() => thread.stopSpeaking()).toThrow("No message is being spoken");
326
+ expect(subscriber).toHaveBeenCalledOnce();
327
+ },
328
+ );
329
+
330
+ it("reports stop and notification failures after notifying later subscribers", () => {
331
+ vi.spyOn(console, "error").mockImplementation(() => {});
332
+ const { utterance } = createUtterance();
333
+ const cleanupError = new Error("cleanup failed");
334
+ const notificationError = new Error("notification failed");
335
+ utterance.cancel = vi.fn(() => {
336
+ throw cleanupError;
337
+ });
338
+ const thread = createThread({ speak: () => utterance });
339
+ thread.speak("first");
340
+ thread.subscribe(() => {
341
+ throw notificationError;
342
+ });
343
+ const laterSubscriber = vi.fn();
344
+ thread.subscribe(laterSubscriber);
345
+
346
+ expect(() => thread.stopSpeaking()).toThrow(
347
+ expect.objectContaining({
348
+ errors: [cleanupError, notificationError],
349
+ }),
350
+ );
351
+
352
+ expect(thread.speech).toBeUndefined();
353
+ expect(laterSubscriber).toHaveBeenCalledOnce();
354
+ });
355
+
356
+ it("notifies completed state even when terminal unsubscribe throws", () => {
357
+ const { utterance, unsubscribe, notify } = createUtterance();
358
+ const error = new Error("unsubscribe failed");
359
+ unsubscribe.mockImplementation(() => {
360
+ throw error;
361
+ });
362
+ const thread = createThread({ speak: () => utterance });
363
+ thread.speak("first");
364
+ let observedSpeech = thread.speech;
365
+ const subscriber = vi.fn(() => {
366
+ observedSpeech = thread.speech;
367
+ });
368
+ thread.subscribe(subscriber);
369
+ utterance.status = { type: "ended", reason: "finished" };
370
+
371
+ expect(notify).toThrow(error);
372
+
373
+ expect(observedSpeech).toBeUndefined();
374
+ expect(subscriber).toHaveBeenCalledOnce();
375
+ expect(unsubscribe).toHaveBeenCalledOnce();
376
+ expect(utterance.cancel).not.toHaveBeenCalled();
377
+ expect(notify).not.toThrow();
378
+ expect(subscriber).toHaveBeenCalledOnce();
379
+ });
380
+
381
+ it("cancels playback if a subscriber throws during initial state publication", () => {
382
+ vi.spyOn(console, "error").mockImplementation(() => {});
383
+ const { utterance, unsubscribe } = createUtterance();
384
+ const error = new Error("subscriber failed");
385
+ const thread = createThread({ speak: () => utterance });
386
+ thread.subscribe(() => {
387
+ throw error;
388
+ });
389
+
390
+ expect(() => thread.speak("first")).toThrow(error);
391
+
392
+ expect(thread.speech).toBeUndefined();
393
+ expect(unsubscribe).toHaveBeenCalledOnce();
394
+ expect(utterance.cancel).toHaveBeenCalledOnce();
395
+ expect(() => thread.stopSpeaking()).toThrow("No message is being spoken");
396
+ });
397
+
398
+ it("cancels a session when subscribing throws", () => {
399
+ const { utterance } = createUtterance();
400
+ const error = new Error("subscription failed");
401
+ utterance.subscribe = () => {
402
+ throw error;
403
+ };
404
+ const thread = createThread({ speak: () => utterance });
405
+
406
+ expect(() => thread.speak("first")).toThrow(error);
407
+ expect(thread.speech).toBeUndefined();
408
+ expect(utterance.cancel).toHaveBeenCalledOnce();
409
+ expect(() => thread.stopSpeaking()).toThrow("No message is being spoken");
410
+ });
411
+
412
+ it("notifies observers when replacement subscription fails", () => {
413
+ const first = createUtterance();
414
+ const second = createUtterance();
415
+ const error = new Error("subscription failed");
416
+ second.utterance.subscribe = () => {
417
+ throw error;
418
+ };
419
+ const thread = createThread({
420
+ speak: vi
421
+ .fn()
422
+ .mockReturnValueOnce(first.utterance)
423
+ .mockReturnValueOnce(second.utterance),
424
+ });
425
+ thread.speak("first");
426
+ let observedSpeech = thread.speech;
427
+ const subscriber = vi.fn(() => {
428
+ observedSpeech = thread.speech;
429
+ });
430
+ thread.subscribe(subscriber);
431
+
432
+ expect(() => thread.speak("second")).toThrow(error);
433
+
434
+ expect(observedSpeech).toBeUndefined();
435
+ expect(subscriber).toHaveBeenCalledOnce();
436
+ expect(second.utterance.cancel).toHaveBeenCalledOnce();
437
+ });
438
+
439
+ it.each(["creation", "cancellation", "unsubscribe"] as const)(
440
+ "notifies observers when replacement fails during %s",
441
+ (failure) => {
442
+ const first = createUtterance();
443
+ const error = new Error("replacement failed");
444
+ const speak = vi
445
+ .fn()
446
+ .mockReturnValueOnce(first.utterance)
447
+ .mockImplementationOnce(() => {
448
+ throw error;
449
+ });
450
+ const thread = createThread({ speak });
451
+ thread.speak("first");
452
+ let observedSpeech = thread.speech;
453
+ const subscriber = vi.fn(() => {
454
+ observedSpeech = thread.speech;
455
+ });
456
+ thread.subscribe(subscriber);
457
+ if (failure === "cancellation") {
458
+ first.utterance.cancel = vi.fn(() => {
459
+ throw error;
460
+ });
461
+ } else if (failure === "unsubscribe") {
462
+ first.unsubscribe.mockImplementation(() => {
463
+ throw error;
464
+ });
465
+ }
466
+
467
+ expect(() => thread.speak("second")).toThrow(error);
468
+
469
+ expect(observedSpeech).toBeUndefined();
470
+ expect(thread.speech).toBeUndefined();
471
+ expect(subscriber).toHaveBeenCalledOnce();
472
+ expect(first.unsubscribe).toHaveBeenCalledOnce();
473
+ expect(first.utterance.cancel).toHaveBeenCalledOnce();
474
+ expect(speak).toHaveBeenCalledTimes(failure === "creation" ? 2 : 1);
475
+ },
476
+ );
477
+
478
+ it("preserves a creation error when rollback notification throws", () => {
479
+ const consoleError = vi
480
+ .spyOn(console, "error")
481
+ .mockImplementation(() => {});
482
+ const first = createUtterance();
483
+ const creationError = new Error("creation failed");
484
+ const notificationError = new Error("notification failed");
485
+ const thread = createThread({
486
+ speak: vi
487
+ .fn()
488
+ .mockReturnValueOnce(first.utterance)
489
+ .mockImplementationOnce(() => {
490
+ throw creationError;
491
+ }),
492
+ });
493
+ thread.speak("first");
494
+ thread.subscribe(() => {
495
+ throw notificationError;
496
+ });
497
+ const laterSubscriber = vi.fn();
498
+ thread.subscribe(laterSubscriber);
499
+
500
+ expect(() => thread.speak("second")).toThrow(creationError);
501
+
502
+ expect(thread.speech).toBeUndefined();
503
+ expect(laterSubscriber).toHaveBeenCalledOnce();
504
+ expect(consoleError).toHaveBeenCalledWith(
505
+ "[assistant-ui] Speech rollback notification threw",
506
+ notificationError,
507
+ );
508
+ });
509
+
510
+ it("does not notify when initial utterance creation fails", () => {
511
+ const error = new Error("creation failed");
512
+ const thread = createThread({
513
+ speak: () => {
514
+ throw error;
515
+ },
516
+ });
517
+ const subscriber = vi.fn();
518
+ thread.subscribe(subscriber);
519
+
520
+ expect(() => thread.speak("first")).toThrow(error);
521
+
522
+ expect(thread.speech).toBeUndefined();
523
+ expect(subscriber).not.toHaveBeenCalled();
524
+ });
525
+
526
+ it("preserves reentrant speech when the outer creation throws", () => {
527
+ const first = createUtterance();
528
+ const replacement = createUtterance();
529
+ const error = new Error("outer creation failed");
530
+ const speak = vi.fn().mockReturnValue(first.utterance);
531
+ const thread = createThread({ speak });
532
+ thread.speak("first");
533
+ speak
534
+ .mockImplementationOnce(() => {
535
+ thread.speak("second");
536
+ throw error;
537
+ })
538
+ .mockReturnValueOnce(replacement.utterance);
539
+ const subscriber = vi.fn();
540
+ thread.subscribe(subscriber);
541
+
542
+ expect(() => thread.speak("second")).toThrow(error);
543
+
544
+ expect(thread.speech?.messageId).toBe("second");
545
+ expect(replacement.utterance.cancel).not.toHaveBeenCalled();
546
+ expect(subscriber).toHaveBeenCalledOnce();
547
+ thread.stopSpeaking();
548
+ expect(replacement.unsubscribe).toHaveBeenCalledOnce();
549
+ });
550
+
551
+ it("preserves the setup error when rollback cancellation throws", () => {
552
+ const consoleError = vi
553
+ .spyOn(console, "error")
554
+ .mockImplementation(() => {});
555
+ const { utterance } = createUtterance();
556
+ const setupError = new Error("subscription failed");
557
+ const cleanupError = new Error("cancellation failed");
558
+ utterance.subscribe = () => {
559
+ throw setupError;
560
+ };
561
+ utterance.cancel = vi.fn(() => {
562
+ throw cleanupError;
563
+ });
564
+ const thread = createThread({ speak: () => utterance });
565
+ const subscriber = vi.fn();
566
+ thread.subscribe(subscriber);
567
+
568
+ expect(() => thread.speak("first")).toThrow(setupError);
569
+
570
+ expect(thread.speech).toBeUndefined();
571
+ expect(subscriber).toHaveBeenCalledOnce();
572
+ expect(consoleError).toHaveBeenCalledWith(
573
+ "[assistant-ui] Speech rollback cleanup threw",
574
+ cleanupError,
575
+ );
576
+ });
577
+
578
+ it("does not notify twice when subscription completes before throwing", () => {
579
+ const { utterance } = createUtterance();
580
+ const error = new Error("subscription failed");
581
+ utterance.subscribe = (callback) => {
582
+ utterance.status = { type: "ended", reason: "finished" };
583
+ callback();
584
+ throw error;
585
+ };
586
+ const thread = createThread({ speak: () => utterance });
587
+ const subscriber = vi.fn();
588
+ thread.subscribe(subscriber);
589
+
590
+ expect(() => thread.speak("first")).toThrow(error);
591
+
592
+ expect(thread.speech).toBeUndefined();
593
+ expect(subscriber).toHaveBeenCalledOnce();
594
+ expect(utterance.cancel).not.toHaveBeenCalled();
595
+ });
596
+
597
+ it("leaves a reentrant replacement active when the old subscription throws", () => {
598
+ const first = createUtterance();
599
+ const second = createUtterance();
600
+ const thread = createThread({
601
+ speak: vi
602
+ .fn()
603
+ .mockReturnValueOnce(first.utterance)
604
+ .mockReturnValueOnce(second.utterance),
605
+ });
606
+ const error = new Error("old subscription failed");
607
+ first.utterance.subscribe = () => {
608
+ thread.speak("second");
609
+ throw error;
610
+ };
611
+ const subscriber = vi.fn();
612
+ thread.subscribe(subscriber);
613
+
614
+ expect(() => thread.speak("first")).toThrow(error);
615
+
616
+ expect(thread.speech?.messageId).toBe("second");
617
+ expect(subscriber).toHaveBeenCalledOnce();
618
+ expect(first.utterance.cancel).toHaveBeenCalledOnce();
619
+ expect(second.utterance.cancel).not.toHaveBeenCalled();
620
+ thread.stopSpeaking();
621
+ expect(second.unsubscribe).toHaveBeenCalledOnce();
622
+ });
623
+ });
624
+
131
625
  describe("BaseThreadRuntimeCore subscriptions", () => {
132
626
  it("notifies later subscribers when an earlier subscriber throws", () => {
133
627
  const runtime = new TestRuntime(createVoiceAdapter());
@@ -231,84 +725,1340 @@ describe("BaseThreadRuntimeCore subscriptions", () => {
231
725
  });
232
726
 
233
727
  describe("BaseThreadRuntimeCore voice volume subscriptions", () => {
234
- it("continues notifying subscribers when one throws", () => {
235
- const consoleError = vi
236
- .spyOn(console, "error")
237
- .mockImplementation(() => {});
728
+ it("finishes disconnecting when a session cleanup throws", () => {
729
+ const cleanupError = new Error("cleanup failed");
730
+ const laterCleanup = vi.fn();
238
731
  const voice = createVoiceAdapter();
732
+ voice.session.onStatusChange = () => () => {
733
+ throw cleanupError;
734
+ };
735
+ voice.session.onModeChange = () => laterCleanup;
239
736
  const runtime = new TestRuntime(voice);
240
737
  runtime.connectVoice();
241
738
 
242
- const listenerError = new Error("listener failed");
243
- const laterListener = vi.fn();
244
- runtime.subscribeVoiceVolume(() => {
245
- throw listenerError;
246
- });
247
- runtime.subscribeVoiceVolume(laterListener);
739
+ expect(() => runtime.disconnectVoice()).toThrow(cleanupError);
740
+ expect(laterCleanup).toHaveBeenCalledOnce();
741
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
742
+ expect(runtime.voice).toBeUndefined();
743
+ expect(runtime.getVoiceVolume()).toBe(0);
248
744
 
249
- expect(() => voice.emitVolume(0.5)).not.toThrow();
250
- expect(laterListener).toHaveBeenCalledOnce();
251
- expect(runtime.getVoiceVolume()).toBe(0.5);
252
- expect(consoleError).toHaveBeenCalledWith(
253
- "[assistant-ui] Voice volume listener threw an error",
254
- listenerError,
255
- );
745
+ expect(() => runtime.disconnectVoice()).not.toThrow();
746
+ expect(laterCleanup).toHaveBeenCalledOnce();
747
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
256
748
  });
257
749
 
258
- it("continues disconnect cleanup when a subscriber throws", () => {
750
+ it("reconnects after cleanup from the previous session throws", () => {
259
751
  const consoleError = vi
260
752
  .spyOn(console, "error")
261
753
  .mockImplementation(() => {});
754
+ const cleanupError = new Error("cleanup failed");
262
755
  const voice = createVoiceAdapter();
756
+ voice.adapter.connect = vi.fn(voice.adapter.connect);
757
+ voice.session.onStatusChange = () => () => {
758
+ throw cleanupError;
759
+ };
263
760
  const runtime = new TestRuntime(voice);
264
761
  runtime.connectVoice();
265
762
 
266
- const listenerError = new Error("disconnect listener failed");
267
- const laterListener = vi.fn();
268
- runtime.subscribeVoiceVolume(() => {
763
+ expect(() => runtime.connectVoice()).not.toThrow();
764
+ expect(voice.adapter.connect).toHaveBeenCalledTimes(2);
765
+ expect(consoleError).toHaveBeenCalledWith(
766
+ "[assistant-ui] Voice cleanup threw before reconnect",
767
+ cleanupError,
768
+ );
769
+ });
770
+
771
+ it("rolls back a new session when initialization throws", () => {
772
+ vi.spyOn(console, "error").mockImplementation(() => {});
773
+ const voice = createVoiceAdapter();
774
+ const runtime = new TestRuntime(voice);
775
+ const listenerError = new Error("subscriber failed");
776
+ runtime.subscribe(() => {
269
777
  throw listenerError;
270
778
  });
271
- runtime.subscribeVoiceVolume(laterListener);
272
779
 
273
- expect(() => runtime.disconnectVoice()).not.toThrow();
274
- expect(laterListener).toHaveBeenCalledOnce();
780
+ expect(() => runtime.connectVoice()).toThrow(listenerError);
781
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
782
+ expect(runtime.voice).toBeUndefined();
275
783
  expect(runtime.getVoiceVolume()).toBe(0);
784
+ });
785
+
786
+ it("releases handlers registered before voice setup throws", () => {
787
+ const setupError = new Error("registration failed");
788
+ const statusCleanup = vi.fn();
789
+ const voice = createVoiceAdapter();
790
+ voice.session.onStatusChange = () => statusCleanup;
791
+ voice.session.onModeChange = () => {
792
+ throw setupError;
793
+ };
794
+ const runtime = new TestRuntime(voice);
795
+
796
+ expect(() => runtime.connectVoice()).toThrow(setupError);
797
+ expect(statusCleanup).toHaveBeenCalledOnce();
798
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
276
799
  expect(runtime.voice).toBeUndefined();
800
+ });
801
+
802
+ it("stops setup when a subscriber disconnects the new session", () => {
803
+ const voice = createVoiceAdapter();
804
+ const statusRegistration = vi.spyOn(voice.session, "onStatusChange");
805
+ const runtime = new TestRuntime(voice);
806
+ runtime.subscribe(() => {
807
+ if (runtime.voice) runtime.disconnectVoice();
808
+ });
809
+
810
+ expect(() => runtime.connectVoice()).not.toThrow();
811
+
277
812
  expect(voice.session.disconnect).toHaveBeenCalledOnce();
278
- expect(consoleError).toHaveBeenCalledWith(
279
- "[assistant-ui] Voice volume listener threw an error",
280
- listenerError,
281
- );
813
+ expect(statusRegistration).not.toHaveBeenCalled();
814
+ expect(runtime.voice).toBeUndefined();
282
815
  });
283
816
 
284
- it("reports rejected subscriber thenables", async () => {
817
+ it("releases a handler returned after reentrant disconnect", () => {
818
+ const statusCleanup = vi.fn();
819
+ const voice = createVoiceAdapter();
820
+ let registeringStatus = false;
821
+ voice.session.onStatusChange = (callback) => {
822
+ registeringStatus = true;
823
+ callback({ type: "running" });
824
+ registeringStatus = false;
825
+ return statusCleanup;
826
+ };
827
+ const modeRegistration = vi.spyOn(voice.session, "onModeChange");
828
+ const runtime = new TestRuntime(voice);
829
+ runtime.subscribe(() => {
830
+ if (registeringStatus) {
831
+ registeringStatus = false;
832
+ runtime.disconnectVoice();
833
+ }
834
+ });
835
+
836
+ expect(() => runtime.connectVoice()).not.toThrow();
837
+
838
+ expect(statusCleanup).toHaveBeenCalledOnce();
839
+ expect(modeRegistration).not.toHaveBeenCalled();
840
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
841
+ expect(runtime.voice).toBeUndefined();
842
+ });
843
+
844
+ it("does not release earlier handlers twice after reentrant disconnect", () => {
845
+ const statusCleanup = vi.fn();
846
+ const modeCleanup = vi.fn();
847
+ const voice = createVoiceAdapter();
848
+ voice.session.onStatusChange = () => statusCleanup;
849
+ let registeringMode = false;
850
+ voice.session.onModeChange = (callback) => {
851
+ registeringMode = true;
852
+ callback("speaking");
853
+ registeringMode = false;
854
+ return modeCleanup;
855
+ };
856
+ const volumeRegistration = vi.spyOn(voice.session, "onVolumeChange");
857
+ const runtime = new TestRuntime(voice);
858
+ runtime.subscribe(() => {
859
+ if (registeringMode) {
860
+ registeringMode = false;
861
+ runtime.disconnectVoice();
862
+ }
863
+ });
864
+
865
+ expect(() => runtime.connectVoice()).not.toThrow();
866
+
867
+ expect(statusCleanup).toHaveBeenCalledOnce();
868
+ expect(modeCleanup).toHaveBeenCalledOnce();
869
+ expect(volumeRegistration).not.toHaveBeenCalled();
870
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
871
+ expect(runtime.voice).toBeUndefined();
872
+ });
873
+
874
+ it("does not disconnect a replacement session after setup throws", () => {
875
+ const setupError = new Error("registration failed");
876
+ const firstVoice = createVoiceAdapter();
877
+ const replacementVoice = createVoiceAdapter();
878
+ firstVoice.adapter.connect = vi
879
+ .fn()
880
+ .mockReturnValueOnce(firstVoice.session)
881
+ .mockReturnValueOnce(replacementVoice.session);
882
+ let replacing = false;
883
+ firstVoice.session.onModeChange = (callback) => {
884
+ replacing = true;
885
+ callback("listening");
886
+ throw setupError;
887
+ };
888
+ const runtime = new TestRuntime(firstVoice);
889
+ runtime.subscribe(() => {
890
+ if (replacing) {
891
+ replacing = false;
892
+ runtime.disconnectVoice();
893
+ runtime.connectVoice();
894
+ }
895
+ });
896
+
897
+ expect(() => runtime.connectVoice()).toThrow(setupError);
898
+
899
+ expect(firstVoice.session.disconnect).toHaveBeenCalledOnce();
900
+ expect(replacementVoice.session.disconnect).not.toHaveBeenCalled();
901
+ expect(runtime.voice).toMatchObject({
902
+ status: replacementVoice.session.status,
903
+ isMuted: replacementVoice.session.isMuted,
904
+ mode: "listening",
905
+ });
906
+ });
907
+
908
+ it("releases setup handlers without disconnecting a self-ended session", () => {
909
+ const voice = createVoiceAdapter();
910
+ const statusCleanup = vi.fn();
911
+ voice.session.onStatusChange = (callback) => {
912
+ voice.session.status = { type: "ended", reason: "finished" };
913
+ callback(voice.session.status);
914
+ return statusCleanup;
915
+ };
916
+ const modeRegistration = vi.spyOn(voice.session, "onModeChange");
917
+ const runtime = new TestRuntime(voice);
918
+
919
+ runtime.connectVoice();
920
+
921
+ expect(statusCleanup).toHaveBeenCalledOnce();
922
+ expect(voice.session.disconnect).not.toHaveBeenCalled();
923
+ expect(modeRegistration).not.toHaveBeenCalled();
924
+ expect(runtime.voice).toBeUndefined();
925
+
926
+ runtime.disconnectVoice();
927
+ expect(statusCleanup).toHaveBeenCalledOnce();
928
+ expect(voice.session.disconnect).not.toHaveBeenCalled();
929
+ });
930
+
931
+ it("releases ended-session handlers when setup notification throws", () => {
932
+ const listenerError = new Error("ended notification failed");
933
+ const cleanupError = new Error("status cleanup failed");
285
934
  const consoleError = vi
286
935
  .spyOn(console, "error")
287
936
  .mockImplementation(() => {});
288
937
  const voice = createVoiceAdapter();
938
+ const statusCleanup = vi.fn(() => {
939
+ throw cleanupError;
940
+ });
941
+ let endSession!: () => void;
942
+ voice.session.onStatusChange = (callback) => {
943
+ endSession = () => {
944
+ voice.session.status = { type: "ended", reason: "finished" };
945
+ callback(voice.session.status);
946
+ };
947
+ return statusCleanup;
948
+ };
949
+ voice.session.onModeChange = () => {
950
+ endSession();
951
+ return () => {};
952
+ };
289
953
  const runtime = new TestRuntime(voice);
290
- runtime.connectVoice();
291
-
292
- const listenerError = new Error("async listener failed");
293
- const laterListener = vi.fn();
294
- runtime.subscribeVoiceVolume(async () => {
295
- throw listenerError;
954
+ let wasConnected = false;
955
+ runtime.subscribe(() => {
956
+ if (runtime.voice) wasConnected = true;
957
+ else if (wasConnected) throw listenerError;
296
958
  });
297
- runtime.subscribeVoiceVolume(laterListener);
298
959
 
299
- voice.emitVolume(0.5);
960
+ expect(() => runtime.connectVoice()).toThrow(listenerError);
300
961
 
301
- await vi.waitFor(() => {
302
- expect(laterListener).toHaveBeenCalledOnce();
303
- expect(consoleError).toHaveBeenCalledWith(
304
- "[assistant-ui] Voice volume listener threw an error",
305
- listenerError,
306
- );
307
- });
962
+ expect(statusCleanup).toHaveBeenCalledOnce();
963
+ expect(voice.session.disconnect).not.toHaveBeenCalled();
964
+ expect(runtime.voice).toBeUndefined();
965
+ expect(consoleError).toHaveBeenCalledWith(
966
+ "[assistant-ui] Detached voice setup cleanup threw",
967
+ cleanupError,
968
+ );
969
+ });
970
+
971
+ it("does not disconnect a session that ends after setup", () => {
972
+ const voice = createVoiceAdapter();
973
+ const statusCleanup = vi.fn();
974
+ let endSession!: () => void;
975
+ voice.session.onStatusChange = (callback) => {
976
+ endSession = () => {
977
+ voice.session.status = { type: "ended", reason: "finished" };
978
+ callback(voice.session.status);
979
+ };
980
+ return statusCleanup;
981
+ };
982
+ const runtime = new TestRuntime(voice);
983
+ runtime.connectVoice();
984
+ endSession();
985
+ expect(runtime.voice).toBeUndefined();
986
+
987
+ runtime.disconnectVoice();
988
+
989
+ expect(statusCleanup).toHaveBeenCalledOnce();
990
+ expect(voice.session.disconnect).not.toHaveBeenCalled();
991
+ });
992
+
993
+ it("rethrows one subscriber error once while disconnecting", () => {
994
+ const voice = createVoiceAdapter();
995
+ const runtime = new TestRuntime(voice);
996
+ runtime.connectVoice();
997
+ voice.emitTranscript({ role: "assistant", text: "Partial" });
998
+ const listenerError = new Error("subscriber failed");
999
+ runtime.subscribe(() => {
1000
+ throw listenerError;
1001
+ });
1002
+
1003
+ let thrown: unknown;
1004
+ try {
1005
+ runtime.disconnectVoice();
1006
+ } catch (error) {
1007
+ thrown = error;
1008
+ }
1009
+
1010
+ expect(thrown).toBe(listenerError);
1011
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
1012
+ });
1013
+
1014
+ it("continues notifying subscribers when one throws", () => {
1015
+ const consoleError = vi
1016
+ .spyOn(console, "error")
1017
+ .mockImplementation(() => {});
1018
+ const voice = createVoiceAdapter();
1019
+ const runtime = new TestRuntime(voice);
1020
+ runtime.connectVoice();
1021
+
1022
+ const listenerError = new Error("listener failed");
1023
+ const laterListener = vi.fn();
1024
+ runtime.subscribeVoiceVolume(() => {
1025
+ throw listenerError;
1026
+ });
1027
+ runtime.subscribeVoiceVolume(laterListener);
1028
+
1029
+ expect(() => voice.emitVolume(0.5)).not.toThrow();
1030
+ expect(laterListener).toHaveBeenCalledOnce();
1031
+ expect(runtime.getVoiceVolume()).toBe(0.5);
1032
+ expect(consoleError).toHaveBeenCalledWith(
1033
+ "[assistant-ui] Voice volume listener threw an error",
1034
+ listenerError,
1035
+ );
1036
+ });
1037
+
1038
+ it("continues disconnect cleanup when a subscriber throws", () => {
1039
+ const consoleError = vi
1040
+ .spyOn(console, "error")
1041
+ .mockImplementation(() => {});
1042
+ const voice = createVoiceAdapter();
1043
+ const runtime = new TestRuntime(voice);
1044
+ runtime.connectVoice();
1045
+
1046
+ const listenerError = new Error("disconnect listener failed");
1047
+ const laterListener = vi.fn();
1048
+ runtime.subscribeVoiceVolume(() => {
1049
+ throw listenerError;
1050
+ });
1051
+ runtime.subscribeVoiceVolume(laterListener);
1052
+
1053
+ expect(() => runtime.disconnectVoice()).not.toThrow();
1054
+ expect(laterListener).toHaveBeenCalledOnce();
1055
+ expect(runtime.getVoiceVolume()).toBe(0);
1056
+ expect(runtime.voice).toBeUndefined();
1057
+ expect(voice.session.disconnect).toHaveBeenCalledOnce();
1058
+ expect(consoleError).toHaveBeenCalledWith(
1059
+ "[assistant-ui] Voice volume listener threw an error",
1060
+ listenerError,
1061
+ );
1062
+ });
1063
+
1064
+ it("reports rejected subscriber thenables", async () => {
1065
+ const consoleError = vi
1066
+ .spyOn(console, "error")
1067
+ .mockImplementation(() => {});
1068
+ const voice = createVoiceAdapter();
1069
+ const runtime = new TestRuntime(voice);
1070
+ runtime.connectVoice();
1071
+
1072
+ const listenerError = new Error("async listener failed");
1073
+ const laterListener = vi.fn();
1074
+ runtime.subscribeVoiceVolume(async () => {
1075
+ throw listenerError;
1076
+ });
1077
+ runtime.subscribeVoiceVolume(laterListener);
1078
+
1079
+ voice.emitVolume(0.5);
1080
+
1081
+ await vi.waitFor(() => {
1082
+ expect(laterListener).toHaveBeenCalledOnce();
1083
+ expect(consoleError).toHaveBeenCalledWith(
1084
+ "[assistant-ui] Voice volume listener threw an error",
1085
+ listenerError,
1086
+ );
1087
+ });
1088
+ });
1089
+ });
1090
+
1091
+ describe("BaseThreadRuntimeCore voice transcripts", () => {
1092
+ const createLocalVoiceThread = async () => {
1093
+ const voiceAdapter = createVoiceAdapter();
1094
+ const history = {
1095
+ load: vi.fn(async () => ({ messages: [] })),
1096
+ append: vi.fn(async () => {}),
1097
+ };
1098
+ const runtime = new LocalRuntimeCore(
1099
+ {
1100
+ adapters: {
1101
+ chatModel: {
1102
+ async run() {
1103
+ return {};
1104
+ },
1105
+ },
1106
+ history,
1107
+ voice: voiceAdapter.adapter,
1108
+ },
1109
+ },
1110
+ undefined,
1111
+ );
1112
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1113
+ await thread.__internal_load();
1114
+ thread.connectVoice();
1115
+ return { history, thread, voiceAdapter };
1116
+ };
1117
+
1118
+ it("holds a queued send that becomes dispatchable while a voice session is connected", async () => {
1119
+ let resolveFirst!: (result: ChatModelRunResult) => void;
1120
+ const firstRun = new Promise<ChatModelRunResult>((resolve) => {
1121
+ resolveFirst = resolve;
1122
+ });
1123
+ let runCount = 0;
1124
+ const run = vi.fn(() =>
1125
+ ++runCount === 1 ? firstRun : Promise.resolve({}),
1126
+ );
1127
+ const voiceAdapter = createVoiceAdapter();
1128
+ const runtime = new LocalRuntimeCore(
1129
+ {
1130
+ unstable_enableMessageQueue: true,
1131
+ adapters: { chatModel: { run }, voice: voiceAdapter.adapter },
1132
+ },
1133
+ undefined,
1134
+ );
1135
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1136
+ const firstMessage: AppendMessage = {
1137
+ parentId: null,
1138
+ sourceId: null,
1139
+ runConfig: {},
1140
+ role: "user",
1141
+ content: [{ type: "text", text: "first" }],
1142
+ attachments: [],
1143
+ metadata: { custom: {} },
1144
+ createdAt: new Date(),
1145
+ };
1146
+
1147
+ await thread.append(firstMessage);
1148
+ expect(run).toHaveBeenCalledOnce();
1149
+ await thread.append({
1150
+ ...firstMessage,
1151
+ parentId: thread.messages.at(-1)?.id ?? null,
1152
+ content: [{ type: "text", text: "second" }],
1153
+ steer: false,
1154
+ });
1155
+
1156
+ const deferred: Array<() => void> = [];
1157
+ const originalQueueMicrotask = globalThis.queueMicrotask;
1158
+ globalThis.queueMicrotask = (callback: () => void) => {
1159
+ deferred.push(callback);
1160
+ };
1161
+ try {
1162
+ resolveFirst({});
1163
+ await new Promise((resolve) => setTimeout(resolve, 0));
1164
+ } finally {
1165
+ globalThis.queueMicrotask = originalQueueMicrotask;
1166
+ }
1167
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
1168
+
1169
+ thread.connectVoice();
1170
+ try {
1171
+ for (const callback of deferred) callback();
1172
+ await Promise.resolve();
1173
+ expect(run).toHaveBeenCalledOnce();
1174
+ expect(thread.getQueueItems()).toHaveLength(1);
1175
+ } finally {
1176
+ thread.disconnectVoice();
1177
+ }
1178
+ await Promise.resolve();
1179
+
1180
+ expect(run).toHaveBeenCalledTimes(2);
1181
+ expect(thread.getQueueItems()).toHaveLength(0);
1182
+ });
1183
+
1184
+ it("commits a final user transcript to the repository and history", async () => {
1185
+ const { history, thread, voiceAdapter } = await createLocalVoiceThread();
1186
+
1187
+ try {
1188
+ voiceAdapter.emitTranscript({
1189
+ role: "user",
1190
+ text: "Hello",
1191
+ isFinal: true,
1192
+ });
1193
+ const message = thread.messages[0]!;
1194
+
1195
+ expect(message.metadata.modality).toBe("voice");
1196
+ expect(history.append).toHaveBeenCalledExactlyOnceWith({
1197
+ parentId: null,
1198
+ message,
1199
+ });
1200
+ expect(thread.export().messages).toContainEqual({
1201
+ parentId: null,
1202
+ message,
1203
+ });
1204
+ } finally {
1205
+ thread.disconnectVoice();
1206
+ }
1207
+ });
1208
+
1209
+ it("commits an assistant transcript when it finalizes", async () => {
1210
+ const { history, thread, voiceAdapter } = await createLocalVoiceThread();
1211
+
1212
+ try {
1213
+ voiceAdapter.emitTranscript({ role: "assistant", text: "Partial" });
1214
+
1215
+ expect(thread.messages[0]?.status).toEqual({ type: "running" });
1216
+ expect(history.append).not.toHaveBeenCalled();
1217
+
1218
+ voiceAdapter.emitTranscript({
1219
+ role: "assistant",
1220
+ text: "Complete",
1221
+ isFinal: true,
1222
+ });
1223
+ const message = thread.messages[0]!;
1224
+
1225
+ expect(message.status).toEqual({ type: "complete", reason: "stop" });
1226
+ expect(history.append).toHaveBeenCalledExactlyOnceWith({
1227
+ parentId: null,
1228
+ message,
1229
+ });
1230
+ } finally {
1231
+ thread.disconnectVoice();
1232
+ }
1233
+ });
1234
+
1235
+ it("commits an in-flight assistant transcript when disconnecting", async () => {
1236
+ const { history, thread, voiceAdapter } = await createLocalVoiceThread();
1237
+
1238
+ voiceAdapter.emitTranscript({ role: "assistant", text: "Partial" });
1239
+ thread.disconnectVoice();
1240
+ const message = thread.messages[0]!;
1241
+
1242
+ expect(message.status).toEqual({ type: "complete", reason: "stop" });
1243
+ expect(history.append).toHaveBeenCalledExactlyOnceWith({
1244
+ parentId: null,
1245
+ message,
1246
+ });
1247
+ });
1248
+
1249
+ it("keeps committed transcripts after disconnect", async () => {
1250
+ const { thread, voiceAdapter } = await createLocalVoiceThread();
1251
+
1252
+ voiceAdapter.emitTranscript({
1253
+ role: "user",
1254
+ text: "Hello",
1255
+ isFinal: true,
1256
+ });
1257
+ const message = thread.messages[0]!;
1258
+
1259
+ thread.disconnectVoice();
1260
+
1261
+ expect(thread.messages).toEqual([message]);
1262
+ });
1263
+
1264
+ it("links the second transcript to the first", async () => {
1265
+ const { thread, voiceAdapter } = await createLocalVoiceThread();
1266
+
1267
+ try {
1268
+ voiceAdapter.emitTranscript({
1269
+ role: "user",
1270
+ text: "First",
1271
+ isFinal: true,
1272
+ });
1273
+ const first = thread.messages[0]!;
1274
+ voiceAdapter.emitTranscript({
1275
+ role: "user",
1276
+ text: "Second",
1277
+ isFinal: true,
1278
+ });
1279
+ const second = thread.messages[1]!;
1280
+
1281
+ expect(thread.export().messages).toContainEqual({
1282
+ parentId: first.id,
1283
+ message: second,
1284
+ });
1285
+ } finally {
1286
+ thread.disconnectVoice();
1287
+ }
1288
+ });
1289
+
1290
+ it("rejects a text send while a session is connected", async () => {
1291
+ const { thread, voiceAdapter } = await createLocalVoiceThread();
1292
+
1293
+ voiceAdapter.emitTranscript({
1294
+ role: "user",
1295
+ text: "Voice message",
1296
+ isFinal: true,
1297
+ });
1298
+ const transcript = thread.messages[0]!;
1299
+ const message: AppendMessage = {
1300
+ parentId: transcript.id,
1301
+ sourceId: null,
1302
+ role: "user",
1303
+ content: [{ type: "text", text: "Text message" }],
1304
+ attachments: [],
1305
+ metadata: { custom: {} },
1306
+ createdAt: new Date(),
1307
+ runConfig: {},
1308
+ startRun: false,
1309
+ };
1310
+
1311
+ await expect(thread.append(message)).rejects.toThrow(
1312
+ "Cannot send a text message while a voice session is connected",
1313
+ );
1314
+ expect(thread.export().messages).toHaveLength(1);
1315
+
1316
+ thread.disconnectVoice();
1317
+
1318
+ await expect(thread.append(message)).resolves.toBeUndefined();
1319
+ expect(thread.export().messages).toContainEqual({
1320
+ parentId: transcript.id,
1321
+ message: expect.objectContaining({ role: "user" }),
1322
+ });
1323
+ });
1324
+
1325
+ it("keeps the replacement session's hooks when a subscriber reconnects during disconnect", () => {
1326
+ class HookRuntime extends TestRuntime {
1327
+ connected = 0;
1328
+ disconnected = 0;
1329
+ protected override _onVoiceConnected() {
1330
+ this.connected += 1;
1331
+ }
1332
+ protected override _onVoiceDisconnected() {
1333
+ this.disconnected += 1;
1334
+ }
1335
+ }
1336
+ const runtime = new HookRuntime(createVoiceAdapter());
1337
+ runtime.connectVoice();
1338
+ expect(runtime.connected).toBe(1);
1339
+
1340
+ let reconnected = false;
1341
+ const unsubscribe = runtime.subscribe(() => {
1342
+ if (reconnected || runtime.voice !== undefined) return;
1343
+ reconnected = true;
1344
+ runtime.connectVoice();
1345
+ });
1346
+
1347
+ try {
1348
+ runtime.disconnectVoice();
1349
+ expect(runtime.voice).toBeDefined();
1350
+ expect(runtime.connected).toBe(2);
1351
+ expect(runtime.disconnected).toBe(0);
1352
+ } finally {
1353
+ unsubscribe();
1354
+ runtime.disconnectVoice();
1355
+ }
1356
+ expect(runtime.disconnected).toBe(1);
1357
+ });
1358
+
1359
+ it("keeps voice hooks held while replacing a session", () => {
1360
+ class HookRuntime extends TestRuntime {
1361
+ connected = 0;
1362
+ disconnected = 0;
1363
+ protected override _onVoiceConnected() {
1364
+ this.connected += 1;
1365
+ }
1366
+ protected override _onVoiceDisconnected() {
1367
+ this.disconnected += 1;
1368
+ }
1369
+ }
1370
+ const runtime = new HookRuntime(createVoiceAdapter());
1371
+
1372
+ runtime.connectVoice();
1373
+ runtime.connectVoice();
1374
+
1375
+ expect(runtime.connected).toBe(2);
1376
+ expect(runtime.disconnected).toBe(0);
1377
+
1378
+ runtime.disconnectVoice();
1379
+
1380
+ expect(runtime.disconnected).toBe(1);
1381
+ });
1382
+
1383
+ it("ignores an ended status from a replaced session", () => {
1384
+ class HookRuntime extends TestRuntime {
1385
+ disconnected = 0;
1386
+ protected override _onVoiceDisconnected() {
1387
+ this.disconnected += 1;
1388
+ }
1389
+ }
1390
+ const statusCallbacks: Array<
1391
+ (status: RealtimeVoiceAdapter.Status) => void
1392
+ > = [];
1393
+ const createSession = (): RealtimeVoiceAdapter.Session => ({
1394
+ status: { type: "running" },
1395
+ isMuted: false,
1396
+ disconnect: vi.fn(),
1397
+ mute: vi.fn(),
1398
+ unmute: vi.fn(),
1399
+ onStatusChange: (callback) => {
1400
+ statusCallbacks.push(callback);
1401
+ return () => {};
1402
+ },
1403
+ onTranscript: () => () => {},
1404
+ onModeChange: () => () => {},
1405
+ onVolumeChange: () => () => {},
1406
+ });
1407
+ const runtime = new HookRuntime({
1408
+ adapter: { connect: () => createSession() },
1409
+ emitVolume: () => {},
1410
+ emitStatus: () => {},
1411
+ emitTranscript: () => {},
1412
+ session: createSession(),
1413
+ });
1414
+
1415
+ runtime.connectVoice();
1416
+ runtime.connectVoice();
1417
+ statusCallbacks[0]!({ type: "ended", reason: "finished" });
1418
+
1419
+ try {
1420
+ expect(runtime.voice).toBeDefined();
1421
+ expect(runtime.disconnected).toBe(0);
1422
+ } finally {
1423
+ runtime.disconnectVoice();
1424
+ }
1425
+ expect(runtime.disconnected).toBe(1);
1426
+ });
1427
+
1428
+ it("releases voice hooks when a replacement cannot connect", () => {
1429
+ class HookRuntime extends TestRuntime {
1430
+ connected = 0;
1431
+ disconnected = 0;
1432
+ protected override _onVoiceConnected() {
1433
+ this.connected += 1;
1434
+ }
1435
+ protected override _onVoiceDisconnected() {
1436
+ this.disconnected += 1;
1437
+ }
1438
+ }
1439
+ const voice = createVoiceAdapter();
1440
+ const error = new Error("connection failed");
1441
+ voice.adapter.connect = vi
1442
+ .fn()
1443
+ .mockReturnValueOnce(voice.session)
1444
+ .mockImplementationOnce(() => {
1445
+ throw error;
1446
+ });
1447
+ const runtime = new HookRuntime(voice);
1448
+ runtime.connectVoice();
1449
+
1450
+ expect(() => runtime.connectVoice()).toThrow(error);
1451
+ expect(runtime.connected).toBe(1);
1452
+ expect(runtime.disconnected).toBe(1);
1453
+ });
1454
+
1455
+ it("rejects starting a voice session while a run is in progress", async () => {
1456
+ const voiceAdapter = createVoiceAdapter();
1457
+ let resolveRun!: (result: ChatModelRunResult) => void;
1458
+ const firstRun = new Promise<ChatModelRunResult>((resolve) => {
1459
+ resolveRun = resolve;
1460
+ });
1461
+ const run = vi.fn(() => firstRun);
1462
+ const runtime = new LocalRuntimeCore(
1463
+ { adapters: { chatModel: { run }, voice: voiceAdapter.adapter } },
1464
+ undefined,
1465
+ );
1466
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1467
+ const pending = thread.append({
1468
+ parentId: null,
1469
+ sourceId: null,
1470
+ role: "user",
1471
+ content: [{ type: "text", text: "hello" }],
1472
+ attachments: [],
1473
+ metadata: { custom: {} },
1474
+ createdAt: new Date(),
1475
+ runConfig: {},
1476
+ });
1477
+ await new Promise((resolve) => setTimeout(resolve, 0));
1478
+ expect(run).toHaveBeenCalledOnce();
1479
+
1480
+ expect(() => thread.connectVoice()).toThrow(
1481
+ "Cannot start a voice session while a run is in progress or paused on a pending tool action",
1482
+ );
1483
+ expect(thread.voice).toBeUndefined();
1484
+
1485
+ resolveRun({});
1486
+ await pending;
1487
+
1488
+ thread.connectVoice();
1489
+ try {
1490
+ expect(thread.voice).toBeDefined();
1491
+ } finally {
1492
+ thread.disconnectVoice();
1493
+ }
1494
+ });
1495
+
1496
+ it("rejects starting a voice session while a run is paused on a tool action", () => {
1497
+ const voiceAdapter = createVoiceAdapter();
1498
+ const runtime = new LocalRuntimeCore(
1499
+ {
1500
+ adapters: {
1501
+ chatModel: {
1502
+ async run() {
1503
+ return {};
1504
+ },
1505
+ },
1506
+ voice: voiceAdapter.adapter,
1507
+ },
1508
+ },
1509
+ undefined,
1510
+ );
1511
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1512
+ thread.reset([
1513
+ {
1514
+ id: "user",
1515
+ role: "user",
1516
+ content: [{ type: "text", text: "Use the tool" }],
1517
+ },
1518
+ {
1519
+ id: "assistant",
1520
+ role: "assistant",
1521
+ content: [
1522
+ {
1523
+ type: "tool-call",
1524
+ toolCallId: "tool-call",
1525
+ toolName: "tool",
1526
+ args: {},
1527
+ argsText: "{}",
1528
+ },
1529
+ ],
1530
+ status: { type: "requires-action", reason: "tool-calls" },
1531
+ },
1532
+ ]);
1533
+
1534
+ expect(() => thread.connectVoice()).toThrow(
1535
+ "Cannot start a voice session while a run is in progress or paused on a pending tool action",
1536
+ );
1537
+ expect(thread.voice).toBeUndefined();
1538
+ });
1539
+
1540
+ it("reloads committed transcripts from history into a fresh runtime", async () => {
1541
+ const items: Array<{ parentId: string | null; message: ThreadMessage }> =
1542
+ [];
1543
+ const history = {
1544
+ load: async () => ({
1545
+ messages: items,
1546
+ headId: items.at(-1)?.message.id ?? null,
1547
+ }),
1548
+ append: async (item: {
1549
+ parentId: string | null;
1550
+ message: ThreadMessage;
1551
+ }) => {
1552
+ items.push(item);
1553
+ },
1554
+ };
1555
+ const chatModel = {
1556
+ async run() {
1557
+ return {};
1558
+ },
1559
+ };
1560
+ const voiceAdapter = createVoiceAdapter();
1561
+ const first = new LocalRuntimeCore(
1562
+ { adapters: { chatModel, history, voice: voiceAdapter.adapter } },
1563
+ undefined,
1564
+ );
1565
+ const thread = first.threads.getMainThreadRuntimeCore();
1566
+ await new Promise((resolve) => setTimeout(resolve, 0));
1567
+ thread.connectVoice();
1568
+ voiceAdapter.emitTranscript({ role: "user", text: "Hi", isFinal: true });
1569
+ voiceAdapter.emitTranscript({
1570
+ role: "assistant",
1571
+ text: "Hello",
1572
+ isFinal: true,
1573
+ });
1574
+ thread.disconnectVoice();
1575
+ await new Promise((resolve) => setTimeout(resolve, 0));
1576
+ expect(items).toHaveLength(2);
1577
+
1578
+ const second = new LocalRuntimeCore(
1579
+ { adapters: { chatModel, history } },
1580
+ undefined,
1581
+ );
1582
+ const reloaded = second.threads.getMainThreadRuntimeCore();
1583
+ await reloaded.__internal_load();
1584
+
1585
+ expect(reloaded.messages.map((message) => message.role)).toEqual([
1586
+ "user",
1587
+ "assistant",
1588
+ ]);
1589
+ expect(reloaded.messages[1]?.metadata.modality).toBe("voice");
1590
+ });
1591
+
1592
+ it("rejects starting a voice session while a send awaits initialization", async () => {
1593
+ const voiceAdapter = createVoiceAdapter();
1594
+ const run = vi.fn(async () => ({}));
1595
+ const runtime = new LocalRuntimeCore(
1596
+ { adapters: { chatModel: { run }, voice: voiceAdapter.adapter } },
1597
+ undefined,
1598
+ );
1599
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1600
+ let release!: () => void;
1601
+ const barrier = new Promise<void>((resolve) => {
1602
+ release = resolve;
1603
+ });
1604
+ thread.__internal_setGetInitializePromise(() => barrier);
1605
+ const pending = thread.append({
1606
+ parentId: null,
1607
+ sourceId: null,
1608
+ role: "user",
1609
+ content: [{ type: "text", text: "hello" }],
1610
+ attachments: [],
1611
+ metadata: { custom: {} },
1612
+ createdAt: new Date(),
1613
+ runConfig: {},
1614
+ });
1615
+ await new Promise((resolve) => setTimeout(resolve, 0));
1616
+ expect(run).not.toHaveBeenCalled();
1617
+
1618
+ expect(() => thread.connectVoice()).toThrow(
1619
+ "Cannot start a voice session while a run is in progress or paused on a pending tool action",
1620
+ );
1621
+
1622
+ release();
1623
+ await pending;
1624
+ expect(run).toHaveBeenCalledOnce();
1625
+
1626
+ thread.connectVoice();
1627
+ try {
1628
+ expect(thread.voice).toBeDefined();
1629
+ } finally {
1630
+ thread.disconnectVoice();
1631
+ }
1632
+ });
1633
+
1634
+ it("rejects opening an edit while connected", async () => {
1635
+ const { thread, voiceAdapter } = await createLocalVoiceThread();
1636
+
1637
+ try {
1638
+ voiceAdapter.emitTranscript({
1639
+ role: "user",
1640
+ text: "Hello",
1641
+ isFinal: true,
1642
+ });
1643
+ const transcript = thread.messages[0]!;
1644
+
1645
+ expect(() => thread.beginEdit(transcript.id)).toThrow(
1646
+ "Cannot edit a message while a voice session is connected",
1647
+ );
1648
+
1649
+ thread.disconnectVoice();
1650
+
1651
+ expect(() => thread.beginEdit(transcript.id)).not.toThrow();
1652
+ } finally {
1653
+ thread.disconnectVoice();
1654
+ }
1655
+ });
1656
+
1657
+ it("speaks an assistant transcript", () => {
1658
+ const voiceAdapter = createVoiceAdapter();
1659
+ const speech = {
1660
+ speak: vi.fn(() => ({
1661
+ status: { type: "running" as const },
1662
+ cancel: vi.fn(),
1663
+ subscribe: () => () => {},
1664
+ })),
1665
+ } satisfies SpeechSynthesisAdapter;
1666
+ const runtime = new TestRuntime(voiceAdapter, undefined, { speech });
1667
+ runtime.connectVoice();
1668
+
1669
+ try {
1670
+ voiceAdapter.emitTranscript({
1671
+ role: "assistant",
1672
+ text: "Hello",
1673
+ isFinal: true,
1674
+ });
1675
+ const message = runtime.messages[0]!;
1676
+
1677
+ expect(() => runtime.speak(message.id)).not.toThrow();
1678
+ expect(speech.speak).toHaveBeenCalledExactlyOnceWith("Hello");
1679
+ } finally {
1680
+ runtime.disconnectVoice();
1681
+ }
1682
+ });
1683
+
1684
+ it("stops speaking a transcript when the session disconnects", () => {
1685
+ const voiceAdapter = createVoiceAdapter();
1686
+ const utterance = {
1687
+ status: { type: "running" as const },
1688
+ cancel: vi.fn(),
1689
+ subscribe: () => () => {},
1690
+ };
1691
+ const speech = {
1692
+ speak: vi.fn(() => utterance),
1693
+ } satisfies SpeechSynthesisAdapter;
1694
+ const runtime = new TestRuntime(voiceAdapter, undefined, { speech });
1695
+ runtime.connectVoice();
1696
+
1697
+ voiceAdapter.emitTranscript({
1698
+ role: "assistant",
1699
+ text: "Hello",
1700
+ isFinal: true,
1701
+ });
1702
+ runtime.speak(runtime.messages[0]!.id);
1703
+ expect(runtime.speech?.messageId).toBe(runtime.messages[0]!.id);
1704
+
1705
+ runtime.disconnectVoice();
1706
+
1707
+ expect(utterance.cancel).toHaveBeenCalledOnce();
1708
+ expect(runtime.speech).toBeUndefined();
1709
+ });
1710
+
1711
+ it("still disconnects the session when stopping a spoken transcript throws", () => {
1712
+ const voiceAdapter = createVoiceAdapter();
1713
+ const utterance = {
1714
+ status: { type: "running" as const },
1715
+ cancel: vi.fn(() => {
1716
+ throw new Error("cancel failed");
1717
+ }),
1718
+ subscribe: () => () => {},
1719
+ };
1720
+ const speech = {
1721
+ speak: vi.fn(() => utterance),
1722
+ } satisfies SpeechSynthesisAdapter;
1723
+ const runtime = new TestRuntime(voiceAdapter, undefined, { speech });
1724
+ runtime.connectVoice();
1725
+
1726
+ voiceAdapter.emitTranscript({
1727
+ role: "assistant",
1728
+ text: "Hello",
1729
+ isFinal: true,
1730
+ });
1731
+ runtime.speak(runtime.messages[0]!.id);
1732
+
1733
+ expect(() => runtime.disconnectVoice()).toThrow("cancel failed");
1734
+ expect(utterance.cancel).toHaveBeenCalledOnce();
1735
+ expect(voiceAdapter.session.disconnect).toHaveBeenCalledOnce();
1736
+ expect(runtime.messages).toHaveLength(0);
1737
+ expect(runtime.speech).toBeUndefined();
1738
+ expect(runtime.voice).toBeUndefined();
1739
+ });
1740
+
1741
+ it("submits feedback for a transcript", () => {
1742
+ const voiceAdapter = createVoiceAdapter();
1743
+ const feedback = { submit: vi.fn() } satisfies FeedbackAdapter;
1744
+ const runtime = new TestRuntime(voiceAdapter, undefined, { feedback });
1745
+ runtime.connectVoice();
1746
+
1747
+ try {
1748
+ voiceAdapter.emitTranscript({
1749
+ role: "assistant",
1750
+ text: "Hello",
1751
+ isFinal: true,
1752
+ });
1753
+ const message = runtime.messages[0]!;
1754
+
1755
+ expect(() =>
1756
+ runtime.submitFeedback({
1757
+ messageId: message.id,
1758
+ type: "positive",
1759
+ comment: "Helpful summary",
1760
+ }),
1761
+ ).not.toThrow();
1762
+ expect(feedback.submit).toHaveBeenCalledExactlyOnceWith({
1763
+ message,
1764
+ type: "positive",
1765
+ comment: "Helpful summary",
1766
+ });
1767
+ expect(runtime.messages[0]?.metadata.submittedFeedback).toEqual({
1768
+ type: "positive",
1769
+ comment: "Helpful summary",
1770
+ });
1771
+
1772
+ const rated = runtime.messages[0]!;
1773
+ runtime.submitFeedback({
1774
+ messageId: message.id,
1775
+ type: "negative",
1776
+ comment: " ",
1777
+ });
1778
+ expect(feedback.submit).toHaveBeenLastCalledWith({
1779
+ message: rated,
1780
+ type: "negative",
1781
+ });
1782
+ expect(runtime.messages[0]?.metadata.submittedFeedback).toEqual({
1783
+ type: "negative",
1784
+ });
1785
+
1786
+ const rerated = runtime.messages[0]!;
1787
+ runtime.submitFeedback({ messageId: message.id, type: "positive" });
1788
+ expect(feedback.submit).toHaveBeenLastCalledWith({
1789
+ message: rerated,
1790
+ type: "positive",
1791
+ });
1792
+ expect(runtime.messages[0]?.metadata.submittedFeedback).toEqual({
1793
+ type: "positive",
1794
+ });
1795
+ } finally {
1796
+ runtime.disconnectVoice();
1797
+ }
1798
+ });
1799
+
1800
+ it("marks transcript feedback locally without an adapter", () => {
1801
+ const voiceAdapter = createVoiceAdapter();
1802
+ const runtime = new TestRuntime(voiceAdapter);
1803
+ runtime.connectVoice();
1804
+
1805
+ try {
1806
+ voiceAdapter.emitTranscript({
1807
+ role: "assistant",
1808
+ text: "Hello",
1809
+ isFinal: true,
1810
+ });
1811
+ const message = runtime.messages[0]!;
1812
+
1813
+ expect(() =>
1814
+ runtime.submitFeedback({ messageId: message.id, type: "negative" }),
1815
+ ).not.toThrow();
1816
+ expect(runtime.messages[0]?.metadata.submittedFeedback).toEqual({
1817
+ type: "negative",
1818
+ });
1819
+ } finally {
1820
+ runtime.disconnectVoice();
1821
+ }
1822
+ });
1823
+
1824
+ it("blocks an edit while connected and accepts it once the session ends", async () => {
1825
+ const voiceAdapter = createVoiceAdapter();
1826
+ const run = vi.fn(async () => ({}));
1827
+ const runtime = new LocalRuntimeCore(
1828
+ { adapters: { chatModel: { run }, voice: voiceAdapter.adapter } },
1829
+ undefined,
1830
+ );
1831
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1832
+ thread.connectVoice();
1833
+
1834
+ try {
1835
+ voiceAdapter.emitTranscript({
1836
+ role: "assistant",
1837
+ text: "Hello",
1838
+ isFinal: true,
1839
+ });
1840
+ const edit = {
1841
+ parentId: null,
1842
+ sourceId: thread.messages[0]!.id,
1843
+ role: "user" as const,
1844
+ content: [{ type: "text" as const, text: "again" }],
1845
+ attachments: [],
1846
+ metadata: { custom: {} },
1847
+ createdAt: new Date(),
1848
+ runConfig: {},
1849
+ };
1850
+
1851
+ await expect(thread.append(edit)).rejects.toThrow(
1852
+ "Cannot send a text message while a voice session is connected",
1853
+ );
1854
+ await expect(thread.append({ ...edit, startRun: false })).rejects.toThrow(
1855
+ "Cannot send a text message while a voice session is connected",
1856
+ );
1857
+ expect(run).not.toHaveBeenCalled();
1858
+ expect(thread.messages).toHaveLength(1);
1859
+
1860
+ thread.disconnectVoice();
1861
+
1862
+ await thread.append(edit);
1863
+ expect(run).toHaveBeenCalledOnce();
1864
+ } finally {
1865
+ thread.disconnectVoice();
1866
+ }
1867
+ });
1868
+
1869
+ it("blocks a reload while connected and allows it once the session ends", async () => {
1870
+ const voiceAdapter = createVoiceAdapter();
1871
+ const run = vi.fn(async () => ({}));
1872
+ const runtime = new LocalRuntimeCore(
1873
+ { adapters: { chatModel: { run }, voice: voiceAdapter.adapter } },
1874
+ undefined,
1875
+ );
1876
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1877
+ thread.connectVoice();
1878
+
1879
+ try {
1880
+ voiceAdapter.emitTranscript({
1881
+ role: "assistant",
1882
+ text: "Hello",
1883
+ isFinal: true,
1884
+ });
1885
+ const message = thread.messages[0]!;
1886
+
1887
+ await expect(
1888
+ thread.startRun({
1889
+ parentId: null,
1890
+ sourceId: message.id,
1891
+ runConfig: {},
1892
+ }),
1893
+ ).rejects.toThrow(
1894
+ "Cannot start a run while a voice session is connected",
1895
+ );
1896
+ expect(run).not.toHaveBeenCalled();
1897
+
1898
+ thread.disconnectVoice();
1899
+
1900
+ await thread.startRun({
1901
+ parentId: null,
1902
+ sourceId: message.id,
1903
+ runConfig: {},
1904
+ });
1905
+ expect(run).toHaveBeenCalledOnce();
1906
+ } finally {
1907
+ thread.disconnectVoice();
1908
+ }
1909
+ });
1910
+
1911
+ it("blocks an approval response while connected before deciding it", () => {
1912
+ const voiceAdapter = createVoiceAdapter();
1913
+ const run = vi.fn(async () => ({}));
1914
+ const runtime = new LocalRuntimeCore(
1915
+ { adapters: { chatModel: { run }, voice: voiceAdapter.adapter } },
1916
+ undefined,
1917
+ );
1918
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1919
+ thread.connectVoice();
1920
+ thread.reset([
1921
+ {
1922
+ id: "user",
1923
+ role: "user",
1924
+ content: [{ type: "text", text: "Use the tool" }],
1925
+ },
1926
+ {
1927
+ id: "assistant",
1928
+ role: "assistant",
1929
+ content: [
1930
+ {
1931
+ type: "tool-call",
1932
+ toolCallId: "tool-call",
1933
+ toolName: "tool",
1934
+ args: {},
1935
+ argsText: "{}",
1936
+ approval: { id: "approval-1" },
1937
+ },
1938
+ ],
1939
+ status: { type: "requires-action", reason: "interrupt" },
1940
+ },
1941
+ ]);
1942
+
1943
+ try {
1944
+ expect(() =>
1945
+ thread.respondToToolApproval({
1946
+ approvalId: "approval-1",
1947
+ approved: true,
1948
+ }),
1949
+ ).toThrow(
1950
+ "Cannot respond to a tool approval while a voice session is connected",
1951
+ );
1952
+ const part = thread.messages[1]!.content[0]!;
1953
+ expect(
1954
+ part.type === "tool-call" && part.approval?.approved,
1955
+ ).toBeUndefined();
1956
+ expect(run).not.toHaveBeenCalled();
1957
+ } finally {
1958
+ thread.disconnectVoice();
1959
+ }
1960
+ });
1961
+
1962
+ it("blocks a tool result continuation while connected", () => {
1963
+ const voiceAdapter = createVoiceAdapter();
1964
+ const run = vi.fn(async () => ({}));
1965
+ const runtime = new LocalRuntimeCore(
1966
+ { adapters: { chatModel: { run }, voice: voiceAdapter.adapter } },
1967
+ undefined,
1968
+ );
1969
+ const thread = runtime.threads.getMainThreadRuntimeCore();
1970
+ const messageId = "assistant";
1971
+ const toolCallId = "tool-call";
1972
+ thread.connectVoice();
1973
+ thread.reset([
1974
+ {
1975
+ id: "user",
1976
+ role: "user",
1977
+ content: [{ type: "text", text: "Use the tool" }],
1978
+ },
1979
+ {
1980
+ id: messageId,
1981
+ role: "assistant",
1982
+ content: [
1983
+ {
1984
+ type: "tool-call",
1985
+ toolCallId,
1986
+ toolName: "tool",
1987
+ args: {},
1988
+ argsText: "{}",
1989
+ },
1990
+ ],
1991
+ status: { type: "requires-action", reason: "tool-calls" },
1992
+ },
1993
+ ]);
1994
+
1995
+ try {
1996
+ expect(() =>
1997
+ thread.addToolResult({
1998
+ messageId,
1999
+ toolCallId,
2000
+ toolName: "tool",
2001
+ result: "done",
2002
+ isError: false,
2003
+ }),
2004
+ ).toThrow("Cannot add a tool result while a voice session is connected");
2005
+ expect(run).not.toHaveBeenCalled();
2006
+ const part = thread.messages[1]!.content[0]!;
2007
+ expect(part).not.toHaveProperty("result");
2008
+ } finally {
2009
+ thread.disconnectVoice();
2010
+ }
2011
+ });
2012
+
2013
+ it("rejects editing a transcript", () => {
2014
+ const voiceAdapter = createVoiceAdapter();
2015
+ const runtime = new TestRuntime(voiceAdapter);
2016
+ runtime.connectVoice();
2017
+
2018
+ try {
2019
+ voiceAdapter.emitTranscript({
2020
+ role: "assistant",
2021
+ text: "Hello",
2022
+ isFinal: true,
2023
+ });
2024
+ voiceAdapter.emitStatus({ type: "ended", reason: "finished" });
2025
+
2026
+ expect(() => runtime.beginEdit(runtime.messages[0]!.id)).toThrow(
2027
+ "Voice transcript messages cannot be edited",
2028
+ );
2029
+ } finally {
2030
+ runtime.disconnectVoice();
2031
+ }
2032
+ });
2033
+
2034
+ it("marks user and assistant transcripts as voice messages", () => {
2035
+ const voiceAdapter = createVoiceAdapter();
2036
+ const runtime = new TestRuntime(voiceAdapter);
2037
+ runtime.connectVoice();
2038
+
2039
+ try {
2040
+ voiceAdapter.emitTranscript({
2041
+ role: "user",
2042
+ text: "Hello",
2043
+ isFinal: true,
2044
+ });
2045
+ voiceAdapter.emitTranscript({ role: "assistant", text: "Hi" });
2046
+
2047
+ expect(runtime.messages[0]?.metadata.modality).toBe("voice");
2048
+ expect(runtime.messages[1]?.metadata.modality).toBe("voice");
2049
+
2050
+ voiceAdapter.emitTranscript({
2051
+ role: "assistant",
2052
+ text: "Hello there",
2053
+ isFinal: true,
2054
+ });
2055
+
2056
+ expect(runtime.messages[1]?.metadata.modality).toBe("voice");
2057
+ } finally {
2058
+ runtime.disconnectVoice();
2059
+ }
308
2060
  });
309
- });
310
2061
 
311
- describe("BaseThreadRuntimeCore voice transcripts", () => {
312
2062
  it("completes a final-only reply before the next streamed reply", () => {
313
2063
  const voiceAdapter = createVoiceAdapter();
314
2064
  const runtime = new TestRuntime(voiceAdapter);