@assistant-ui/core 0.3.17 → 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 (284) 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 +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal.d.ts +3 -2
  7. package/dist/internal.js +3 -2
  8. package/dist/model-context/frame/host.d.ts.map +1 -1
  9. package/dist/model-context/frame/host.js +10 -4
  10. package/dist/model-context/frame/host.js.map +1 -1
  11. package/dist/model-context/frame/provider.d.ts +2 -0
  12. package/dist/model-context/frame/provider.d.ts.map +1 -1
  13. package/dist/model-context/frame/provider.js +30 -7
  14. package/dist/model-context/frame/provider.js.map +1 -1
  15. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
  16. package/dist/model-context/interactable-composer-metadata.js +16 -9
  17. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  18. package/dist/model-context/registry.d.ts.map +1 -1
  19. package/dist/model-context/registry.js +3 -5
  20. package/dist/model-context/registry.js.map +1 -1
  21. package/dist/model-context/types.d.ts.map +1 -1
  22. package/dist/model-context/types.js +5 -4
  23. package/dist/model-context/types.js.map +1 -1
  24. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  25. package/dist/react/client/InMemoryThreadList.js +62 -56
  26. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  27. package/dist/react/client/Interactables.d.ts.map +1 -1
  28. package/dist/react/client/Interactables.js +24 -27
  29. package/dist/react/client/Interactables.js.map +1 -1
  30. package/dist/react/client/RemoteThreadList.d.ts +1 -0
  31. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  32. package/dist/react/client/RemoteThreadList.js +151 -141
  33. package/dist/react/client/RemoteThreadList.js.map +1 -1
  34. package/dist/react/client/Tools.d.ts.map +1 -1
  35. package/dist/react/client/Tools.js +18 -16
  36. package/dist/react/client/Tools.js.map +1 -1
  37. package/dist/react/interactables-legacy/Interactables.js +242 -121
  38. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  39. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
  40. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
  41. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
  42. package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
  43. package/dist/react/model-context/makeAssistantTool.js +1 -1
  44. package/dist/react/model-context/makeAssistantTool.js.map +1 -1
  45. package/dist/react/model-context/useAssistantTool.d.ts +2 -2
  46. package/dist/react/model-context/useAssistantTool.js +1 -1
  47. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  48. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
  49. package/dist/react/model-context/useToolArgsStatus.js +2 -1
  50. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  51. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  52. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +16 -8
  53. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  54. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -1
  55. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +103 -103
  57. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -0
  59. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  60. package/dist/react/runtimes/RemoteThreadResource.js +13 -11
  61. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  62. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +4 -1
  63. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  64. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +401 -34
  65. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  66. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  67. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +2 -1
  68. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  69. package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
  70. package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
  71. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  72. package/dist/react/runtimes/useRemoteThreadListRuntime.js +63 -23
  73. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  74. package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
  75. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  76. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  77. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  78. package/dist/runtime/api/thread-list-runtime.d.ts +3 -0
  79. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  80. package/dist/runtime/api/thread-list-runtime.js +18 -14
  81. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  82. package/dist/runtime/api/thread-runtime.d.ts +4 -0
  83. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  84. package/dist/runtime/api/thread-runtime.js +1 -1
  85. package/dist/runtime/api/thread-runtime.js.map +1 -1
  86. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  87. package/dist/runtime/base/base-composer-runtime-core.js +16 -12
  88. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  89. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  90. package/dist/runtime/base/base-thread-runtime-core.js +6 -5
  91. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  92. package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
  93. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  94. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -1
  95. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  96. package/dist/runtime/utils/external-message-conversion.js +1 -1
  97. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  98. package/dist/runtime/utils/message-repository.d.ts +1 -0
  99. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  100. package/dist/runtime/utils/message-repository.js +15 -6
  101. package/dist/runtime/utils/message-repository.js.map +1 -1
  102. package/dist/runtime/utils/thread-message-like.js +1 -1
  103. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  104. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  105. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +21 -27
  106. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  107. package/dist/runtimes/internal.d.ts +2 -2
  108. package/dist/runtimes/internal.js +2 -2
  109. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
  110. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  111. package/dist/runtimes/remote-thread-list/remote-thread-state.js +130 -42
  112. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  113. package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
  114. package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
  115. package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
  116. package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
  117. package/dist/runtimes/remote-thread-list/title.d.ts +3 -1
  118. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
  119. package/dist/runtimes/remote-thread-list/title.js +9 -1
  120. package/dist/runtimes/remote-thread-list/title.js.map +1 -1
  121. package/dist/runtimes/remote-thread-list/types.d.ts +10 -0
  122. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  123. package/dist/store/clients/external-thread.d.ts.map +1 -1
  124. package/dist/store/clients/external-thread.js +19 -3
  125. package/dist/store/clients/external-thread.js.map +1 -1
  126. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  127. package/dist/store/clients/model-context-client.js +2 -1
  128. package/dist/store/clients/model-context-client.js.map +1 -1
  129. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  130. package/dist/store/clients/single-thread-list.js +1 -0
  131. package/dist/store/clients/single-thread-list.js.map +1 -1
  132. package/dist/store/clients/suggestions.d.ts.map +1 -1
  133. package/dist/store/clients/suggestions.js +5 -4
  134. package/dist/store/clients/suggestions.js.map +1 -1
  135. package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
  136. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  137. package/dist/store/runtime-clients/composer-runtime-client.js +97 -61
  138. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  139. package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
  140. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  141. package/dist/store/runtime-clients/message-runtime-client.js +221 -144
  142. package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
  143. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  144. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  145. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  146. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  147. package/dist/store/runtime-clients/thread-list-runtime-client.js +54 -52
  148. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  149. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  150. package/dist/store/runtime-clients/thread-runtime-client.js +193 -121
  151. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  152. package/dist/store/scopes/composer.d.ts +10 -2
  153. package/dist/store/scopes/composer.d.ts.map +1 -1
  154. package/dist/store/scopes/message.d.ts +30 -1
  155. package/dist/store/scopes/message.d.ts.map +1 -1
  156. package/dist/store/scopes/thread-list-item.d.ts +2 -2
  157. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  158. package/dist/store/scopes/thread.d.ts +8 -0
  159. package/dist/store/scopes/thread.d.ts.map +1 -1
  160. package/dist/store/scopes/threads.d.ts +1 -0
  161. package/dist/store/scopes/threads.d.ts.map +1 -1
  162. package/dist/types/attachment.d.ts +2 -1
  163. package/dist/types/attachment.d.ts.map +1 -1
  164. package/dist/types/attachment.js +2 -1
  165. package/dist/types/attachment.js.map +1 -1
  166. package/dist/utils/abortable-iterable.d.ts +19 -0
  167. package/dist/utils/abortable-iterable.d.ts.map +1 -0
  168. package/dist/utils/abortable-iterable.js +72 -0
  169. package/dist/utils/abortable-iterable.js.map +1 -0
  170. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  171. package/dist/utils/composite-context-provider.js +20 -2
  172. package/dist/utils/composite-context-provider.js.map +1 -1
  173. package/dist/utils/record.d.ts +11 -0
  174. package/dist/utils/record.d.ts.map +1 -0
  175. package/dist/utils/record.js +12 -0
  176. package/dist/utils/record.js.map +1 -0
  177. package/package.json +10 -10
  178. package/src/adapters/attachment.test.ts +50 -1
  179. package/src/adapters/attachment.ts +7 -4
  180. package/src/index.ts +1 -0
  181. package/src/internal.ts +4 -0
  182. package/src/model-context/frame/host.test.ts +24 -0
  183. package/src/model-context/frame/host.ts +10 -4
  184. package/src/model-context/frame/provider.test.ts +71 -0
  185. package/src/model-context/frame/provider.ts +37 -7
  186. package/src/model-context/interactable-composer-metadata.test.ts +28 -0
  187. package/src/model-context/interactable-composer-metadata.ts +9 -3
  188. package/src/model-context/registry.test.ts +12 -0
  189. package/src/model-context/registry.ts +3 -2
  190. package/src/model-context/types.test.ts +25 -0
  191. package/src/model-context/types.ts +9 -4
  192. package/src/react/RuntimeAdapter.test.ts +34 -0
  193. package/src/react/client/InMemoryThreadList.ts +1 -0
  194. package/src/react/client/Interactables.test.ts +28 -0
  195. package/src/react/client/Interactables.ts +15 -13
  196. package/src/react/client/RemoteThreadList.background.test.ts +148 -6
  197. package/src/react/client/RemoteThreadList.concurrent.test.tsx +42 -1
  198. package/src/react/client/RemoteThreadList.test.ts +345 -3
  199. package/src/react/client/RemoteThreadList.ts +166 -110
  200. package/src/react/client/Tools.ts +22 -11
  201. package/src/react/interactables-legacy/Interactables.ts +16 -16
  202. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
  203. package/src/react/model-context/makeAssistantTool.ts +2 -2
  204. package/src/react/model-context/useAssistantTool.ts +2 -2
  205. package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
  206. package/src/react/model-context/useToolArgsStatus.ts +2 -1
  207. package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
  208. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +33 -0
  209. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
  210. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +28 -5
  211. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
  212. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +105 -0
  213. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +136 -111
  214. package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
  215. package/src/react/runtimes/RemoteThreadResource.ts +19 -12
  216. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +483 -6
  217. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +504 -68
  218. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +15 -0
  219. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +1 -0
  220. package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
  221. package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
  222. package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
  223. package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
  224. package/src/react/runtimes/useRemoteThreadListRuntime.ts +33 -0
  225. package/src/runtime/api/message-runtime.test.ts +75 -0
  226. package/src/runtime/api/thread-list-item-runtime.ts +10 -3
  227. package/src/runtime/api/thread-list-runtime.ts +21 -11
  228. package/src/runtime/api/thread-runtime.test.ts +78 -0
  229. package/src/runtime/api/thread-runtime.ts +8 -1
  230. package/src/runtime/base/base-composer-runtime-core.ts +36 -8
  231. package/src/runtime/base/base-thread-runtime-core.test.ts +55 -1
  232. package/src/runtime/base/base-thread-runtime-core.ts +6 -4
  233. package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
  234. package/src/runtime/interfaces/thread-list-runtime-core.ts +5 -1
  235. package/src/runtime/utils/external-message-conversion.test.ts +68 -0
  236. package/src/runtime/utils/external-message-conversion.ts +1 -1
  237. package/src/runtime/utils/message-repository.ts +29 -17
  238. package/src/runtime/utils/thread-message-like.ts +1 -1
  239. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
  240. package/src/runtimes/internal.ts +1 -0
  241. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +128 -2
  242. package/src/runtimes/remote-thread-list/remote-thread-state.ts +182 -42
  243. package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
  244. package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
  245. package/src/runtimes/remote-thread-list/title.test.ts +62 -0
  246. package/src/runtimes/remote-thread-list/title.ts +13 -0
  247. package/src/runtimes/remote-thread-list/types.ts +10 -0
  248. package/src/store/clients/external-thread.ts +48 -3
  249. package/src/store/clients/model-context-client.ts +5 -1
  250. package/src/store/clients/single-thread-list.ts +1 -0
  251. package/src/store/clients/suggestions.test.ts +40 -0
  252. package/src/store/clients/suggestions.ts +13 -10
  253. package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
  254. package/src/store/runtime-clients/message-runtime-client.ts +54 -6
  255. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
  256. package/src/store/runtime-clients/thread-list-runtime-client.ts +1 -0
  257. package/src/store/runtime-clients/thread-runtime-client.ts +58 -10
  258. package/src/store/scopes/composer.ts +16 -4
  259. package/src/store/scopes/message.ts +14 -0
  260. package/src/store/scopes/thread-list-item.ts +5 -2
  261. package/src/store/scopes/thread.ts +4 -0
  262. package/src/store/scopes/threads.ts +1 -0
  263. package/src/tests/MessageRepository.test.ts +116 -0
  264. package/src/tests/OptimisticState-list-race.test.ts +1 -0
  265. package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
  266. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +47 -0
  267. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
  268. package/src/tests/attachment-adapters.test.ts +14 -0
  269. package/src/tests/base-composer-runtime-core.test.ts +39 -0
  270. package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
  271. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +27 -18
  272. package/src/tests/external-thread-attachments.test.tsx +139 -0
  273. package/src/tests/external-thread-parity.test.tsx +23 -0
  274. package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
  275. package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
  276. package/src/tests/thread-switch-events.test.tsx +78 -0
  277. package/src/tests/tools-scope-migration.test.tsx +28 -0
  278. package/src/types/attachment.ts +4 -0
  279. package/src/utils/abortable-iterable.test.ts +213 -0
  280. package/src/utils/abortable-iterable.ts +93 -0
  281. package/src/utils/composite-context-provider.test.ts +25 -0
  282. package/src/utils/composite-context-provider.ts +23 -2
  283. package/src/utils/record.test.ts +48 -0
  284. package/src/utils/record.ts +11 -0
