@assistant-ui/core 0.3.16 → 0.3.17

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 (305) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +2 -2
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +2 -1
  5. package/dist/internal.js +2 -1
  6. package/dist/model-context/frame/provider.d.ts +3 -0
  7. package/dist/model-context/frame/provider.d.ts.map +1 -1
  8. package/dist/model-context/frame/provider.js +71 -21
  9. package/dist/model-context/frame/provider.js.map +1 -1
  10. package/dist/model-context/tool-call-text.d.ts +26 -0
  11. package/dist/model-context/tool-call-text.d.ts.map +1 -0
  12. package/dist/model-context/tool-call-text.js +18 -0
  13. package/dist/model-context/tool-call-text.js.map +1 -0
  14. package/dist/react/RuntimeAdapter.js +1 -1
  15. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  16. package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -1
  17. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  18. package/dist/react/client/AdaptedRemoteThread.js +1 -1
  19. package/dist/react/client/InMemoryThreadList.js +1 -1
  20. package/dist/react/client/Interactables.js +1 -1
  21. package/dist/react/client/RemoteThreadList.d.ts +15 -0
  22. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  23. package/dist/react/client/RemoteThreadList.js +240 -138
  24. package/dist/react/client/RemoteThreadList.js.map +1 -1
  25. package/dist/react/client/Tools.d.ts.map +1 -1
  26. package/dist/react/client/Tools.js +6 -2
  27. package/dist/react/client/Tools.js.map +1 -1
  28. package/dist/react/index.d.ts +2 -1
  29. package/dist/react/index.js +2 -1
  30. package/dist/react/interactables-legacy/Interactables.js +4 -4
  31. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  32. package/dist/react/model-context/toolbox.d.ts +2 -14
  33. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  34. package/dist/react/model-context/toolbox.js +1 -13
  35. package/dist/react/model-context/toolbox.js.map +1 -1
  36. package/dist/react/model-context/useAssistantContext.js +37 -7
  37. package/dist/react/model-context/useAssistantContext.js.map +1 -1
  38. package/dist/react/model-context/useAuiToolOverrides.js +31 -5
  39. package/dist/react/model-context/useAuiToolOverrides.js.map +1 -1
  40. package/dist/react/model-context/useInlineRender.d.ts.map +1 -1
  41. package/dist/react/model-context/useInlineRender.js +10 -36
  42. package/dist/react/model-context/useInlineRender.js.map +1 -1
  43. package/dist/react/primitive-hooks/useMessageError.d.ts.map +1 -1
  44. package/dist/react/primitive-hooks/useMessageError.js +2 -8
  45. package/dist/react/primitive-hooks/useMessageError.js.map +1 -1
  46. package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts.map +1 -1
  47. package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -4
  48. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  49. package/dist/react/primitives/composer/ComposerAttachments.d.ts.map +1 -1
  50. package/dist/react/primitives/composer/ComposerAttachments.js +4 -3
  51. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  52. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  53. package/dist/react/primitives/composer/ComposerQueue.js +4 -3
  54. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  55. package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
  56. package/dist/react/primitives/message/MessageAttachments.js +7 -6
  57. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  58. package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
  59. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  60. package/dist/react/primitives/message/MessageParts.js +3 -3
  61. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  62. package/dist/react/primitives/threadList/ThreadListItems.d.ts.map +1 -1
  63. package/dist/react/primitives/threadList/ThreadListItems.js +5 -4
  64. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  65. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +6 -4
  66. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  67. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +44 -22
  68. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  69. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -5
  70. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  71. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +79 -62
  72. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  73. package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -6
  74. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  75. package/dist/react/runtimes/RemoteThreadResource.js +3 -3
  76. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  77. package/dist/react/runtimes/assistant-transport/useLatestRef.js +2 -2
  78. package/dist/react/runtimes/assistant-transport/useLatestRef.js.map +1 -1
  79. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  80. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +23 -18
  81. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  82. package/dist/react/runtimes/cloud/auiV0.d.ts +5 -1
  83. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  84. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  85. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +29 -0
  86. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -0
  87. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +159 -0
  88. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -0
  89. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +1 -9
  90. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  91. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +16 -153
  92. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  93. package/dist/react/runtimes/external-message-converter.js +1 -1
  94. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  95. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  96. package/dist/react/runtimes/useRemoteThreadListRuntime.js +14 -6
  97. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  98. package/dist/react/types/MessagePartComponentTypes.d.ts +7 -3
  99. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  100. package/dist/react/types/scopes/tools.d.ts +2 -0
  101. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  102. package/dist/runtime/api/message-part-runtime.d.ts +2 -2
  103. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  104. package/dist/runtime/api/message-part-runtime.js +1 -1
  105. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  106. package/dist/runtime/api/thread-list-runtime.d.ts +9 -1
  107. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  108. package/dist/runtime/api/thread-list-runtime.js +5 -0
  109. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  110. package/dist/runtime/api/thread-runtime.d.ts +1 -1
  111. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -2
  112. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  113. package/dist/runtime/base/base-thread-runtime-core.js +2 -2
  114. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  115. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +14 -2
  116. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  117. package/dist/runtime/interfaces/thread-runtime-core.d.ts +15 -1
  118. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  119. package/dist/runtime/utils/auto-status.d.ts +1 -1
  120. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  121. package/dist/runtime/utils/auto-status.js +6 -2
  122. package/dist/runtime/utils/auto-status.js.map +1 -1
  123. package/dist/runtime/utils/external-message-conversion.d.ts +3 -2
  124. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  125. package/dist/runtime/utils/external-message-conversion.js +4 -3
  126. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  127. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts +6 -3
  128. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts.map +1 -1
  129. package/dist/runtime/utils/resolveToolApprovalResponse.js +12 -3
  130. package/dist/runtime/utils/resolveToolApprovalResponse.js.map +1 -1
  131. package/dist/runtime/utils/thread-message-like.d.ts +2 -10
  132. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  133. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  134. package/dist/runtime/utils/tool-call-tree.d.ts +39 -0
  135. package/dist/runtime/utils/tool-call-tree.d.ts.map +1 -0
  136. package/dist/runtime/utils/tool-call-tree.js +85 -0
  137. package/dist/runtime/utils/tool-call-tree.js.map +1 -0
  138. package/dist/runtimes/external-store/external-store-adapter.d.ts +29 -2
  139. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  140. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -1
  141. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  142. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +34 -28
  143. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  144. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +1 -1
  145. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -1
  146. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -1
  147. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
  148. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  149. package/dist/runtimes/local/local-thread-runtime-core.js +3 -1
  150. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  151. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +1 -1
  152. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  153. package/dist/runtimes/remote-thread-list/remote-thread-state.js +30 -13
  154. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  155. package/dist/runtimes/remote-thread-list/title.d.ts +9 -0
  156. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -0
  157. package/dist/runtimes/remote-thread-list/title.js +6 -0
  158. package/dist/runtimes/remote-thread-list/title.js.map +1 -0
  159. package/dist/runtimes/remote-thread-list/types.d.ts +6 -5
  160. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  161. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +31 -8
  162. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  163. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +97 -60
  164. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  165. package/dist/store/clients/external-thread.d.ts +1 -1
  166. package/dist/store/clients/external-thread.d.ts.map +1 -1
  167. package/dist/store/clients/external-thread.js +11 -4
  168. package/dist/store/clients/external-thread.js.map +1 -1
  169. package/dist/store/clients/single-thread-list.js +1 -1
  170. package/dist/store/clients/suggestions.d.ts.map +1 -1
  171. package/dist/store/clients/suggestions.js +18 -6
  172. package/dist/store/clients/suggestions.js.map +1 -1
  173. package/dist/store/clients/thread-message-client.js +1 -1
  174. package/dist/store/clients/thread-selection-events.js +1 -1
  175. package/dist/store/index.d.ts +2 -1
  176. package/dist/store/index.js +2 -1
  177. package/dist/store/internal.d.ts +2 -2
  178. package/dist/store/internal.js +2 -2
  179. package/dist/store/primitive-predicates.d.ts +4 -1
  180. package/dist/store/primitive-predicates.d.ts.map +1 -1
  181. package/dist/store/primitive-predicates.js +9 -1
  182. package/dist/store/primitive-predicates.js.map +1 -1
  183. package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
  184. package/dist/store/runtime-clients/message-runtime-client.js +1 -1
  185. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  186. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  187. package/dist/store/runtime-clients/thread-list-runtime-client.js +124 -105
  188. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  189. package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
  190. package/dist/store/runtime-clients/useSubscribable.d.ts +3 -1
  191. package/dist/store/runtime-clients/useSubscribable.d.ts.map +1 -1
  192. package/dist/store/runtime-clients/useSubscribable.js +1 -1
  193. package/dist/store/runtime-clients/useSubscribable.js.map +1 -1
  194. package/dist/store/scopes/part.d.ts +6 -3
  195. package/dist/store/scopes/part.d.ts.map +1 -1
  196. package/dist/subscribable/subscribable.d.ts +8 -1
  197. package/dist/subscribable/subscribable.d.ts.map +1 -1
  198. package/dist/subscribable/subscribable.js +24 -16
  199. package/dist/subscribable/subscribable.js.map +1 -1
  200. package/dist/types/message.d.ts +40 -1
  201. package/dist/types/message.d.ts.map +1 -1
  202. package/dist/types/message.js +7 -1
  203. package/dist/types/message.js.map +1 -1
  204. package/package.json +12 -17
  205. package/src/index.ts +6 -1
  206. package/src/internal.ts +8 -0
  207. package/src/model-context/frame/provider.test.ts +217 -1
  208. package/src/model-context/frame/provider.ts +111 -23
  209. package/src/model-context/tool-call-text.test.ts +67 -0
  210. package/src/model-context/tool-call-text.ts +67 -0
  211. package/src/react/AssistantRuntimeProvider.test.tsx +66 -2
  212. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +1 -3
  213. package/src/react/client/RemoteThreadList.background.test.ts +384 -0
  214. package/src/react/client/RemoteThreadList.concurrent.test.tsx +274 -0
  215. package/src/react/client/RemoteThreadList.test.ts +273 -1
  216. package/src/react/client/RemoteThreadList.ts +293 -83
  217. package/src/react/client/Tools.ts +7 -1
  218. package/src/react/index.ts +4 -0
  219. package/src/react/model-context/modelContextHooks.test.tsx +131 -0
  220. package/src/react/model-context/toolbox.test.ts +9 -9
  221. package/src/react/model-context/toolbox.ts +9 -36
  222. package/src/react/model-context/useAssistantContext.ts +4 -2
  223. package/src/react/model-context/useAuiToolOverrides.ts +4 -2
  224. package/src/react/model-context/useInlineRender.test.tsx +48 -0
  225. package/src/react/model-context/useInlineRender.ts +14 -10
  226. package/src/react/primitive-hooks/useMessageError.ts +2 -19
  227. package/src/react/primitive-hooks/useThreadListLoadMore.ts +2 -3
  228. package/src/react/primitives/composer/ComposerAttachments.test.tsx +76 -0
  229. package/src/react/primitives/composer/ComposerAttachments.tsx +9 -4
  230. package/src/react/primitives/composer/ComposerQueue.test.tsx +72 -0
  231. package/src/react/primitives/composer/ComposerQueue.tsx +7 -4
  232. package/src/react/primitives/message/MessageAttachments.test.tsx +107 -37
  233. package/src/react/primitives/message/MessageAttachments.tsx +10 -7
  234. package/src/react/primitives/message/MessageGroupedParts.tsx +2 -2
  235. package/src/react/primitives/message/MessageParts.tsx +3 -3
  236. package/src/react/primitives/threadList/ThreadListItems.test.tsx +99 -0
  237. package/src/react/primitives/threadList/ThreadListItems.tsx +8 -5
  238. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +94 -1
  239. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +72 -31
  240. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +169 -0
  241. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +114 -68
  242. package/src/react/runtimes/RemoteThreadResource.ts +1 -4
  243. package/src/react/runtimes/assistant-transport/transport-scheduling.test.ts +51 -2
  244. package/src/react/runtimes/assistant-transport/useLatestRef.ts +2 -2
  245. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +45 -0
  246. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +65 -1
  247. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +42 -35
  248. package/src/react/runtimes/cloud/auiV0.ts +5 -0
  249. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +88 -0
  250. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +236 -0
  251. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +90 -1
  252. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +33 -238
  253. package/src/react/runtimes/external-message-converter.ts +1 -0
  254. package/src/react/runtimes/useRemoteThreadListRuntime.concurrent.test.tsx +216 -0
  255. package/src/react/runtimes/useRemoteThreadListRuntime.ts +24 -6
  256. package/src/react/types/MessagePartComponentTypes.ts +7 -3
  257. package/src/react/types/scopes/tools.ts +2 -0
  258. package/src/runtime/api/message-part-runtime.ts +3 -3
  259. package/src/runtime/api/thread-list-runtime.ts +25 -1
  260. package/src/runtime/base/base-thread-runtime-core.test.ts +94 -3
  261. package/src/runtime/base/base-thread-runtime-core.ts +10 -4
  262. package/src/runtime/interfaces/thread-list-runtime-core.ts +20 -1
  263. package/src/runtime/interfaces/thread-runtime-core.ts +17 -1
  264. package/src/runtime/utils/auto-status.test.ts +57 -0
  265. package/src/runtime/utils/auto-status.ts +13 -1
  266. package/src/runtime/utils/external-message-conversion.test.ts +97 -0
  267. package/src/runtime/utils/external-message-conversion.ts +17 -1
  268. package/src/runtime/utils/resolveToolApprovalResponse.test.ts +73 -0
  269. package/src/runtime/utils/resolveToolApprovalResponse.ts +27 -6
  270. package/src/runtime/utils/thread-message-like.ts +2 -10
  271. package/src/runtime/utils/tool-call-tree.test.ts +191 -0
  272. package/src/runtime/utils/tool-call-tree.ts +97 -0
  273. package/src/runtimes/external-store/external-store-adapter.ts +38 -2
  274. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +56 -33
  275. package/src/runtimes/inert/InertThreadRuntimeCore.ts +1 -1
  276. package/src/runtimes/local/local-thread-runtime-core.test.ts +29 -3
  277. package/src/runtimes/local/local-thread-runtime-core.ts +5 -1
  278. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +112 -0
  279. package/src/runtimes/remote-thread-list/remote-thread-state.ts +55 -16
  280. package/src/runtimes/remote-thread-list/title.ts +3 -0
  281. package/src/runtimes/remote-thread-list/types.ts +6 -5
  282. package/src/runtimes/tool-invocations/EDGE_CASES.md +110 -5
  283. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +750 -136
  284. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +144 -91
  285. package/src/store/clients/external-thread.ts +14 -8
  286. package/src/store/clients/suggestions.test.ts +155 -1
  287. package/src/store/clients/suggestions.ts +42 -34
  288. package/src/store/index.ts +4 -0
  289. package/src/store/internal.ts +2 -0
  290. package/src/store/primitive-predicates.ts +35 -0
  291. package/src/store/runtime-clients/thread-list-runtime-client.ts +16 -2
  292. package/src/store/runtime-clients/useSubscribable.ts +8 -2
  293. package/src/store/scopes/part.ts +6 -3
  294. package/src/subscribable/subscribable.test.ts +50 -0
  295. package/src/subscribable/subscribable.ts +33 -21
  296. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +111 -0
  297. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +78 -0
  298. package/src/tests/RemoteThreadListThreadListRuntimeCore-reload.test.ts +52 -0
  299. package/src/tests/external-store-optimistic-id.test.ts +79 -0
  300. package/src/tests/external-store-repository-instance.test.ts +166 -0
  301. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +22 -16
  302. package/src/tests/external-thread-attachments.test.tsx +85 -1
  303. package/src/tests/remote-thread-list-adapter-switch.test.ts +66 -1
  304. package/src/tests/thread-switch-events.test.tsx +397 -2
  305. package/src/types/message.ts +50 -2
