@assistant-ui/core 0.3.16 → 0.3.18

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 (474) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/attachment.js +5 -4
  3. package/dist/adapters/attachment.js.map +1 -1
  4. package/dist/index.d.ts +3 -3
  5. package/dist/index.js +2 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/internal.d.ts +4 -2
  8. package/dist/internal.js +4 -2
  9. package/dist/model-context/frame/host.d.ts.map +1 -1
  10. package/dist/model-context/frame/host.js +10 -4
  11. package/dist/model-context/frame/host.js.map +1 -1
  12. package/dist/model-context/frame/provider.d.ts +5 -0
  13. package/dist/model-context/frame/provider.d.ts.map +1 -1
  14. package/dist/model-context/frame/provider.js +101 -28
  15. package/dist/model-context/frame/provider.js.map +1 -1
  16. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
  17. package/dist/model-context/interactable-composer-metadata.js +16 -9
  18. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  19. package/dist/model-context/registry.d.ts.map +1 -1
  20. package/dist/model-context/registry.js +3 -5
  21. package/dist/model-context/registry.js.map +1 -1
  22. package/dist/model-context/tool-call-text.d.ts +26 -0
  23. package/dist/model-context/tool-call-text.d.ts.map +1 -0
  24. package/dist/model-context/tool-call-text.js +18 -0
  25. package/dist/model-context/tool-call-text.js.map +1 -0
  26. package/dist/model-context/types.d.ts.map +1 -1
  27. package/dist/model-context/types.js +5 -4
  28. package/dist/model-context/types.js.map +1 -1
  29. package/dist/react/RuntimeAdapter.js +1 -1
  30. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  31. package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -1
  32. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  33. package/dist/react/client/AdaptedRemoteThread.js +1 -1
  34. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  35. package/dist/react/client/InMemoryThreadList.js +63 -57
  36. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  37. package/dist/react/client/Interactables.d.ts.map +1 -1
  38. package/dist/react/client/Interactables.js +25 -28
  39. package/dist/react/client/Interactables.js.map +1 -1
  40. package/dist/react/client/RemoteThreadList.d.ts +16 -0
  41. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  42. package/dist/react/client/RemoteThreadList.js +354 -242
  43. package/dist/react/client/RemoteThreadList.js.map +1 -1
  44. package/dist/react/client/Tools.d.ts.map +1 -1
  45. package/dist/react/client/Tools.js +24 -18
  46. package/dist/react/client/Tools.js.map +1 -1
  47. package/dist/react/index.d.ts +2 -1
  48. package/dist/react/index.js +2 -1
  49. package/dist/react/interactables-legacy/Interactables.js +246 -125
  50. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  51. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
  52. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
  53. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
  54. package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
  55. package/dist/react/model-context/makeAssistantTool.js +1 -1
  56. package/dist/react/model-context/makeAssistantTool.js.map +1 -1
  57. package/dist/react/model-context/toolbox.d.ts +2 -14
  58. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  59. package/dist/react/model-context/toolbox.js +1 -13
  60. package/dist/react/model-context/toolbox.js.map +1 -1
  61. package/dist/react/model-context/useAssistantContext.js +37 -7
  62. package/dist/react/model-context/useAssistantContext.js.map +1 -1
  63. package/dist/react/model-context/useAssistantTool.d.ts +2 -2
  64. package/dist/react/model-context/useAssistantTool.js +1 -1
  65. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  66. package/dist/react/model-context/useAuiToolOverrides.js +31 -5
  67. package/dist/react/model-context/useAuiToolOverrides.js.map +1 -1
  68. package/dist/react/model-context/useInlineRender.d.ts.map +1 -1
  69. package/dist/react/model-context/useInlineRender.js +10 -36
  70. package/dist/react/model-context/useInlineRender.js.map +1 -1
  71. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
  72. package/dist/react/model-context/useToolArgsStatus.js +2 -1
  73. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  74. package/dist/react/primitive-hooks/useMessageError.d.ts.map +1 -1
  75. package/dist/react/primitive-hooks/useMessageError.js +2 -8
  76. package/dist/react/primitive-hooks/useMessageError.js.map +1 -1
  77. package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts.map +1 -1
  78. package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -4
  79. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  80. package/dist/react/primitives/composer/ComposerAttachments.d.ts.map +1 -1
  81. package/dist/react/primitives/composer/ComposerAttachments.js +4 -3
  82. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  83. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  84. package/dist/react/primitives/composer/ComposerQueue.js +4 -3
  85. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  86. package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
  87. package/dist/react/primitives/message/MessageAttachments.js +7 -6
  88. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  89. package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
  90. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  91. package/dist/react/primitives/message/MessageParts.js +3 -3
  92. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  93. package/dist/react/primitives/threadList/ThreadListItems.d.ts.map +1 -1
  94. package/dist/react/primitives/threadList/ThreadListItems.js +5 -4
  95. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  96. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +6 -4
  97. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  98. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +59 -29
  99. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  100. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +11 -6
  101. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  102. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +160 -143
  103. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  104. package/dist/react/runtimes/RemoteThreadResource.d.ts +2 -6
  105. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  106. package/dist/react/runtimes/RemoteThreadResource.js +15 -13
  107. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  108. package/dist/react/runtimes/assistant-transport/useLatestRef.js +2 -2
  109. package/dist/react/runtimes/assistant-transport/useLatestRef.js.map +1 -1
  110. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +4 -1
  111. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  112. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +424 -52
  113. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  114. package/dist/react/runtimes/cloud/auiV0.d.ts +5 -1
  115. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  116. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  117. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +29 -0
  118. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -0
  119. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +160 -0
  120. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -0
  121. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +1 -9
  122. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  123. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +16 -153
  124. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  125. package/dist/react/runtimes/external-message-converter.js +1 -1
  126. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  127. package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
  128. package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
  129. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  130. package/dist/react/runtimes/useRemoteThreadListRuntime.js +77 -29
  131. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  132. package/dist/react/types/MessagePartComponentTypes.d.ts +7 -3
  133. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  134. package/dist/react/types/scopes/tools.d.ts +2 -0
  135. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  136. package/dist/runtime/api/message-part-runtime.d.ts +2 -2
  137. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  138. package/dist/runtime/api/message-part-runtime.js +1 -1
  139. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  140. package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
  141. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  142. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  143. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  144. package/dist/runtime/api/thread-list-runtime.d.ts +12 -1
  145. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  146. package/dist/runtime/api/thread-list-runtime.js +23 -14
  147. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  148. package/dist/runtime/api/thread-runtime.d.ts +5 -1
  149. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  150. package/dist/runtime/api/thread-runtime.js +1 -1
  151. package/dist/runtime/api/thread-runtime.js.map +1 -1
  152. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  153. package/dist/runtime/base/base-composer-runtime-core.js +16 -12
  154. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  155. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -2
  156. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  157. package/dist/runtime/base/base-thread-runtime-core.js +8 -7
  158. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  159. package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
  160. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  161. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +18 -3
  162. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  163. package/dist/runtime/interfaces/thread-runtime-core.d.ts +15 -1
  164. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  165. package/dist/runtime/utils/auto-status.d.ts +1 -1
  166. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  167. package/dist/runtime/utils/auto-status.js +6 -2
  168. package/dist/runtime/utils/auto-status.js.map +1 -1
  169. package/dist/runtime/utils/external-message-conversion.d.ts +3 -2
  170. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  171. package/dist/runtime/utils/external-message-conversion.js +5 -4
  172. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  173. package/dist/runtime/utils/message-repository.d.ts +1 -0
  174. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  175. package/dist/runtime/utils/message-repository.js +15 -6
  176. package/dist/runtime/utils/message-repository.js.map +1 -1
  177. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts +6 -3
  178. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts.map +1 -1
  179. package/dist/runtime/utils/resolveToolApprovalResponse.js +12 -3
  180. package/dist/runtime/utils/resolveToolApprovalResponse.js.map +1 -1
  181. package/dist/runtime/utils/thread-message-like.d.ts +2 -10
  182. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  183. package/dist/runtime/utils/thread-message-like.js +1 -1
  184. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  185. package/dist/runtime/utils/tool-call-tree.d.ts +39 -0
  186. package/dist/runtime/utils/tool-call-tree.d.ts.map +1 -0
  187. package/dist/runtime/utils/tool-call-tree.js +85 -0
  188. package/dist/runtime/utils/tool-call-tree.js.map +1 -0
  189. package/dist/runtimes/external-store/external-store-adapter.d.ts +29 -2
  190. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  191. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  192. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +21 -27
  193. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  194. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -1
  195. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  196. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +34 -28
  197. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  198. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +1 -1
  199. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -1
  200. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -1
  201. package/dist/runtimes/internal.d.ts +2 -2
  202. package/dist/runtimes/internal.js +2 -2
  203. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
  204. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  205. package/dist/runtimes/local/local-thread-runtime-core.js +3 -1
  206. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  207. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
  208. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  209. package/dist/runtimes/remote-thread-list/remote-thread-state.js +148 -43
  210. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  211. package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
  212. package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
  213. package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
  214. package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
  215. package/dist/runtimes/remote-thread-list/title.d.ts +11 -0
  216. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -0
  217. package/dist/runtimes/remote-thread-list/title.js +14 -0
  218. package/dist/runtimes/remote-thread-list/title.js.map +1 -0
  219. package/dist/runtimes/remote-thread-list/types.d.ts +16 -5
  220. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  221. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +31 -8
  222. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  223. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +97 -60
  224. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  225. package/dist/store/clients/external-thread.d.ts +1 -1
  226. package/dist/store/clients/external-thread.d.ts.map +1 -1
  227. package/dist/store/clients/external-thread.js +30 -7
  228. package/dist/store/clients/external-thread.js.map +1 -1
  229. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  230. package/dist/store/clients/model-context-client.js +2 -1
  231. package/dist/store/clients/model-context-client.js.map +1 -1
  232. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  233. package/dist/store/clients/single-thread-list.js +2 -1
  234. package/dist/store/clients/single-thread-list.js.map +1 -1
  235. package/dist/store/clients/suggestions.d.ts.map +1 -1
  236. package/dist/store/clients/suggestions.js +19 -6
  237. package/dist/store/clients/suggestions.js.map +1 -1
  238. package/dist/store/clients/thread-message-client.js +1 -1
  239. package/dist/store/clients/thread-selection-events.js +1 -1
  240. package/dist/store/index.d.ts +2 -1
  241. package/dist/store/index.js +2 -1
  242. package/dist/store/internal.d.ts +2 -2
  243. package/dist/store/internal.js +2 -2
  244. package/dist/store/primitive-predicates.d.ts +4 -1
  245. package/dist/store/primitive-predicates.d.ts.map +1 -1
  246. package/dist/store/primitive-predicates.js +9 -1
  247. package/dist/store/primitive-predicates.js.map +1 -1
  248. package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
  249. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  250. package/dist/store/runtime-clients/composer-runtime-client.js +98 -62
  251. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  252. package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
  253. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  254. package/dist/store/runtime-clients/message-runtime-client.js +221 -144
  255. package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
  256. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  257. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +2 -2
  258. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  259. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  260. package/dist/store/runtime-clients/thread-list-runtime-client.js +128 -107
  261. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  262. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  263. package/dist/store/runtime-clients/thread-runtime-client.js +194 -122
  264. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  265. package/dist/store/runtime-clients/useSubscribable.d.ts +3 -1
  266. package/dist/store/runtime-clients/useSubscribable.d.ts.map +1 -1
  267. package/dist/store/runtime-clients/useSubscribable.js +1 -1
  268. package/dist/store/runtime-clients/useSubscribable.js.map +1 -1
  269. package/dist/store/scopes/composer.d.ts +10 -2
  270. package/dist/store/scopes/composer.d.ts.map +1 -1
  271. package/dist/store/scopes/message.d.ts +30 -1
  272. package/dist/store/scopes/message.d.ts.map +1 -1
  273. package/dist/store/scopes/part.d.ts +6 -3
  274. package/dist/store/scopes/part.d.ts.map +1 -1
  275. package/dist/store/scopes/thread-list-item.d.ts +2 -2
  276. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  277. package/dist/store/scopes/thread.d.ts +8 -0
  278. package/dist/store/scopes/thread.d.ts.map +1 -1
  279. package/dist/store/scopes/threads.d.ts +1 -0
  280. package/dist/store/scopes/threads.d.ts.map +1 -1
  281. package/dist/subscribable/subscribable.d.ts +8 -1
  282. package/dist/subscribable/subscribable.d.ts.map +1 -1
  283. package/dist/subscribable/subscribable.js +24 -16
  284. package/dist/subscribable/subscribable.js.map +1 -1
  285. package/dist/types/attachment.d.ts +2 -1
  286. package/dist/types/attachment.d.ts.map +1 -1
  287. package/dist/types/attachment.js +2 -1
  288. package/dist/types/attachment.js.map +1 -1
  289. package/dist/types/message.d.ts +40 -1
  290. package/dist/types/message.d.ts.map +1 -1
  291. package/dist/types/message.js +7 -1
  292. package/dist/types/message.js.map +1 -1
  293. package/dist/utils/abortable-iterable.d.ts +19 -0
  294. package/dist/utils/abortable-iterable.d.ts.map +1 -0
  295. package/dist/utils/abortable-iterable.js +72 -0
  296. package/dist/utils/abortable-iterable.js.map +1 -0
  297. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  298. package/dist/utils/composite-context-provider.js +20 -2
  299. package/dist/utils/composite-context-provider.js.map +1 -1
  300. package/dist/utils/record.d.ts +11 -0
  301. package/dist/utils/record.d.ts.map +1 -0
  302. package/dist/utils/record.js +12 -0
  303. package/dist/utils/record.js.map +1 -0
  304. package/package.json +12 -17
  305. package/src/adapters/attachment.test.ts +50 -1
  306. package/src/adapters/attachment.ts +7 -4
  307. package/src/index.ts +7 -1
  308. package/src/internal.ts +12 -0
  309. package/src/model-context/frame/host.test.ts +24 -0
  310. package/src/model-context/frame/host.ts +10 -4
  311. package/src/model-context/frame/provider.test.ts +288 -1
  312. package/src/model-context/frame/provider.ts +148 -30
  313. package/src/model-context/interactable-composer-metadata.test.ts +28 -0
  314. package/src/model-context/interactable-composer-metadata.ts +9 -3
  315. package/src/model-context/registry.test.ts +12 -0
  316. package/src/model-context/registry.ts +3 -2
  317. package/src/model-context/tool-call-text.test.ts +67 -0
  318. package/src/model-context/tool-call-text.ts +67 -0
  319. package/src/model-context/types.test.ts +25 -0
  320. package/src/model-context/types.ts +9 -4
  321. package/src/react/AssistantRuntimeProvider.test.tsx +66 -2
  322. package/src/react/RuntimeAdapter.test.ts +34 -0
  323. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +1 -3
  324. package/src/react/client/InMemoryThreadList.ts +1 -0
  325. package/src/react/client/Interactables.test.ts +28 -0
  326. package/src/react/client/Interactables.ts +15 -13
  327. package/src/react/client/RemoteThreadList.background.test.ts +526 -0
  328. package/src/react/client/RemoteThreadList.concurrent.test.tsx +315 -0
  329. package/src/react/client/RemoteThreadList.test.ts +617 -3
  330. package/src/react/client/RemoteThreadList.ts +433 -167
  331. package/src/react/client/Tools.ts +29 -12
  332. package/src/react/index.ts +4 -0
  333. package/src/react/interactables-legacy/Interactables.ts +16 -16
  334. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
  335. package/src/react/model-context/makeAssistantTool.ts +2 -2
  336. package/src/react/model-context/modelContextHooks.test.tsx +131 -0
  337. package/src/react/model-context/toolbox.test.ts +9 -9
  338. package/src/react/model-context/toolbox.ts +9 -36
  339. package/src/react/model-context/useAssistantContext.ts +4 -2
  340. package/src/react/model-context/useAssistantTool.ts +2 -2
  341. package/src/react/model-context/useAuiToolOverrides.ts +4 -2
  342. package/src/react/model-context/useInlineRender.test.tsx +48 -0
  343. package/src/react/model-context/useInlineRender.ts +14 -10
  344. package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
  345. package/src/react/model-context/useToolArgsStatus.ts +2 -1
  346. package/src/react/primitive-hooks/useMessageError.ts +2 -19
  347. package/src/react/primitive-hooks/useThreadListLoadMore.ts +2 -3
  348. package/src/react/primitives/composer/ComposerAttachments.test.tsx +76 -0
  349. package/src/react/primitives/composer/ComposerAttachments.tsx +9 -4
  350. package/src/react/primitives/composer/ComposerQueue.test.tsx +72 -0
  351. package/src/react/primitives/composer/ComposerQueue.tsx +7 -4
  352. package/src/react/primitives/message/MessageAttachments.test.tsx +107 -37
  353. package/src/react/primitives/message/MessageAttachments.tsx +10 -7
  354. package/src/react/primitives/message/MessageGroupedParts.tsx +2 -2
  355. package/src/react/primitives/message/MessageParts.tsx +3 -3
  356. package/src/react/primitives/threadList/ThreadListItems.test.tsx +99 -0
  357. package/src/react/primitives/threadList/ThreadListItems.tsx +8 -5
  358. package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
  359. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +127 -1
  360. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
  361. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +99 -35
  362. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
  363. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +274 -0
  364. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +223 -152
  365. package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
  366. package/src/react/runtimes/RemoteThreadResource.ts +20 -16
  367. package/src/react/runtimes/assistant-transport/transport-scheduling.test.ts +51 -2
  368. package/src/react/runtimes/assistant-transport/useLatestRef.ts +2 -2
  369. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +45 -0
  370. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +546 -5
  371. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +546 -103
  372. package/src/react/runtimes/cloud/auiV0.ts +5 -0
  373. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +103 -0
  374. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +237 -0
  375. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +90 -1
  376. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +33 -238
  377. package/src/react/runtimes/external-message-converter.ts +1 -0
  378. package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
  379. package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
  380. package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
  381. package/src/react/runtimes/useRemoteThreadListRuntime.concurrent.test.tsx +216 -0
  382. package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
  383. package/src/react/runtimes/useRemoteThreadListRuntime.ts +57 -6
  384. package/src/react/types/MessagePartComponentTypes.ts +7 -3
  385. package/src/react/types/scopes/tools.ts +2 -0
  386. package/src/runtime/api/message-part-runtime.ts +3 -3
  387. package/src/runtime/api/message-runtime.test.ts +75 -0
  388. package/src/runtime/api/thread-list-item-runtime.ts +10 -3
  389. package/src/runtime/api/thread-list-runtime.ts +46 -12
  390. package/src/runtime/api/thread-runtime.test.ts +78 -0
  391. package/src/runtime/api/thread-runtime.ts +8 -1
  392. package/src/runtime/base/base-composer-runtime-core.ts +36 -8
  393. package/src/runtime/base/base-thread-runtime-core.test.ts +149 -4
  394. package/src/runtime/base/base-thread-runtime-core.ts +16 -8
  395. package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
  396. package/src/runtime/interfaces/thread-list-runtime-core.ts +25 -2
  397. package/src/runtime/interfaces/thread-runtime-core.ts +17 -1
  398. package/src/runtime/utils/auto-status.test.ts +57 -0
  399. package/src/runtime/utils/auto-status.ts +13 -1
  400. package/src/runtime/utils/external-message-conversion.test.ts +165 -0
  401. package/src/runtime/utils/external-message-conversion.ts +18 -2
  402. package/src/runtime/utils/message-repository.ts +29 -17
  403. package/src/runtime/utils/resolveToolApprovalResponse.test.ts +73 -0
  404. package/src/runtime/utils/resolveToolApprovalResponse.ts +27 -6
  405. package/src/runtime/utils/thread-message-like.ts +3 -11
  406. package/src/runtime/utils/tool-call-tree.test.ts +191 -0
  407. package/src/runtime/utils/tool-call-tree.ts +97 -0
  408. package/src/runtimes/external-store/external-store-adapter.ts +38 -2
  409. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
  410. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +56 -33
  411. package/src/runtimes/inert/InertThreadRuntimeCore.ts +1 -1
  412. package/src/runtimes/internal.ts +1 -0
  413. package/src/runtimes/local/local-thread-runtime-core.test.ts +29 -3
  414. package/src/runtimes/local/local-thread-runtime-core.ts +5 -1
  415. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +238 -0
  416. package/src/runtimes/remote-thread-list/remote-thread-state.ts +223 -44
  417. package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
  418. package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
  419. package/src/runtimes/remote-thread-list/title.test.ts +62 -0
  420. package/src/runtimes/remote-thread-list/title.ts +16 -0
  421. package/src/runtimes/remote-thread-list/types.ts +16 -5
  422. package/src/runtimes/tool-invocations/EDGE_CASES.md +110 -5
  423. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +750 -136
  424. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +144 -91
  425. package/src/store/clients/external-thread.ts +62 -11
  426. package/src/store/clients/model-context-client.ts +5 -1
  427. package/src/store/clients/single-thread-list.ts +1 -0
  428. package/src/store/clients/suggestions.test.ts +195 -1
  429. package/src/store/clients/suggestions.ts +45 -34
  430. package/src/store/index.ts +4 -0
  431. package/src/store/internal.ts +2 -0
  432. package/src/store/primitive-predicates.ts +35 -0
  433. package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
  434. package/src/store/runtime-clients/message-runtime-client.ts +54 -6
  435. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
  436. package/src/store/runtime-clients/thread-list-runtime-client.ts +17 -2
  437. package/src/store/runtime-clients/thread-runtime-client.ts +58 -10
  438. package/src/store/runtime-clients/useSubscribable.ts +8 -2
  439. package/src/store/scopes/composer.ts +16 -4
  440. package/src/store/scopes/message.ts +14 -0
  441. package/src/store/scopes/part.ts +6 -3
  442. package/src/store/scopes/thread-list-item.ts +5 -2
  443. package/src/store/scopes/thread.ts +4 -0
  444. package/src/store/scopes/threads.ts +1 -0
  445. package/src/subscribable/subscribable.test.ts +50 -0
  446. package/src/subscribable/subscribable.ts +33 -21
  447. package/src/tests/MessageRepository.test.ts +116 -0
  448. package/src/tests/OptimisticState-list-race.test.ts +1 -0
  449. package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
  450. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +158 -0
  451. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +78 -0
  452. package/src/tests/RemoteThreadListThreadListRuntimeCore-reload.test.ts +52 -0
  453. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
  454. package/src/tests/attachment-adapters.test.ts +14 -0
  455. package/src/tests/base-composer-runtime-core.test.ts +39 -0
  456. package/src/tests/external-store-optimistic-id.test.ts +79 -0
  457. package/src/tests/external-store-repository-instance.test.ts +166 -0
  458. package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
  459. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +49 -34
  460. package/src/tests/external-thread-attachments.test.tsx +224 -1
  461. package/src/tests/external-thread-parity.test.tsx +23 -0
  462. package/src/tests/remote-thread-list-adapter-switch.test.ts +66 -1
  463. package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
  464. package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
  465. package/src/tests/thread-switch-events.test.tsx +475 -2
  466. package/src/tests/tools-scope-migration.test.tsx +28 -0
  467. package/src/types/attachment.ts +4 -0
  468. package/src/types/message.ts +50 -2
  469. package/src/utils/abortable-iterable.test.ts +213 -0
  470. package/src/utils/abortable-iterable.ts +93 -0
  471. package/src/utils/composite-context-provider.test.ts +25 -0
  472. package/src/utils/composite-context-provider.ts +23 -2
  473. package/src/utils/record.test.ts +48 -0
  474. package/src/utils/record.ts +11 -0