package/src/internal.ts CHANGED
@@ -67,6 +67,10 @@ export {
67
67
  resolveFilePartSource,
68
68
  } from "./utils/data-url";
69
69
  export { invokeUserCallback } from "./utils/invoke-user-callback";
70
+ export {
71
+ abortableIterable,
72
+ openAbortableIterable,
73
+ } from "./utils/abortable-iterable";
70
74
  export { detectImageMediaType } from "./utils/image-media-type";
71
75
  export {
72
76
  resolveFileMediaType,
@@ -133,6 +133,30 @@ describe("AssistantFrameHost", () => {
133
133
  host.dispose();
134
134
  });
135
135
 
136
+ it("cleans up tool calls when posting the request fails", async () => {
137
+ const { execute, host, postMessage } = createHost();
138
+ const error = new Error("postMessage failed");
139
+ const abortController = new AbortController();
140
+ const removeEventListener = vi.spyOn(
141
+ abortController.signal,
142
+ "removeEventListener",
143
+ );
144
+ postMessage.mockImplementation((data) => {
145
+ if (data.message.type === "tool-call") throw error;
146
+ });
147
+
148
+ await expect(
149
+ execute({}, { ...executionContext, abortSignal: abortController.signal }),
150
+ ).rejects.toBe(error);
151
+
152
+ expect(vi.getTimerCount()).toBe(0);
153
+ expect(removeEventListener).toHaveBeenCalledWith(
154
+ "abort",
155
+ expect.any(Function),
156
+ );
157
+ host.dispose();
158
+ });
159
+
136
160
  it("rejects tool results that carry an empty error message", async () => {
137
161
  const { dispatchMessage, execute, getToolCallId, host } = createHost();
138
162
  const result = Promise.resolve(
@@ -195,10 +195,16 @@ export class AssistantFrameHost implements ModelContextProvider {
195
195
  }, timeout);
196
196
  abortSignal?.addEventListener("abort", onAbort, { once: true });
197
197
 
198
- this._iframeWindow.postMessage(
199
- { channel: FRAME_MESSAGE_CHANNEL, message },
200
- this._targetOrigin,
201
- );
198
+ try {
199
+ this._iframeWindow.postMessage(
200
+ { channel: FRAME_MESSAGE_CHANNEL, message },
201
+ this._targetOrigin,
202
+ );
203
+ } catch (error) {
204
+ const pending = this._pendingRequests.get(message.id);
205
+ this._pendingRequests.delete(message.id);
206
+ pending?.reject(error);
207
+ }
202
208
  });
203
209
  }