@@ -76,6 +76,16 @@ const createAssistantMessage = (
76
76
  ],
77
77
  });
78
78
 
79
+ /**
80
+ * The stream failure that sets this flag cannot be provoked through the public
81
+ * API: `unstable_toolResultStream` turns a throwing tool into an error result
82
+ * rather than an error chunk, so the transform never rejects. The recovery
83
+ * branch it guards is reachable on its own.
84
+ */
85
+ const killPipeline = (tracker: ToolInvocationTracker) => {
86
+ (tracker as unknown as { _pipelineDead: boolean })._pipelineDead = true;
87
+ };
88
+
79
89
  describe("ToolInvocationTracker", () => {
80
90
  it("does not crash and does not re-fire streamCall when tool argsText regresses mid-stream", async () => {
81
91
  // The tracker holds the contract: streamCall fires exactly once per
@@ -237,6 +247,96 @@ describe("ToolInvocationTracker", () => {
237
247
  }
238
248
  });
239
249
 
250
+ it("keeps a human-input interrupt that execute requests before its first await", async () => {
251
+ const execute = vi.fn(async (_args, { human }) => ({
252
+ approved: (await human({ request: "approve" })) === true,
253
+ }));
254
+ const getTools = () => ({
255
+ weatherSearch: {
256
+ parameters: { type: "object", properties: {} },
257
+ execute,
258
+ } satisfies Tool,
259
+ });
260
+ const onResult = vi.fn();
261
+ let statuses: Record<string, ToolExecutionStatus> = {};
262
+ const onStatusesChange = (s: ReadonlyMap<string, ToolExecutionStatus>) => {
263
+ statuses = Object.fromEntries(s);
264
+ };
265
+
266
+ const tracker = new ToolInvocationTracker(getTools, {
267
+ onResult,
268
+ onStatusesChange,
269
+ });
270
+ tracker.setState(createState([], false));
271
+ tracker.setState(
272
+ createState(
273
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
274
+ false,
275
+ ),
276
+ );
277
+
278
+ await waitFor(() => {
279
+ expect(statuses["tool-1"]?.type).toBe("interrupt");
280
+ });
281
+
282
+ expect(tracker.resume("tool-1", true)).toBe(true);
283
+
284
+ await waitFor(() => {
285
+ expect(onResult).toHaveBeenCalledWith(
286
+ expect.objectContaining({
287
+ toolCallId: "tool-1",
288
+ result: { approved: true },
289
+ }),
290
+ );
291
+ });
292
+ expect(statuses).toEqual({});
293
+ });
294
+
295
+ it("marks a fresh execution as executing when an earlier one left a human-input request behind", async () => {
296
+ const execute = vi
297
+ .fn()
298
+ .mockImplementationOnce((_args, { human }) =>
299
+ human({ request: "approve" }),
300
+ )
301
+ .mockImplementationOnce(() => new Promise(() => {}));
302
+ const getTools = () => ({
303
+ weatherSearch: {
304
+ parameters: { type: "object", properties: {} },
305
+ execute,
306
+ } satisfies Tool,
307
+ });
308
+ let statuses: Record<string, ToolExecutionStatus> = {};
309
+ const tracker = new ToolInvocationTracker(getTools, {
310
+ onResult: vi.fn(),
311
+ onStatusesChange: (s: ReadonlyMap<string, ToolExecutionStatus>) => {
312
+ statuses = Object.fromEntries(s);
313
+ },
314
+ });
315
+ tracker.setState(createState([], false));
316
+ tracker.setState(
317
+ createState(
318
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
319
+ false,
320
+ ),
321
+ );
322
+ await waitFor(() => {
323
+ expect(statuses["tool-1"]?.type).toBe("interrupt");
324
+ });
325
+
326
+ killPipeline(tracker);
327
+ tracker.setState(
328
+ createState(
329
+ [createAssistantMessage('{"query":"Paris"}', { query: "Paris" })],
330
+ false,
331
+ ),
332
+ );
333
+
334
+ await waitFor(() => {
335
+ expect(execute).toHaveBeenCalledTimes(2);
336
+ expect(statuses["tool-1"]?.type).toBe("executing");
337
+ });
338
+ });
339
+
240
340
  it("does not auto-submit a parse-error result for a non-executable tool whose divergent argsText closes", async () => {
241
341
  // Same close-gating mismatch as the executable case, but for a tool with
242
342
  // no frontend execute. Closing on the divergent complete snapshot would
@@ -306,19 +406,21 @@ describe("ToolInvocationTracker", () => {
306
406
  onResult,
307
407
  onStatusesChange,
308
408
  });
309
- tracker.setState(createState([]));
409
+ tracker.setState(createState([], false));
310
410
 
311
411
  // Single monotonic snapshot growing to a complete value triggers execute.
312
412
  tracker.setState(
313
- createState([
314
- createAssistantMessage('{"query":"London"', { query: "London" }),
315
- ]),
413
+ createState(
414
+ [createAssistantMessage('{"query":"London"', { query: "London" })],
415
+ false,
416
+ ),
316
417
  );
317
418
 
318
419
  tracker.setState(
319
- createState([
320
- createAssistantMessage('{"query":"London"}', { query: "London" }),
321
- ]),
420
+ createState(
421
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
422
+ false,
423
+ ),
322
424
  );
323
425
 
324
426
  await waitFor(() => {
@@ -394,20 +496,424 @@ describe("ToolInvocationTracker", () => {
394
496
  { toolName: "deleteFile", approval },
395
497
  );
396
498
 
397
- tracker.setState(createState([gated({ id: "approval-1" })], false));
499
+ tracker.setState(createState([gated({ id: "approval-1" })], false));
500
+ await new Promise((r) => setTimeout(r, 0));
501
+ expect(execute).not.toHaveBeenCalled();
502
+
503
+ tracker.setState(
504
+ createState([gated({ id: "approval-1", approved: true })], true),
505
+ );
506
+ await new Promise((r) => setTimeout(r, 0));
507
+
508
+ expect(execute).not.toHaveBeenCalled();
509
+ expect(onResult).not.toHaveBeenCalled();
510
+ });
511
+
512
+ it("stops executing a live tool call once a provider approval lands", async () => {
513
+ const execute = vi.fn(async () => ({ deleted: true }));
514
+ const getTools = () => ({
515
+ deleteFile: {
516
+ parameters: { type: "object", properties: {} },
517
+ execute,
518
+ } satisfies Tool,
519
+ });
520
+ const onResult = vi.fn();
521
+ const tracker = new ToolInvocationTracker(getTools, {
522
+ onResult,
523
+ onStatusesChange: () => {},
524
+ });
525
+ tracker.setState(createState([]));
526
+
527
+ tracker.setState(
528
+ createState(
529
+ [
530
+ createAssistantMessage(
531
+ '{"path":"/tmp',
532
+ {},
533
+ { toolName: "deleteFile" },
534
+ ),
535
+ ],
536
+ true,
537
+ ),
538
+ );
539
+ tracker.setState(
540
+ createState(
541
+ [
542
+ createAssistantMessage(
543
+ '{"path":"/tmp/a"}',
544
+ { path: "/tmp/a" },
545
+ { toolName: "deleteFile", approval: { id: "approval-1" } },
546
+ ),
547
+ ],
548
+ true,
549
+ ),
550
+ );
551
+ await new Promise((r) => setTimeout(r, 0));
552
+
553
+ expect(execute).not.toHaveBeenCalled();
554
+ expect(onResult).not.toHaveBeenCalled();
555
+ });
556
+
557
+ it("drops the result of an in-flight execute once a provider approval lands", async () => {
558
+ let resolveExecute!: (value: { deleted: boolean }) => void;
559
+ const execute = vi.fn(
560
+ () =>
561
+ new Promise<{ deleted: boolean }>((r) => {
562
+ resolveExecute = r;
563
+ }),
564
+ );
565
+ const getTools = () => ({
566
+ deleteFile: {
567
+ parameters: { type: "object", properties: {} },
568
+ execute,
569
+ } satisfies Tool,
570
+ });
571
+ const onResult = vi.fn();
572
+ const tracker = new ToolInvocationTracker(
573
+ getTools,
574
+ { onResult, onStatusesChange: () => {} },
575
+ () => true,
576
+ );
577
+ tracker.setState(createState([]));
578
+
579
+ tracker.setState(
580
+ createState(
581
+ [
582
+ createAssistantMessage(
583
+ '{"path":"/tmp/a"}',
584
+ { path: "/tmp/a" },
585
+ {
586
+ toolName: "deleteFile",
587
+ },
588
+ ),
589
+ ],
590
+ true,
591
+ ),
592
+ );
593
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
594
+
595
+ tracker.setState(
596
+ createState(
597
+ [
598
+ createAssistantMessage(
599
+ '{"path":"/tmp/a"}',
600
+ { path: "/tmp/a" },
601
+ {
602
+ toolName: "deleteFile",
603
+ approval: { id: "approval-1" },
604
+ },
605
+ ),
606
+ ],
607
+ true,
608
+ ),
609
+ );
610
+ resolveExecute({ deleted: true });
611
+ await new Promise((r) => setTimeout(r, 0));
612
+
613
+ expect(onResult).not.toHaveBeenCalled();
614
+ });
615
+
616
+ it("does not execute a registered tool while the provider's run is open", async () => {
617
+ const execute = vi.fn(async () => ({ deleted: true }));
618
+ const getTools = () => ({
619
+ deleteFile: {
620
+ parameters: { type: "object", properties: {} },
621
+ execute,
622
+ } satisfies Tool,
623
+ });
624
+ const onResult = vi.fn();
625
+ const tracker = new ToolInvocationTracker(getTools, {
626
+ onResult,
627
+ onStatusesChange: () => {},
628
+ });
629
+ tracker.setState(createState([]));
630
+
631
+ const complete = (isRunning: boolean) =>
632
+ createState(
633
+ [
634
+ createAssistantMessage(
635
+ '{"path":"/tmp/a"}',
636
+ { path: "/tmp/a" },
637
+ { toolName: "deleteFile" },
638
+ ),
639
+ ],
640
+ isRunning,
641
+ );
642
+
643
+ tracker.setState(complete(true));
644
+ await new Promise((r) => setTimeout(r, 0));
645
+ expect(execute).not.toHaveBeenCalled();
646
+
647
+ tracker.setState(complete(false));
648
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
649
+ await waitFor(() => expect(onResult).toHaveBeenCalledTimes(1));
650
+ });
651
+
652
+ it("streams a streamCall tool's args while the run is open and executes it after", async () => {
653
+ const streamed: unknown[] = [];
654
+ const streamCall = vi.fn(
655
+ async (reader: {
656
+ args: { streamValues: () => AsyncIterable<unknown> };
657
+ }) => {
658
+ for await (const partial of reader.args.streamValues())
659
+ streamed.push(partial);
660
+ },
661
+ );
662
+ const execute = vi.fn(async () => ({ deleted: true }));
663
+ const getTools = () => ({
664
+ deleteFile: {
665
+ parameters: { type: "object", properties: {} },
666
+ streamCall,
667
+ execute,
668
+ } satisfies Tool,
669
+ });
670
+ const onResult = vi.fn();
671
+ const tracker = new ToolInvocationTracker(getTools, {
672
+ onResult,
673
+ onStatusesChange: () => {},
674
+ });
675
+ tracker.setState(createState([], false));
676
+
677
+ tracker.setState(
678
+ createState([
679
+ createAssistantMessage(
680
+ '{"path":"/tmp/a"',
681
+ { path: "/tmp/a" },
682
+ {
683
+ toolName: "deleteFile",
684
+ },
685
+ ),
686
+ ]),
687
+ );
688
+ tracker.setState(
689
+ createState([
690
+ createAssistantMessage(
691
+ '{"path":"/tmp/a"}',
692
+ { path: "/tmp/a" },
693
+ {
694
+ toolName: "deleteFile",
695
+ },
696
+ ),
697
+ ]),
698
+ );
699
+
700
+ await waitFor(() => expect(streamCall).toHaveBeenCalledTimes(1));
701
+ await waitFor(() => expect(streamed.length).toBeGreaterThan(0));
702
+ expect(execute).not.toHaveBeenCalled();
703
+
704
+ tracker.setState(
705
+ createState(
706
+ [
707
+ createAssistantMessage(
708
+ '{"path":"/tmp/a"}',
709
+ { path: "/tmp/a" },
710
+ {
711
+ toolName: "deleteFile",
712
+ },
713
+ ),
714
+ ],
715
+ false,
716
+ ),
717
+ );
718
+
719
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
720
+ await waitFor(() => expect(onResult).toHaveBeenCalledTimes(1));
721
+ expect(streamCall).toHaveBeenCalledTimes(1);
722
+ });
723
+
724
+ it("never executes a registered tool that was waiting when the turn was aborted", async () => {
725
+ const execute = vi.fn(async () => ({ deleted: true }));
726
+ const getTools = () => ({
727
+ deleteFile: {
728
+ parameters: { type: "object", properties: {} },
729
+ execute,
730
+ } satisfies Tool,
731
+ });
732
+ const onResult = vi.fn();
733
+ const tracker = new ToolInvocationTracker(getTools, {
734
+ onResult,
735
+ onStatusesChange: () => {},
736
+ });
737
+ tracker.setState(createState([], false));
738
+
739
+ const complete = (isRunning: boolean) =>
740
+ createState(
741
+ [
742
+ createAssistantMessage(
743
+ '{"path":"/tmp/a"}',
744
+ { path: "/tmp/a" },
745
+ { toolName: "deleteFile" },
746
+ ),
747
+ ],
748
+ isRunning,
749
+ );
750
+
751
+ tracker.setState(complete(true));
752
+ await new Promise((r) => setTimeout(r, 0));
753
+ expect(execute).not.toHaveBeenCalled();
754
+
755
+ await tracker.abort({ discardPending: true });
756
+
757
+ tracker.setState(complete(false));
758
+ await new Promise((r) => setTimeout(r, 0));
759
+
760
+ expect(execute).not.toHaveBeenCalled();
761
+ expect(onResult).not.toHaveBeenCalled();
762
+ });
763
+
764
+ it("still executes a waiting tool when an abort only interrupts the turn", async () => {
765
+ const execute = vi.fn(async () => ({ deleted: true }));
766
+ const getTools = () => ({
767
+ deleteFile: {
768
+ parameters: { type: "object", properties: {} },
769
+ execute,
770
+ } satisfies Tool,
771
+ });
772
+ const onResult = vi.fn();
773
+ const tracker = new ToolInvocationTracker(getTools, {
774
+ onResult,
775
+ onStatusesChange: () => {},
776
+ });
777
+ tracker.setState(createState([], false));
778
+
779
+ const complete = (isRunning: boolean) =>
780
+ createState(
781
+ [
782
+ createAssistantMessage(
783
+ '{"path":"/tmp/a"}',
784
+ { path: "/tmp/a" },
785
+ { toolName: "deleteFile" },
786
+ ),
787
+ ],
788
+ isRunning,
789
+ );
790
+
791
+ tracker.setState(complete(true));
792
+ await new Promise((r) => setTimeout(r, 0));
793
+
794
+ await tracker.abort();
795
+
796
+ tracker.setState(complete(false));
797
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
798
+ });
799
+
800
+ it("keeps a discarded tool call skipped once its args complete", async () => {
801
+ const execute = vi.fn(async () => ({ deleted: true }));
802
+ const getTools = () => ({
803
+ deleteFile: {
804
+ parameters: { type: "object", properties: {} },
805
+ execute,
806
+ } satisfies Tool,
807
+ });
808
+ const onResult = vi.fn();
809
+ const tracker = new ToolInvocationTracker(getTools, {
810
+ onResult,
811
+ onStatusesChange: () => {},
812
+ });
813
+ tracker.setState(createState([], false));
814
+
815
+ const live = (argsText: string, isRunning: boolean) =>
816
+ createState(
817
+ [
818
+ createAssistantMessage(
819
+ argsText,
820
+ { path: "/tmp/a" },
821
+ { toolName: "deleteFile" },
822
+ ),
823
+ ],
824
+ isRunning,
825
+ );
826
+
827
+ tracker.setState(live('{"path":"/tm', true));
828
+ await new Promise((r) => setTimeout(r, 0));
829
+
830
+ await tracker.abort({ discardPending: true });
831
+
832
+ tracker.setState(live('{"path":"/tmp/a"}', false));
833
+ await new Promise((r) => setTimeout(r, 0));
834
+
835
+ expect(execute).not.toHaveBeenCalled();
836
+ expect(onResult).not.toHaveBeenCalled();
837
+ });
838
+
839
+ it("keeps a discarded call skipped when the pipeline restarts before it settles", async () => {
840
+ const execute = vi.fn(async () => ({ deleted: true }));
841
+ const getTools = () => ({
842
+ deleteFile: {
843
+ parameters: { type: "object", properties: {} },
844
+ execute,
845
+ } satisfies Tool,
846
+ });
847
+ const onResult = vi.fn();
848
+ const tracker = new ToolInvocationTracker(getTools, {
849
+ onResult,
850
+ onStatusesChange: () => {},
851
+ });
852
+ tracker.setState(createState([], false));
853
+
854
+ const complete = (isRunning: boolean) =>
855
+ createState(
856
+ [
857
+ createAssistantMessage(
858
+ '{"path":"/tmp/a"}',
859
+ { path: "/tmp/a" },
860
+ { toolName: "deleteFile" },
861
+ ),
862
+ ],
863
+ isRunning,
864
+ );
865
+
866
+ tracker.setState(complete(true));
867
+ await new Promise((r) => setTimeout(r, 0));
868
+ await tracker.abort({ discardPending: true });
869
+
870
+ killPipeline(tracker);
871
+ tracker.setState(complete(false));
872
+ await new Promise((r) => setTimeout(r, 0));
873
+
874
+ expect(execute).not.toHaveBeenCalled();
875
+ expect(onResult).not.toHaveBeenCalled();
876
+ });
877
+
878
+ it("still executes a waiting call when the pipeline restarts before it settles", async () => {
879
+ const execute = vi.fn(async () => ({ deleted: true }));
880
+ const getTools = () => ({
881
+ deleteFile: {
882
+ parameters: { type: "object", properties: {} },
883
+ execute,
884
+ } satisfies Tool,
885
+ });
886
+ const onResult = vi.fn();
887
+ const tracker = new ToolInvocationTracker(getTools, {
888
+ onResult,
889
+ onStatusesChange: () => {},
890
+ });
891
+ tracker.setState(createState([], false));
892
+
893
+ const complete = (isRunning: boolean) =>
894
+ createState(
895
+ [
896
+ createAssistantMessage(
897
+ '{"path":"/tmp/a"}',
898
+ { path: "/tmp/a" },
899
+ { toolName: "deleteFile" },
900
+ ),
901
+ ],
902
+ isRunning,
903
+ );
904
+
905
+ tracker.setState(complete(true));
398
906
  await new Promise((r) => setTimeout(r, 0));
399
907
  expect(execute).not.toHaveBeenCalled();
400
908
 
401
- tracker.setState(
402
- createState([gated({ id: "approval-1", approved: true })], true),
403
- );
404
- await new Promise((r) => setTimeout(r, 0));
909
+ killPipeline(tracker);
910
+ tracker.setState(complete(false));
405
911
 
406
- expect(execute).not.toHaveBeenCalled();
407
- expect(onResult).not.toHaveBeenCalled();
912
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
913
+ await waitFor(() => expect(onResult).toHaveBeenCalledTimes(1));
408
914
  });
409
915
 
410
- it("stops executing a live tool call once a provider approval lands", async () => {
916
+ it("never executes a registered tool the provider gates before its run ends", async () => {
411
917
  const execute = vi.fn(async () => ({ deleted: true }));
412
918
  const getTools = () => ({
413
919
  deleteFile: {
@@ -426,14 +932,16 @@ describe("ToolInvocationTracker", () => {
426
932
  createState(
427
933
  [
428
934
  createAssistantMessage(
429
- '{"path":"/tmp',
430
- {},
935
+ '{"path":"/tmp/a"}',
936
+ { path: "/tmp/a" },
431
937
  { toolName: "deleteFile" },
432
938
  ),
433
939
  ],
434
940
  true,
435
941
  ),
436
942
  );
943
+ await new Promise((r) => setTimeout(r, 0));
944
+
437
945
  tracker.setState(
438
946
  createState(
439
947
  [
@@ -443,7 +951,7 @@ describe("ToolInvocationTracker", () => {
443
951
  { toolName: "deleteFile", approval: { id: "approval-1" } },
444
952
  ),
445
953
  ],
446
- true,
954
+ false,
447
955
  ),
448
956
  );
449
957
  await new Promise((r) => setTimeout(r, 0));
@@ -452,14 +960,8 @@ describe("ToolInvocationTracker", () => {
452
960
  expect(onResult).not.toHaveBeenCalled();
453
961
  });
454
962
 
455
- it("drops the result of an in-flight execute once a provider approval lands", async () => {
456
- let resolveExecute!: (value: { deleted: boolean }) => void;
457
- const execute = vi.fn(
458
- () =>
459
- new Promise<{ deleted: boolean }>((r) => {
460
- resolveExecute = r;
461
- }),
462
- );
963
+ it("never executes a tool call the adapter reports as provider-owned", async () => {
964
+ const execute = vi.fn(async () => ({ deleted: true }));
463
965
  const getTools = () => ({
464
966
  deleteFile: {
465
967
  parameters: { type: "object", properties: {} },
@@ -467,10 +969,69 @@ describe("ToolInvocationTracker", () => {
467
969
  } satisfies Tool,
468
970
  });
469
971
  const onResult = vi.fn();
470
- const tracker = new ToolInvocationTracker(getTools, {
471
- onResult,
472
- onStatusesChange: () => {},
972
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
973
+
974
+ try {
975
+ const tracker = new ToolInvocationTracker(
976
+ getTools,
977
+ { onResult, onStatusesChange: () => {} },
978
+ () => false,
979
+ );
980
+ tracker.setState(createState([]));
981
+
982
+ tracker.setState(
983
+ createState(
984
+ [
985
+ createAssistantMessage(
986
+ '{"path":"/tmp/a"}',
987
+ { path: "/tmp/a" },
988
+ { toolName: "deleteFile" },
989
+ ),
990
+ ],
991
+ true,
992
+ ),
993
+ );
994
+ await new Promise((r) => setTimeout(r, 0));
995
+
996
+ expect(execute).not.toHaveBeenCalled();
997
+ expect(onResult).not.toHaveBeenCalled();
998
+ expect(warnSpy).toHaveBeenCalledTimes(1);
999
+
1000
+ tracker.setState(
1001
+ createState(
1002
+ [
1003
+ createAssistantMessage(
1004
+ '{"path":"/tmp/a"}',
1005
+ { path: "/tmp/a" },
1006
+ { toolName: "deleteFile", result: { server: "deleted" } },
1007
+ ),
1008
+ ],
1009
+ false,
1010
+ ),
1011
+ );
1012
+ await new Promise((r) => setTimeout(r, 0));
1013
+
1014
+ expect(execute).not.toHaveBeenCalled();
1015
+ expect(onResult).not.toHaveBeenCalled();
1016
+ } finally {
1017
+ warnSpy.mockRestore();
1018
+ }
1019
+ });
1020
+
1021
+ it("executes a tool call the adapter reports as client-owned", async () => {
1022
+ const execute = vi.fn(async () => ({ deleted: true }));
1023
+ const getTools = () => ({
1024
+ deleteFile: {
1025
+ parameters: { type: "object", properties: {} },
1026
+ execute,
1027
+ } satisfies Tool,
473
1028
  });
1029
+ const onResult = vi.fn();
1030
+ const tracker = new ToolInvocationTracker(
1031
+ getTools,
1032
+ { onResult, onStatusesChange: () => {} },
1033
+ (toolCall) => toolCall.toolCallId === "tool-1",
1034
+ );
474
1035
  tracker.setState(createState([]));
475
1036
 
476
1037
  tracker.setState(
@@ -479,35 +1040,59 @@ describe("ToolInvocationTracker", () => {
479
1040
  createAssistantMessage(
480
1041
  '{"path":"/tmp/a"}',
481
1042
  { path: "/tmp/a" },
482
- {
483
- toolName: "deleteFile",
484
- },
1043
+ { toolName: "deleteFile" },
485
1044
  ),
486
1045
  ],
487
1046
  true,
488
1047
  ),
489
1048
  );
490
1049
  await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
1050
+ await waitFor(() => expect(onResult).toHaveBeenCalledTimes(1));
1051
+ });
491
1052
 
492
- tracker.setState(
1053
+ it("keeps the result of a call the adapter stops reporting as client-owned mid-execution", async () => {
1054
+ let resolveExecute!: (value: { deleted: boolean }) => void;
1055
+ const execute = vi.fn(
1056
+ () =>
1057
+ new Promise<{ deleted: boolean }>((r) => {
1058
+ resolveExecute = r;
1059
+ }),
1060
+ );
1061
+ const getTools = () => ({
1062
+ deleteFile: {
1063
+ parameters: { type: "object", properties: {} },
1064
+ execute,
1065
+ } satisfies Tool,
1066
+ });
1067
+ const onResult = vi.fn();
1068
+ let clientOwned = true;
1069
+ const tracker = new ToolInvocationTracker(
1070
+ getTools,
1071
+ { onResult, onStatusesChange: () => {} },
1072
+ () => clientOwned,
1073
+ );
1074
+ tracker.setState(createState([]));
1075
+
1076
+ const live = () =>
493
1077
  createState(
494
1078
  [
495
1079
  createAssistantMessage(
496
1080
  '{"path":"/tmp/a"}',
497
1081
  { path: "/tmp/a" },
498
- {
499
- toolName: "deleteFile",
500
- approval: { id: "approval-1" },
501
- },
1082
+ { toolName: "deleteFile" },
502
1083
  ),
503
1084
  ],
504
1085
  true,
505
- ),
506
- );
1086
+ );
1087
+
1088
+ tracker.setState(live());
1089
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
1090
+
1091
+ clientOwned = false;
1092
+ tracker.setState(live());
507
1093
  resolveExecute({ deleted: true });
508
- await new Promise((r) => setTimeout(r, 0));
509
1094
 
510
- expect(onResult).not.toHaveBeenCalled();
1095
+ await waitFor(() => expect(onResult).toHaveBeenCalledTimes(1));
511
1096
  });
512
1097
 
513
1098
  it("does not re-execute asynchronously loaded resolved tool calls after reset", async () => {
@@ -525,12 +1110,13 @@ describe("ToolInvocationTracker", () => {
525
1110
  onResult,
526
1111
  onStatusesChange,
527
1112
  });
528
- tracker.setState(createState([]));
1113
+ tracker.setState(createState([], false));
529
1114
 
530
1115
  tracker.setState(
531
- createState([
532
- createAssistantMessage('{"query":"London"}', { query: "London" }),
533
- ]),
1116
+ createState(
1117
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
1118
+ false,
1119
+ ),
534
1120
  );
535
1121
 
536
1122
  await waitFor(() => {
@@ -541,16 +1127,19 @@ describe("ToolInvocationTracker", () => {
541
1127
 
542
1128
  await Promise.resolve();
543
1129
 
544
- tracker.setState(createState([]));
1130
+ tracker.setState(createState([], false));
545
1131
 
546
1132
  tracker.setState(
547
- createState([
548
- createAssistantMessage(
549
- '{"query":"London"}',
550
- { query: "London" },
551
- { result: { source: "history" } },
552
- ),
553
- ]),
1133
+ createState(
1134
+ [
1135
+ createAssistantMessage(
1136
+ '{"query":"London"}',
1137
+ { query: "London" },
1138
+ { result: { source: "history" } },
1139
+ ),
1140
+ ],
1141
+ false,
1142
+ ),
554
1143
  );
555
1144
 
556
1145
  await waitFor(() => {
@@ -588,20 +1177,23 @@ describe("ToolInvocationTracker", () => {
588
1177
  onResult,
589
1178
  onStatusesChange,
590
1179
  });
591
- tracker.setState(createState([]));
1180
+ tracker.setState(createState([], false));
592
1181
 
593
1182
  tracker.setState(
594
- createState([
595
- createAssistantMessage(
596
- '{"query":"parent"}',
597
- { query: "parent" },
598
- {
599
- result: { source: "history" },
600
- toolName: "resolvedOnly",
601
- nestedMessages: [nestedMessage],
602
- },
603
- ),
604
- ]),
1183
+ createState(
1184
+ [
1185
+ createAssistantMessage(
1186
+ '{"query":"parent"}',
1187
+ { query: "parent" },
1188
+ {
1189
+ result: { source: "history" },
1190
+ toolName: "resolvedOnly",
1191
+ nestedMessages: [nestedMessage],
1192
+ },
1193
+ ),
1194
+ ],
1195
+ false,
1196
+ ),
605
1197
  );
606
1198
 
607
1199
  await waitFor(() => {
@@ -747,15 +1339,18 @@ describe("ToolInvocationTracker", () => {
747
1339
  onResult,
748
1340
  onStatusesChange,
749
1341
  });
750
- tracker.setState(createState([]));
1342
+ tracker.setState(createState([], false));
751
1343
 
752
1344
  tracker.setState(
753
- createState([
754
- createAssistantMessage('{"a":1,"b":2}', {
755
- a: 1,
756
- b: 2,
757
- }),
758
- ]),
1345
+ createState(
1346
+ [
1347
+ createAssistantMessage('{"a":1,"b":2}', {
1348
+ a: 1,
1349
+ b: 2,
1350
+ }),
1351
+ ],
1352
+ false,
1353
+ ),
759
1354
  );
760
1355
 
761
1356
  await waitFor(() => {
@@ -763,18 +1358,21 @@ describe("ToolInvocationTracker", () => {
763
1358
  });
764
1359
 
765
1360
  tracker.setState(
766
- createState([
767
- createAssistantMessage(
768
- '{"b":2,"a":1}',
769
- {
770
- a: 1,
771
- b: 2,
772
- },
773
- {
774
- result: { source: "backend" },
775
- },
776
- ),
777
- ]),
1361
+ createState(
1362
+ [
1363
+ createAssistantMessage(
1364
+ '{"b":2,"a":1}',
1365
+ {
1366
+ a: 1,
1367
+ b: 2,
1368
+ },
1369
+ {
1370
+ result: { source: "backend" },
1371
+ },
1372
+ ),
1373
+ ],
1374
+ false,
1375
+ ),
778
1376
  );
779
1377
 
780
1378
  resolveExecute?.({ source: "client" });
@@ -1332,11 +1930,12 @@ describe("ToolInvocationTracker reset", () => {
1332
1930
  execute: vi.fn(() => new Promise(() => {})),
1333
1931
  } satisfies Tool,
1334
1932
  });
1335
- tracker.setState(createState([]));
1933
+ tracker.setState(createState([], false));
1336
1934
  tracker.setState(
1337
- createState([
1338
- createAssistantMessage('{"query":"London"}', { query: "London" }),
1339
- ]),
1935
+ createState(
1936
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
1937
+ false,
1938
+ ),
1340
1939
  );
1341
1940
  await waitFor(() => {
1342
1941
  expect(statuses()["tool-1"]?.type).toBe("executing");
@@ -1350,15 +1949,18 @@ describe("ToolInvocationTracker reset", () => {
1350
1949
  // discarded id back in through the whole-map republication. The first
1351
1950
  // post-reset snapshot is the fresh session's empty state, consuming the
1352
1951
  // pending-restore mark.
1353
- tracker.setState(createState([]));
1952
+ tracker.setState(createState([], false));
1354
1953
  tracker.setState(
1355
- createState([
1356
- createAssistantMessage(
1357
- '{"query":"Paris"}',
1358
- { query: "Paris" },
1359
- { toolCallId: "tool-2" },
1360
- ),
1361
- ]),
1954
+ createState(
1955
+ [
1956
+ createAssistantMessage(
1957
+ '{"query":"Paris"}',
1958
+ { query: "Paris" },
1959
+ { toolCallId: "tool-2" },
1960
+ ),
1961
+ ],
1962
+ false,
1963
+ ),
1362
1964
  );
1363
1965
  await waitFor(() => {
1364
1966
  expect(statuses()["tool-2"]?.type).toBe("executing");
@@ -1386,19 +1988,22 @@ describe("ToolInvocationTracker reset", () => {
1386
1988
  execute: vi.fn(() => new Promise(() => {})),
1387
1989
  } satisfies Tool,
1388
1990
  });
1389
- tracker.setState(createState([]));
1991
+ tracker.setState(createState([], false));
1390
1992
  tracker.setState(
1391
- createState([
1392
- createAssistantMessage('{"query":"London"}', { query: "London" }),
1393
- createAssistantMessage(
1394
- '{"query":"London"}',
1395
- { query: "London" },
1396
- {
1397
- toolCallId: "tool-2",
1398
- toolName: "pressureSearch",
1399
- },
1400
- ),
1401
- ]),
1993
+ createState(
1994
+ [
1995
+ createAssistantMessage('{"query":"London"}', { query: "London" }),
1996
+ createAssistantMessage(
1997
+ '{"query":"London"}',
1998
+ { query: "London" },
1999
+ {
2000
+ toolCallId: "tool-2",
2001
+ toolName: "pressureSearch",
2002
+ },
2003
+ ),
2004
+ ],
2005
+ false,
2006
+ ),
1402
2007
  );
1403
2008
  await waitFor(() => {
1404
2009
  expect(statuses()["tool-1"]?.type).toBe("executing");
@@ -1426,11 +2031,12 @@ describe("ToolInvocationTracker reset", () => {
1426
2031
  }),
1427
2032
  } satisfies Tool,
1428
2033
  });
1429
- tracker.setState(createState([]));
2034
+ tracker.setState(createState([], false));
1430
2035
  tracker.setState(
1431
- createState([
1432
- createAssistantMessage('{"query":"London"}', { query: "London" }),
1433
- ]),
2036
+ createState(
2037
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
2038
+ false,
2039
+ ),
1434
2040
  );
1435
2041
  await waitFor(() => {
1436
2042
  expect(statuses()["tool-1"]?.type).toBe("executing");
@@ -1457,11 +2063,12 @@ describe("ToolInvocationTracker reset", () => {
1457
2063
  } satisfies Tool,
1458
2064
  });
1459
2065
 
1460
- tracker.setState(createState([]));
2066
+ tracker.setState(createState([], false));
1461
2067
  tracker.setState(
1462
- createState([
1463
- createAssistantMessage('{"query":"London"}', { query: "London" }),
1464
- ]),
2068
+ createState(
2069
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
2070
+ false,
2071
+ ),
1465
2072
  );
1466
2073
  await waitFor(() => {
1467
2074
  expect(humanFns).toHaveLength(1);
@@ -1469,15 +2076,18 @@ describe("ToolInvocationTracker reset", () => {
1469
2076
  });
1470
2077
 
1471
2078
  tracker.reset();
1472
- tracker.setState(createState([]));
2079
+ tracker.setState(createState([], false));
1473
2080
  tracker.setState(
1474
- createState([
1475
- createAssistantMessage(
1476
- '{"query":"Paris"}',
1477
- { query: "Paris" },
1478
- { toolCallId: "tool-1" },
1479
- ),
1480
- ]),
2081
+ createState(
2082
+ [
2083
+ createAssistantMessage(
2084
+ '{"query":"Paris"}',
2085
+ { query: "Paris" },
2086
+ { toolCallId: "tool-1" },
2087
+ ),
2088
+ ],
2089
+ false,
2090
+ ),
1481
2091
  );
1482
2092
  await waitFor(() => expect(humanFns).toHaveLength(2));
1483
2093
 
@@ -1518,24 +2128,28 @@ describe("ToolInvocationTracker reset", () => {
1518
2128
  { onResult, onStatusesChange: vi.fn() },
1519
2129
  );
1520
2130
 
1521
- tracker.setState(createState([]));
2131
+ tracker.setState(createState([], false));
1522
2132
  tracker.setState(
1523
- createState([
1524
- createAssistantMessage('{"query":"London"}', { query: "London" }),
1525
- ]),
2133
+ createState(
2134
+ [createAssistantMessage('{"query":"London"}', { query: "London" })],
2135
+ false,
2136
+ ),
1526
2137
  );
1527
2138
  await waitFor(() => expect(executions).toHaveLength(1));
1528
2139
 
1529
2140
  tracker.reset();
1530
- tracker.setState(createState([]));
2141
+ tracker.setState(createState([], false));
1531
2142
  tracker.setState(
1532
- createState([
1533
- createAssistantMessage(
1534
- '{"query":"Paris"}',
1535
- { query: "Paris" },
1536
- { toolCallId: "tool-1" },
1537
- ),
1538
- ]),
2143
+ createState(
2144
+ [
2145
+ createAssistantMessage(
2146
+ '{"query":"Paris"}',
2147
+ { query: "Paris" },
2148
+ { toolCallId: "tool-1" },
2149
+ ),
2150
+ ],
2151
+ false,
2152
+ ),
1539
2153
  );
1540
2154
  await waitFor(() => expect(executions).toHaveLength(2));
1541
2155
  await new Promise((resolve) => setTimeout(resolve, 0));