@@ -182,12 +182,48 @@ export type ToolApprovalOption = {
182
182
  readonly confirm?: boolean | { title?: string; description?: string };
183
183
  };
184
184
 
185
+ /**
186
+ * How an approval request should be presented, and which answers it accepts.
187
+ * Absent: `"decision"`.
188
+ *
189
+ * The approval seam carries a host-owned request addressed by its own id and
190
+ * resolved once; a request wanting an answer these modes cannot express stays
191
+ * on the `interrupt` seam, which hands the tool an arbitrary payload instead.
192
+ * Unlike {@link ToolApprovalOptionKind} the set is closed, because a renderer
193
+ * that cannot cover every mode exhaustively is back to guessing the affordance.
194
+ */
195
+ export type ToolApprovalDisplay = "decision" | "select" | "text";
196
+
197
+ /**
198
+ * Whether the request asks for a free-form answer, on its own or alongside its
199
+ * options. Renderers read this to decide whether to offer a text affordance,
200
+ * and the runtime reads it to reject a `text` response the host cannot record.
201
+ */
202
+ export const toolApprovalAcceptsText = (approval: {
203
+ readonly display?: ToolApprovalDisplay;
204
+ readonly allowFreeform?: boolean;
205
+ }): boolean => approval.display === "text" || approval.allowFreeform === true;
206
+
185
207
  export type ToolApprovalResponse =