204
210
 
@@ -120,6 +120,42 @@ describe("AssistantFrameProvider", () => {
120
120
  await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
121
121
  });
122
122
 
123
+ it("does not broadcast from a disposed provider", async () => {
124
+ vi.useFakeTimers();
125
+
126
+ try {
127
+ AssistantFrameProvider.addModelContextProvider({
128
+ getModelContext: () => ({ system: "disposed context" }),
129
+ });
130
+ AssistantFrameProvider.dispose();
131
+
132
+ AssistantFrameProvider.addModelContextProvider({
133
+ getModelContext: () => ({ system: "current context" }),
134
+ });
135
+ vi.mocked(parentWindow.postMessage).mockClear();
136
+
137
+ await vi.runAllTimersAsync();
138
+
139
+ expect(parentWindow.postMessage).toHaveBeenCalledOnce();
140
+ expect(parentWindow.postMessage).toHaveBeenCalledWith(
141
+ {
142
+ channel: FRAME_MESSAGE_CHANNEL,
143
+ message: {
144
+ type: "model-context-update",
145
+ context: {
146
+ system: "current context",
147
+ tools: {},
148
+ },
149
+ },
150
+ },
151
+ window.location.origin,
152
+ );
153
+ } finally {
154
+ AssistantFrameProvider.dispose();
155
+ vi.useRealTimers();
156
+ }
157
+ });
158
+
123
159
  it("reports a failure even when the thrown error has an empty message", async () => {
124
160
  const execute = vi.fn(async () => {
125
161
  throw new Error();
@@ -146,6 +182,41 @@ describe("AssistantFrameProvider", () => {
146
182
  });
147
183
  });
148
184
 
185
+ it("reports tool results that cannot cross the frame boundary", async () => {
186
+ const consoleError = vi
187
+ .spyOn(console, "error")
188
+ .mockImplementation(() => undefined);
189
+ const execute = vi.fn(async () => () => undefined);
190
+ vi.mocked(parentWindow.postMessage).mockImplementation((data) => {
191
+ structuredClone(data);
192
+ });
193
+ AssistantFrameProvider.addModelContextProvider({
194
+ getModelContext: () => ({
195
+ tools: { sensitiveTool: { execute } },
196
+ }),
197
+ });
198
+
199
+ dispatchToolCall(window.location.origin);
200
+
201
+ await vi.waitFor(() => {
202
+ expect(consoleError).toHaveBeenCalledWith(
203
+ "[assistant-ui] AssistantFrame tool result could not be sent.",
204
+ expect.objectContaining({ name: "DataCloneError" }),
205
+ );
206
+ expect(parentWindow.postMessage).toHaveBeenCalledWith(
207
+ {
208
+ channel: FRAME_MESSAGE_CHANNEL,
209
+ message: {
210
+ type: "tool-result",
211
+ id: "tool-call-1",
212
+ error: "Tool result could not be sent across the frame boundary",
213
+ },
214
+ },
215
+ { targetOrigin: window.location.origin },
216
+ );
217
+ });
218
+ });
219
+
149
220
  it("aborts in-flight tool calls when the parent cancels them", async () => {
150
221
  let toolSignal: AbortSignal | undefined;
151
222
  const execute = vi.fn(
@@ -47,13 +47,18 @@ export class AssistantFrameProvider {
47
47
  private _targetOrigin: string;
48
48
  private _strictRegistrations = 0;
49
49
  private _wildcardRegistrations = 0;
50
+ private _startupTimer: ReturnType<typeof setTimeout> | undefined;
51
+ private _disposed = false;
50
52
 
51
53
  private constructor(targetOrigin: string = getDefaultTargetOrigin()) {
52
54
  this._targetOrigin = targetOrigin;
53
55
  this.handleMessage = this.handleMessage.bind(this);
54
56
  window.addEventListener("message", this.handleMessage);
55
57
 
56
- setTimeout(() => this.broadcastUpdate(), 0);
58
+ this._startupTimer = setTimeout(() => {
59
+ this._startupTimer = undefined;
60
+ this.broadcastUpdate();
61
+ }, 0);
57
62
  }
58
63
 
59
64
  private static getInstance(targetOrigin?: string): AssistantFrameProvider {
@@ -106,7 +111,12 @@ export class AssistantFrameProvider {
106
111
  break;
107
112
 
108
113
  case "tool-call":
109
- this.handleToolCall(message, event);
114
+ void this.handleToolCall(message, event).catch((error: unknown) => {
115
+ console.error(
116
+ "[assistant-ui] AssistantFrame tool call failed.",
117
+ error,
118
+ );
119
+ });
110
120
  break;
111
121
 
112
122
  case "tool-cancel":
@@ -156,11 +166,25 @@ export class AssistantFrameProvider {
156
166
  if (this._activeToolCalls.get(message.id) !== activeCall) return;
157
167
  this._activeToolCalls.delete(message.id);
158
168
 
159
- this.sendMessage(event, {
160
- type: "tool-result",
161
- id: message.id,
162
- ...(error !== undefined ? { error } : { result }),
163
- });
169
+ try {
170
+ this.sendMessage(event, {
171
+ type: "tool-result",
172
+ id: message.id,
173
+ ...(error !== undefined ? { error } : { result }),
174
+ });
175
+ } catch (sendError) {
176
+ if (error !== undefined) throw sendError;
177
+
178
+ console.error(
179
+ "[assistant-ui] AssistantFrame tool result could not be sent.",
180
+ sendError,
181
+ );
182
+ this.sendMessage(event, {
183
+ type: "tool-result",
184
+ id: message.id,
185
+ error: "Tool result could not be sent across the frame boundary",
186
+ });
187
+ }
164
188
  }
165
189
 
166
190
  private cancelToolCall(id: string) {
@@ -244,6 +268,7 @@ export class AssistantFrameProvider {
244
268
  }
245
269
 
246
270
  private broadcastUpdate() {
271
+ if (this._disposed) return;
247
272
  if (window.parent && window.parent !== window) {
248
273
  const updateMessage: FrameMessage = {
249
274
  type: "model-context-update",
@@ -375,6 +400,11 @@ export class AssistantFrameProvider {
375
400
  static dispose() {
376
401
  if (AssistantFrameProvider._instance) {
377
402
  const instance = AssistantFrameProvider._instance;
403
+ instance._disposed = true;
404
+ if (instance._startupTimer !== undefined) {
405
+ clearTimeout(instance._startupTimer);
406
+ instance._startupTimer = undefined;
407
+ }
378
408
  window.removeEventListener("message", instance.handleMessage);
379
409
 
380
410
  let cleanupFailed = false;
@@ -131,6 +131,34 @@ describe("shallowMergeInteractableState", () => {
131
131
  });
132
132
  });
133
133
 
134
+ it("uses the first valid patch for each array item id", () => {
135
+ expect(
136
+ shallowMergeInteractableState(
137
+ { tasks: [{ id: 1, title: "Original", done: false }] },
138
+ {
139
+ tasks: {
140
+ update: [
141
+ null,
142
+ { title: "Missing id" },
143
+ { id: "1", title: "String id" },
144
+ { id: 1, title: "First", done: true },
145
+ { id: 1, title: "Second" },
146
+ ],
147
+ },
148
+ },
149
+ ),
150
+ ).toEqual({ tasks: [{ id: 1, title: "First", done: true }] });
151
+ });
152
+
153
+ it("does not match NaN array item ids", () => {
154
+ expect(
155
+ shallowMergeInteractableState(
156
+ { tasks: [{ id: Number.NaN, title: "Original" }] },
157
+ { tasks: { update: [{ id: Number.NaN, title: "Patched" }] } },
158
+ ),
159
+ ).toEqual({ tasks: [{ id: Number.NaN, title: "Original" }] });
160
+ });
161
+
134
162
  it("keeps raw array replacement semantics", () => {
135
163
  expect(
136
164
  shallowMergeInteractableState(
@@ -94,12 +94,18 @@ function applyArrayUpdate(
94
94
 
95
95
  const patches = update.update;
96
96
  if (Array.isArray(patches) && patches.length > 0) {
97
+ const patchesById = new Map<string | number, Record<string, unknown>>();
98
+ for (const candidate of patches) {
99
+ const id = getArrayItemId(candidate);
100
+ if (id !== undefined && !Number.isNaN(id) && !patchesById.has(id)) {
101
+ patchesById.set(id, candidate as Record<string, unknown>);
102
+ }
103
+ }
104
+
97
105
  next = next.map((item) => {
98
106
  const id = getArrayItemId(item);
99
107
  if (id === undefined || !isRecord(item)) return item;
100
- const patch = patches.find(
101
- (candidate) => isRecord(candidate) && candidate.id === id,
102
- );
108
+ const patch = patchesById.get(id);
103
109
  return patch ? { ...item, ...patch } : item;
104
110
  });
105
111
  }
@@ -2,6 +2,18 @@ import { describe, expect, it, vi } from "vitest";
2
2
  import { ModelContextRegistry } from "./registry";
3
3
 
4
4
  describe("ModelContextRegistry", () => {
5
+ it.each(["__proto__", "constructor", "toString"])(
6
+ "retains a tool named %s",
7
+ (toolName) => {
8
+ const registry = new ModelContextRegistry();
9
+ registry.addTool({ toolName });
10
+
11
+ const tools = registry.getModelContext().tools!;
12
+ expect(Object.hasOwn(tools, toolName)).toBe(true);
13
+ expect(Object.keys(tools)).toEqual([toolName]);
14
+ },
15
+ );
16
+
5
17
  it("notifies every subscriber and rethrows when a registration subscriber throws", () => {
6
18
  const registry = new ModelContextRegistry();
7
19
  const error = new Error("subscriber failed");
@@ -13,6 +13,7 @@ import type {
13
13
  ModelContextRegistryInstructionHandle,
14
14
  ModelContextRegistryProviderHandle,
15
15
  } from "./registry-handles";
16
+ import { nullProtoRecord } from "../utils/record";
16
17
 
17
18
  export class ModelContextRegistry implements ModelContextProvider {
18
19
  private _tools = new Map<symbol, AssistantToolProps<any, any>>();
@@ -34,7 +35,7 @@ export class ModelContextRegistry implements ModelContextProvider {
34
35
  const system =
35
36
  instructions.length > 0 ? instructions.join("\n\n") : undefined;
36
37
 
37
- const tools: Record<string, Tool<any, any>> = {};
38
+ const tools = nullProtoRecord<Tool<any, any>>();
38
39
  for (const toolProps of this._tools.values()) {
39
40
  const { toolName, render, ...tool } = toolProps;
40
41
  tools[toolName] = tool;
@@ -54,7 +55,7 @@ export class ModelContextRegistry implements ModelContextProvider {
54
55
  }
55
56
 
56
57
  if (providerContexts.tools) {
57
- context.tools = { ...(context.tools || {}), ...providerContexts.tools };
58
+ context.tools = nullProtoRecord(context.tools, providerContexts.tools);
58
59
  }
59
60
 
60
61
  if (providerContexts.callSettings) {
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mergeModelContexts } from "./types";
3
+
4
+ const provider = (tools: Record<string, any>, priority?: number) =>
5
+ ({ getModelContext: () => ({ tools, priority }) }) as any;
6
+
7
+ describe("mergeModelContexts", () => {
8
+ it.each(["__proto__", "constructor", "toString"])(
9
+ "retains and prioritizes a tool named %s",
10
+ (name) => {
11
+ const highPriorityTool = { description: "high", parameters: {} };
12
+ const lowPriorityTool = { description: "low", parameters: {} };
13
+ const otherTool = { description: "other", parameters: {} };
14
+ const result = mergeModelContexts(
15
+ new Set([
16
+ provider({ [name]: highPriorityTool, ok: otherTool }, 1),
17
+ provider({ [name]: lowPriorityTool }, 0),
18
+ ]),
19
+ );
20
+
21
+ expect(Object.keys(result.tools ?? {})).toEqual([name, "ok"]);
22
+ expect(result.tools?.[name]?.description).toBe("high");
23
+ },
24
+ );
25
+ });
@@ -1,5 +1,6 @@
1
1
  import type { Unsubscribe } from "../types/unsubscribe";
2
2
  import type { Tool } from "assistant-stream";
3
+ import { nullProtoRecord } from "../utils/record";
3
4
 
4
5
  export type LanguageModelV1CallSettings = {
5
6
  maxTokens?: number;
@@ -68,7 +69,7 @@ export const mergeModelContexts = (
68
69
  .map((c) => c.getModelContext())
69
70
  .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
70
71
 
71
- const toolPriorities: Record<string, number> = {};
72
+ const toolPriorities = nullProtoRecord<number>();
72
73
 
73
74
  return configs.reduce((acc, config) => {
74
75
  const priority = config.priority ?? 0;
@@ -81,7 +82,9 @@ export const mergeModelContexts = (
81
82
  }
82
83
  if (config.tools) {
83
84
  for (const [name, tool] of Object.entries(config.tools)) {
84
- const existing = acc.tools?.[name];
85
+ const hasExisting =
86
+ acc.tools !== undefined && Object.hasOwn(acc.tools, name);
87
+ const existing = hasExisting ? acc.tools![name] : undefined;
85
88
  if (existing && existing !== tool) {
86
89
  const existingPriority = toolPriorities[name]!;
87
90
  if (existingPriority === priority) {
@@ -106,9 +109,11 @@ export const mergeModelContexts = (
106
109
  continue;
107
110
  }
108
111
 
109
- if (!acc.tools) acc.tools = {};
112
+ if (!acc.tools) acc.tools = nullProtoRecord();
110
113
  acc.tools[name] = stripOverwrite(tool);
111
- toolPriorities[name] ??= priority;
114
+ if (!Object.hasOwn(toolPriorities, name)) {
115
+ toolPriorities[name] = priority;
116
+ }
112
117
  }
113
118
  }
114
119
  if (config.config) {
@@ -69,6 +69,40 @@ describe("RuntimeAdapter via the neutral store entry", () => {
69
69
  handle.destroy();
70
70
  });
71
71
 
72
+ it("emits composer.send for a thread.append, flagging a suggestion", async () => {
73
+ const { runtime, onNew } = createRuntime({
74
+ suggestions: [{ prompt: "hi there" }],
75
+ });
76
+ const handle = createAssistantClient(
77
+ AuiConfig({ threads: RuntimeAdapter(runtime) }),
78
+ );
79
+ handle.subscribe(() => {});
80
+ const aui = handle.getClient();
81
+ const sent = vi.fn();
82
+ aui.on({ scope: "thread", event: "composer.send" }, sent);
83
+
84
+ flushTapSync(() =>
85
+ aui.thread.append({ content: [{ type: "text", text: "hi there" }] }),
86
+ );
87
+ flushTapSync(() => aui.thread.append("plain text"));
88
+ await new Promise((resolve) => setTimeout(resolve));
89
+
90
+ expect(onNew).toHaveBeenCalledTimes(2);
91
+ expect(sent).toHaveBeenCalledTimes(2);
92
+ expect(sent.mock.calls[0]![0]).toMatchObject({
93
+ chars: 8,
94
+ attachments: 0,
95
+ suggestion: true,
96
+ });
97
+ expect(sent.mock.calls[1]![0]).toMatchObject({
98
+ chars: 10,
99
+ attachments: 0,
100
+ });
101
+ expect(sent.mock.calls[1]![0]).not.toHaveProperty("suggestion");
102
+
103
+ handle.destroy();
104
+ });
105
+
72
106
  it("registers the client's model context on the runtime", () => {
73
107
  const { runtime } = createRuntime();
74
108
  const register = vi.spyOn(runtime, "registerModelContextProvider");
@@ -209,6 +209,7 @@ const useInMemoryThreadList = (
209
209
  mainThreadId,
210
210
  newThreadId: null,
211
211
  isLoading: false,
212
+ loadError: undefined,
212
213
  isLoadingMore: false,
213
214
  hasMore: false,
214
215
  threadIds: regularThreads.map((t) => t.id),
@@ -210,6 +210,34 @@ describe("Interactables registration", () => {
210
210
  expect(stateOf(root, "n1")).toEqual({ v: 7 });
211
211
  });
212
212
 
213
+ // `definitions` is read by bare key with ids that reach the runtime from
214
+ // model tool calls, so it must stay prototype-free through every transition.
215
+ it.each(["__proto__", "constructor", "toString"])(
216
+ "registers, updates and unregisters an interactable named %s",
217
+ (id) => {
218
+ root = mount();
219
+ const unregister = root.getValue().register(reg(id));
220
+ expect(stateOf(root, id)).toEqual({ v: 0 });
221
+
222
+ root.getValue().setState(id, () => ({ v: 1 }));
223
+ expect(stateOf(root, id)).toEqual({ v: 1 });
224
+ expect(Object.keys(root.getValue().getState().definitions)).toEqual([id]);
225
+
226
+ unregister();
227
+ expect(Object.keys(root.getValue().getState().definitions)).toEqual([]);
228
+ },
229
+ );
230
+
231
+ it("keeps the state records prototype-free", () => {
232
+ root = mount();
233
+ root.getValue().register(reg("n1"));
234
+ root.getValue().setState("n1", () => ({ v: 1 }));
235
+
236
+ const state = root.getValue().getState();
237
+ expect(Object.getPrototypeOf(state.definitions)).toBeNull();
238
+ expect(Object.getPrototypeOf(state.persistence)).toBeNull();
239
+ });
240
+
213
241
  it("restores detached state when an instance re-registers in-session", async () => {
214
242
  root = mount();
215
243
  const unregister = root.getValue().register(reg("n1"));
@@ -25,6 +25,7 @@ import {
25
25
  } from "../../model-context/interactable-composer-metadata";
26
26
  import { notifySubscribers as notifyStateSubscribers } from "../../subscribable/subscribable";
27
27
  import { useInteractablePersistenceQueue } from "../interactables-shared/useInteractablePersistenceQueue";
28
+ import { nullProtoRecord } from "../../utils/record";
28
29
 
29
30
  type RestorePersistedStateOptions = {
30
31
  stash: Map<string, unknown>;
@@ -77,8 +78,8 @@ const useInteractablesResource = ({
77
78
  persistence,
78
79
  }: Unstable_InteractablesConfig = {}): ClientOutput<"unstable_interactables"> => {
79
80
  const [state, setState] = useState<Unstable_InteractablesState>(() => ({
80
- definitions: {},
81
- persistence: {},
81
+ definitions: nullProtoRecord(),
82
+ persistence: nullProtoRecord(),
82
83
  }));
83
84
 
84
85
  const clientRef = useAssistantClientRef();
@@ -123,7 +124,8 @@ const useInteractablesResource = ({
123
124
  );
124
125
 
125
126
  const exportState = useCallback((): Unstable_InteractablePersistedState => {
126
- const result: Unstable_InteractablePersistedState = {};
127
+ const result =
128
+ nullProtoRecord<Unstable_InteractablePersistedState[string]>();
127
129
  for (const [id, def] of Object.entries(stateRef.current.definitions)) {
128
130
  if (def.scope === "thread") continue; // thread items persist via snapshot, not the adapter
129
131
  result[id] = { name: def.name, state: def.state };
@@ -167,7 +169,7 @@ const useInteractablesResource = ({
167
169
  }
168
170
  setStateAndRef((prev) => {
169
171
  let changed = false;
170
- const definitions = { ...prev.definitions };
172
+ const definitions = nullProtoRecord(prev.definitions);
171
173
  for (const [id, entry] of Object.entries(saved)) {
172
174
  const def = definitions[id];
173
175
  if (!def || !shouldApply(id, def)) continue;
@@ -260,10 +262,9 @@ const useInteractablesResource = ({
260
262
  if (!existing) return prev;
261
263
  return {
262
264
  ...prev,
263
- definitions: {
264
- ...prev.definitions,
265
+ definitions: nullProtoRecord(prev.definitions, {
265
266
  [id]: { ...existing, state: updater(existing.state) },
266
- },
267
+ }),
267
268
  };
268
269
  });
269
270
  if (stateRef.current.definitions[id]?.scope !== "thread") {
@@ -450,8 +451,7 @@ const useInteractablesResource = ({
450
451
 
451
452
  setStateAndRef((prev) => ({
452
453
  ...prev,
453
- definitions: {
454
- ...prev.definitions,
454
+ definitions: nullProtoRecord(prev.definitions, {
455
455
  [def.id]: {
456
456
  id: def.id,
457
457
  name: def.name,
@@ -466,7 +466,7 @@ const useInteractablesResource = ({
466
466
  loaded ??
467
467
  def.initialState,
468
468
  },
469
- },
469
+ }),
470
470
  }));
471
471
 
472
472
  return () => {
@@ -498,9 +498,11 @@ const useInteractablesResource = ({
498
498
  }
499
499
  }
500
500
  partialSchemaCacheRef.current.delete(def.id);
501
- const { [def.id]: _, ...rest } = prev.definitions;
502
- const { [def.id]: __, ...restPersistence } = prev.persistence;
503
- return { ...prev, definitions: rest, persistence: restPersistence };
501
+ const definitions = nullProtoRecord(prev.definitions);
502
+ const persistence = nullProtoRecord(prev.persistence);
503
+ delete definitions[def.id];
504
+ delete persistence[def.id];
505
+ return { ...prev, definitions, persistence };
504
506
  });
505
507
  };
506
508
  },