186
- | { readonly approved: boolean; readonly reason?: string }
187
- | { readonly optionId: string; readonly reason?: string }
208
+ | {
209
+ readonly approved: boolean;
210
+ readonly text?: string;
211
+ readonly reason?: string;
212
+ }
213
+ | {
214
+ readonly optionId: string;
215
+ readonly text?: string;
216
+ readonly reason?: string;
217
+ }
188
218
  | {
189
219
  readonly approved: boolean;
190
220
  readonly optionId: string;
221
+ readonly text?: string;
222
+ readonly reason?: string;
223
+ }
224
+ | {
225
+ /** Answer to a request that asks a question rather than for a decision. */
226
+ readonly text: string;
191
227
  readonly reason?: string;
192
228
  };
193
229
 
@@ -228,6 +264,16 @@ export type ToolCallMessagePart<
228
264
  /** Server-side approval gate. `respondToApproval` may only be called while `approved` is undefined and no `resolution` is recorded. */
229
265
  readonly approval?: {
230
266
  readonly id: string;
267
+ /**
268
+ * The question put to the user. A request that asks for something other
269
+ * than permission carries it here, so a renderer can show the question
270
+ * rather than inferring a decision from the tool name.
271
+ */
272
+ readonly prompt?: string;
273
+ /** How the request should be presented. Absent: a plain decision. */
274
+ readonly display?: ToolApprovalDisplay;
275
+ /** Whether a free-form answer is accepted alongside the options. */
276
+ readonly allowFreeform?: boolean;
231
277
  readonly approved?: boolean;
232
278
  readonly reason?: string;
233
279
  readonly isAutomatic?: boolean;
@@ -235,6 +281,8 @@ export type ToolCallMessagePart<
235
281
  readonly options?: readonly ToolApprovalOption[];
236
282
  /** The option chosen at resolution, when options were present. */
237
283
  readonly optionId?: string;
284
+ /** The free-form answer recorded at resolution, when one was given. */
285
+ readonly text?: string;
238
286
  /** Terminal non-decision state: the request was cancelled or expired without a user decision. Set by the host. */
239
287
  readonly resolution?: "cancelled" | "expired";
240
288
  };
@@ -0,0 +1,213 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { abortableIterable, openAbortableIterable } from "./abortable-iterable";
3
+
4
+ const deferred = <T>() => {
5
+ let resolve!: (value: T) => void;
6
+ const promise = new Promise<T>((r) => {
7
+ resolve = r;
8
+ });
9
+ return { promise, resolve };
10
+ };
11
+
12
+ const collect = async <T>(iterable: AsyncIterable<T>) => {
13
+ const seen: T[] = [];
14
+ for await (const value of iterable) seen.push(value);
15
+ return seen;
16
+ };
17
+
18
+ describe("abortableIterable", () => {
19
+ it("settles an opening stream on abort and finalizes a late iterable", async () => {
20
+ const opened = deferred<AsyncIterable<number>>();
21
+ const finalize = vi.fn(async () => ({
22
+ done: true as const,
23
+ value: undefined,
24
+ }));
25
+ const controller = new AbortController();
26
+
27
+ const opening = openAbortableIterable(opened.promise, controller.signal);
28
+ controller.abort();
29
+
30
+ await expect(opening).resolves.toBeUndefined();
31
+ opened.resolve({
32
+ [Symbol.asyncIterator]: () => ({
33
+ next: () => Promise.resolve({ done: true, value: undefined }),
34
+ return: finalize,
35
+ }),
36
+ });
37
+ await vi.waitFor(() => expect(finalize).toHaveBeenCalledTimes(1));
38
+ });
39
+
40
+ it("passes a stream that completes on its own through untouched", async () => {
41
+ async function* source() {
42
+ yield 1;
43
+ yield 2;
44
+ }
45
+ const controller = new AbortController();
46
+
47
+ expect(
48
+ await collect(abortableIterable(source(), controller.signal)),
49
+ ).toEqual([1, 2]);
50
+ });
51
+
52
+ // the reason this wrapper exists: the source ignores the signal and parks on
53
+ // its own work, so nothing but the abort can end the loop
54
+ it("ends the loop while the source is parked mid-chunk", async () => {
55
+ const hang = deferred<void>();
56
+ const finallyRan = vi.fn();
57
+ async function* source() {
58
+ try {
59
+ yield 1;
60
+ await hang.promise;
61
+ yield 2;
62
+ } finally {
63
+ finallyRan();
64
+ }
65
+ }
66
+ const controller = new AbortController();
67
+
68
+ const seen: number[] = [];
69
+ const consuming = (async () => {
70
+ for await (const value of abortableIterable(
71
+ source(),
72
+ controller.signal,
73
+ )) {
74
+ seen.push(value);
75
+ }
76
+ })();
77
+ // let the loop take the first chunk and park awaiting the second
78
+ await new Promise((resolve) => setTimeout(resolve, 0));
79
+
80
+ controller.abort();
81
+
82
+ // settles even though the source never does
83
+ await consuming;
84
+ expect(seen).toEqual([1]);
85
+ expect(finallyRan).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it("finalizes a source still suspended at a yield when the signal aborts", async () => {
89
+ const finallyRan = vi.fn();
90
+ async function* source() {
91
+ try {
92
+ yield 1;
93
+ yield 2;
94
+ } finally {
95
+ finallyRan();
96
+ }
97
+ }
98
+ const controller = new AbortController();
99
+
100
+ const seen: number[] = [];
101
+ for await (const value of abortableIterable(source(), controller.signal)) {
102
+ seen.push(value);
103
+ controller.abort();
104
+ }
105
+
106
+ expect(seen).toEqual([1]);
107
+ expect(finallyRan).toHaveBeenCalledTimes(1);
108
+ });
109
+
110
+ it("reports a stream aborted before the first read as exhausted", async () => {
111
+ const next = vi.fn();
112
+ const controller = new AbortController();
113
+ controller.abort();
114
+
115
+ const seen = await collect(
116
+ abortableIterable(
117
+ {
118
+ [Symbol.asyncIterator]: () => ({ next }),
119
+ } as AsyncIterable<number>,
120
+ controller.signal,
121
+ ),
122
+ );
123
+
124
+ expect(seen).toEqual([]);
125
+ expect(next).not.toHaveBeenCalled();
126
+ });
127
+
128
+ it("propagates a source failure", async () => {
129
+ async function* source() {
130
+ yield 1;
131
+ throw new Error("stream failed");
132
+ }
133
+ const controller = new AbortController();
134
+
135
+ await expect(
136
+ collect(abortableIterable(source(), controller.signal)),
137
+ ).rejects.toThrow("stream failed");
138
+ });
139
+
140
+ it("finalizes a source after its next call rejects", async () => {
141
+ const finalize = vi.fn(async () => ({
142
+ done: true as const,
143
+ value: undefined,
144
+ }));
145
+ const source: AsyncIterable<number> = {
146
+ [Symbol.asyncIterator]: () => ({
147
+ next: () => Promise.reject(new Error("stream failed")),
148
+ return: finalize,
149
+ }),
150
+ };
151
+ const controller = new AbortController();
152
+
153
+ await expect(
154
+ collect(abortableIterable(source, controller.signal)),
155
+ ).rejects.toThrow("stream failed");
156
+ expect(finalize).toHaveBeenCalledTimes(1);
157
+ });
158
+
159
+ it("finalizes the source when the consumer breaks out", async () => {
160
+ const finallyRan = vi.fn();
161
+ async function* source() {
162
+ try {
163
+ yield 1;
164
+ yield 2;
165
+ } finally {
166
+ finallyRan();
167
+ }
168
+ }
169
+ const controller = new AbortController();
170
+
171
+ for await (const _ of abortableIterable(source(), controller.signal)) {
172
+ break;
173
+ }
174
+
175
+ // finalized without the break waiting on it
176
+ await new Promise((resolve) => setTimeout(resolve, 0));
177
+ expect(finallyRan).toHaveBeenCalledTimes(1);
178
+ });
179
+
180
+ it("does not let a source that parks in its own cleanup stall the break", async () => {
181
+ const hang = deferred<void>();
182
+ async function* source() {
183
+ try {
184
+ yield 1;
185
+ yield 2;
186
+ } finally {
187
+ await hang.promise;
188
+ }
189
+ }
190
+ const controller = new AbortController();
191
+
192
+ // resolves even though the source's finally never does
193
+ for await (const _ of abortableIterable(source(), controller.signal)) {
194
+ break;
195
+ }
196
+ });
197
+
198
+ it("releases its abort listener after each chunk", async () => {
199
+ const controller = new AbortController();
200
+ const added = vi.spyOn(controller.signal, "addEventListener");
201
+ const removed = vi.spyOn(controller.signal, "removeEventListener");
202
+ async function* source() {
203
+ yield 1;
204
+ yield 2;
205
+ yield 3;
206
+ }
207
+
208
+ await collect(abortableIterable(source(), controller.signal));
209
+
210
+ expect(added).toHaveBeenCalledTimes(removed.mock.calls.length);
211
+ expect(added.mock.calls.length).toBeGreaterThan(0);
212
+ });
213
+ });
@@ -0,0 +1,93 @@
1
+ const done = <T>(): IteratorResult<T> => ({ done: true, value: undefined });
2
+
3
+ /** Resolves at cancellation, so an await on a stream can stop being the only way out. */
4
+ const whenAborted = (signal: AbortSignal): Promise<undefined> =>
5
+ new Promise((resolve) => {
6
+ if (signal.aborted) return resolve(undefined);
7
+ signal.addEventListener("abort", () => resolve(undefined), { once: true });
8
+ });
9
+
10
+ export const openAbortableIterable = async <T>(
11
+ source: AsyncIterable<T> | Promise<AsyncIterable<T>>,
12
+ signal: AbortSignal,
13
+ ): Promise<AsyncIterable<T> | undefined> => {
14
+ const opened = Promise.resolve(source);
15
+ const iterable = await Promise.race([opened, whenAborted(signal)]);
16
+ if (!iterable) {
17
+ void opened
18
+ .then((late) => late[Symbol.asyncIterator]().return?.(undefined))
19
+ .catch(() => {});
20
+ }
21
+ return iterable;
22
+ };
23
+
24
+ /**
25
+ * Presents a stream as exhausted once the signal aborts, so a consumer stops
26
+ * waiting on it.
27
+ *
28
+ * A caller's stream is free to ignore the abort signal it is handed. One that
29
+ * also stops yielding, because it is awaiting its own work, leaves `for await`
30
+ * parked on `next()` with nothing left to wake it: the run never settles and
31
+ * whatever is serialized behind it never starts. Reporting the stream as
32
+ * exhausted ends the loop at cancellation instead.
33
+ *
34
+ * The source is finalized on the way out, but never awaited, since a stream
35
+ * that is already parked would not settle that either.
36
+ */
37
+ export const abortableIterable = <T>(
38
+ source: AsyncIterable<T>,
39
+ signal: AbortSignal,
40
+ ): AsyncIterable<T> => ({
41
+ [Symbol.asyncIterator]: () => {
42
+ const iterator = source[Symbol.asyncIterator]();
43
+ const finalize = () => {
44
+ try {
45
+ iterator.return?.(undefined)?.catch(() => {});
46
+ } catch {
47
+ // a source that throws on finalize has nothing left to clean up
48
+ }
49
+ };
50
+
51
+ return {
52
+ [Symbol.asyncIterator]() {
53
+ return this;
54
+ },
55
+ next: () => {
56
+ if (signal.aborted) {
57
+ finalize();
58
+ return Promise.resolve(done<T>());
59
+ }
60
+
61
+ return new Promise<IteratorResult<T>>((resolve, reject) => {
62
+ // Registered per chunk and released as soon as the chunk settles, so
63
+ // a long stream does not accumulate one reaction per chunk.
64
+ const onAbort = () => {
65
+ finalize();
66
+ resolve(done<T>());
67
+ };
68
+ signal.addEventListener("abort", onAbort, { once: true });
69
+ const release = () => signal.removeEventListener("abort", onAbort);
70
+
71
+ iterator.next().then(
72
+ (result) => {
73
+ release();
74
+ resolve(result);
75
+ },
76
+ (error: unknown) => {
77
+ release();
78
+ finalize();
79
+ reject(error);
80
+ },
81
+ );
82
+ });
83
+ },
84
+ // The consumer breaks out of the loop to stop a cancelled run, so this is
85
+ // reached on the same path as an abort and finalizes the same way: a
86
+ // source whose own cleanup parks must not stall the break either.
87
+ return: async () => {
88
+ finalize();
89
+ return done<T>();
90
+ },
91
+ };
92
+ },
93
+ });
@@ -114,6 +114,31 @@ describe("CompositeContextProvider", () => {
114
114
  expect(composite.getModelContext().system).toBeUndefined();
115
115
  });
116
116
 
117
+ it("finishes unregistering when the provider cleanup throws", () => {
118
+ const composite = new CompositeContextProvider();
119
+ const cleanupError = new Error("cleanup failed");
120
+ const subscriber = vi.fn();
121
+ const unsubscribe = vi.fn(() => {
122
+ throw cleanupError;
123
+ });
124
+
125
+ composite.subscribe(subscriber);
126
+ const unregister = composite.registerModelContextProvider({
127
+ getModelContext: () => ({ system: "provider instructions" }),
128
+ subscribe: () => unsubscribe,
129
+ });
130
+ subscriber.mockClear();
131
+
132
+ expect(() => unregister()).toThrow(cleanupError);
133
+ expect(composite.getModelContext().system).toBeUndefined();
134
+ expect(subscriber).toHaveBeenCalledOnce();
135
+ expect(unsubscribe).toHaveBeenCalledOnce();
136
+
137
+ expect(() => unregister()).not.toThrow();
138
+ expect(subscriber).toHaveBeenCalledOnce();
139
+ expect(unsubscribe).toHaveBeenCalledOnce();
140
+ });
141
+
117
142
  it("notifies every subscriber and rethrows on provider updates", () => {
118
143
  const composite = new CompositeContextProvider();
119
144
  const error = new Error("subscriber failed");
@@ -32,12 +32,33 @@ export class CompositeContextProvider implements ModelContextProvider {
32
32
  }
33
33
  this._providerUnsubscribes.set(id, unsubscribe);
34
34
  this.notifySubscribers();
35
+ let released = false;
35
36
  return () => {
37
+ if (released) return;
38
+ released = true;
36
39
  this._providers.delete(id);
37
40
  const unsubscribe = this._providerUnsubscribes.get(id);
38
- unsubscribe?.();
39
41
  this._providerUnsubscribes.delete(id);
40
- this.notifySubscribers();
42
+
43
+ let cleanupFailed = false;
44
+ let cleanupError: unknown;
45
+ const runCleanup = (cleanup: () => void) => {
46
+ try {
47
+ cleanup();
48
+ } catch (error) {
49
+ if (cleanupFailed) {
50
+ console.error(error);
51
+ } else {
52
+ cleanupFailed = true;
53
+ cleanupError = error;
54
+ }
55
+ }
56
+ };
57
+
58
+ if (unsubscribe) runCleanup(unsubscribe);
59
+ runCleanup(() => this.notifySubscribers());
60
+
61
+ if (cleanupFailed) throw cleanupError;
41
62
  };
42
63
  }
43
64
 
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nullProtoRecord } from "./record";
3
+
4
+ describe("nullProtoRecord", () => {
5
+ it("has no prototype", () => {
6
+ expect(Object.getPrototypeOf(nullProtoRecord())).toBeNull();
7
+ expect(Object.getPrototypeOf(nullProtoRecord({ a: 1 }))).toBeNull();
8
+ });
9
+
10
+ it("copies own entries from every source, later sources winning", () => {
11
+ expect({ ...nullProtoRecord({ a: 1, b: 2 }, undefined, { b: 3 }) }).toEqual(
12
+ {
13
+ a: 1,
14
+ b: 3,
15
+ },
16
+ );
17
+ });
18
+
19
+ it.each(["__proto__", "constructor", "toString"])(
20
+ "assigns %s as an own property",
21
+ (key) => {
22
+ const record = nullProtoRecord<number>();
23
+ record[key] = 1;
24
+
25
+ expect(Object.hasOwn(record, key)).toBe(true);
26
+ expect(record[key]).toBe(1);
27
+ expect(Object.keys(record)).toEqual([key]);
28
+ },
29
+ );
30
+
31
+ it.each(["__proto__", "constructor", "toString"])(
32
+ "carries %s through a source",
33
+ (key) => {
34
+ const record = nullProtoRecord<number>({ [key]: 1, ok: 2 });
35
+
36
+ expect(Object.keys(record)).toEqual([key, "ok"]);
37
+ expect(record[key]).toBe(1);
38
+ },
39
+ );
40
+
41
+ it("reads an absent prototype-named key as undefined", () => {
42
+ const record = nullProtoRecord<number>({ ok: 1 });
43
+
44
+ expect(record["__proto__"]).toBeUndefined();
45
+ expect(record["constructor"]).toBeUndefined();
46
+ expect(record["toString"]).toBeUndefined();
47
+ });
48
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Builds a dictionary with no prototype from the own entries of its arguments.
3
+ * Bracket assignment on a plain object routes a `__proto__` key through the
4
+ * `Object.prototype` setter instead of defining an own property, so a record
5
+ * assigned into by a key the app does not control is built this way.
6
+ */
7
+ export const nullProtoRecord = <T>(
8
+ base?: Readonly<Record<string, T>>,
9
+ ...rest: readonly (Readonly<Record<string, NoInfer<T>>> | undefined)[]
10
+ ): Record<string, T> =>
11
+ Object.assign(Object.create(null) as Record<string, T>, base, ...rest);