@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
@@ -36,6 +36,15 @@ const AUTO_STATUS_COMPLETE = Object.freeze(
36
36
  { [symbolAutoStatus]: true },
37
37
  ),
38
38
  );
39
+ const AUTO_STATUS_CANCELLED = Object.freeze(
40
+ Object.assign(
41
+ {
42
+ type: "incomplete" as const,
43
+ reason: "cancelled" as const,
44
+ },
45
+ { [symbolAutoStatus]: true },
46
+ ),
47
+ );
39
48
 
40
49
  const AUTO_STATUS_PENDING = Object.freeze(
41
50
  Object.assign(
@@ -66,6 +75,7 @@ export const getAutoStatus = (
66
75
  hasInterruptedToolCalls: boolean,
67
76
  hasPendingToolCalls: boolean,
68
77
  error?: ReadonlyJSONValue,
78
+ isCancelled?: boolean,
69
79
  ): MessageStatus => {
70
80
  if (isLast && error) {
71
81
  return Object.assign(
@@ -84,7 +94,9 @@ export const getAutoStatus = (
84
94
  ? AUTO_STATUS_INTERRUPT
85
95
  : hasPendingToolCalls
86
96
  ? AUTO_STATUS_PENDING
87
- : AUTO_STATUS_COMPLETE;
97
+ : isCancelled
98
+ ? AUTO_STATUS_CANCELLED
99
+ : AUTO_STATUS_COMPLETE;
88
100
  };
89
101
 
90
102
  export const getContentAutoStatus = (
@@ -110,4 +110,101 @@ describe("convertExternalMessageChunk", () => {
110
110
 
111
111
  expect(second).toBe(first);
112
112
  });
113
+
114
+ it("replaces a cached complete status after cancellation", () => {
115
+ const input = {};
116
+ const chunk = {
117
+ inputs: [input],
118
+ outputs: [{ id: "m1", role: "assistant" as const, content: "partial" }],
119
+ };
120
+ const cancelled = new Set(["m1"]);
121
+ const generatedFallbackMessages = new WeakSet<object>();
122
+ const first = convertExternalMessageChunk(chunk, 0, 1, false, undefined, {
123
+ message: undefined,
124
+ generatedFallbackMessages,
125
+ });
126
+
127
+ const second = convertExternalMessageChunk(
128
+ chunk,
129
+ 0,
130
+ 1,
131
+ false,
132
+ undefined,
133
+ { message: first, generatedFallbackMessages },
134
+ cancelled,
135
+ );
136
+
137
+ expect(first.status).toMatchObject({ type: "complete", reason: "unknown" });
138
+ expect(second).not.toBe(first);
139
+ expect(second.status).toMatchObject({
140
+ type: "incomplete",
141
+ reason: "cancelled",
142
+ });
143
+
144
+ const failed = convertExternalMessageChunk(
145
+ chunk,
146
+ 0,
147
+ 1,
148
+ false,
149
+ "failed",
150
+ { message: second, generatedFallbackMessages },
151
+ cancelled,
152
+ );
153
+ expect(failed.status).toMatchObject({
154
+ type: "incomplete",
155
+ reason: "error",
156
+ error: "failed",
157
+ });
158
+ });
159
+
160
+ it("cancels a joined chunk when any joined message was stopped", () => {
161
+ const generatedFallbackMessages = new WeakSet<object>();
162
+ const result = convertExternalMessageChunk(
163
+ {
164
+ inputs: [{}, {}],
165
+ outputs: [
166
+ { id: "m1", role: "assistant" as const, content: "first" },
167
+ { id: "m2", role: "assistant" as const, content: "second" },
168
+ ],
169
+ },
170
+ 0,
171
+ 1,
172
+ false,
173
+ undefined,
174
+ { message: undefined, generatedFallbackMessages },
175
+ new Set(["m2"]),
176
+ );
177
+
178
+ expect(result.status).toMatchObject({
179
+ type: "incomplete",
180
+ reason: "cancelled",
181
+ });
182
+ });
183
+
184
+ it("cancels an earlier message that is no longer the last one", () => {
185
+ const cancelled = new Set(["m1"]);
186
+ const generatedFallbackMessages = new WeakSet<object>();
187
+ const build = (id: string, idx: number) =>
188
+ convertExternalMessageChunk(
189
+ {
190
+ inputs: [{}],
191
+ outputs: [{ id, role: "assistant" as const, content: "text" }],
192
+ },
193
+ idx,
194
+ 2,
195
+ false,
196
+ undefined,
197
+ { message: undefined, generatedFallbackMessages },
198
+ cancelled,
199
+ );
200
+
201
+ expect(build("m1", 0).status).toMatchObject({
202
+ type: "incomplete",
203
+ reason: "cancelled",
204
+ });
205
+ expect(build("m2", 1).status).toMatchObject({
206
+ type: "complete",
207
+ reason: "unknown",
208
+ });
209
+ });
113
210
  });
@@ -46,6 +46,7 @@ export type ExternalMessageConverterMessage =
46
46
  export type ExternalMessageConverterMetadata = {
47
47
  readonly toolStatuses?: Record<string, ToolExecutionStatus>;
48
48
  readonly error?: ReadonlyJSONValue;
49
+ readonly cancelledMessageIds?: ReadonlySet<string>;
49
50
  readonly messageTiming?: Record<string, MessageTiming>;
50
51
  };
51
52
 
@@ -358,7 +359,10 @@ export const chunkExternalMessages = <T>(
358
359
  return results;
359
360
  };
360
361
 
361
- export const shallowArrayEqual = (a: unknown[], b: unknown[]) => {
362
+ export const shallowArrayEqual = (
363
+ a: readonly unknown[],
364
+ b: readonly unknown[],
365
+ ) => {
362
366
  if (a.length !== b.length) return false;
363
367
  for (let i = 0; i < a.length; i++) {
364
368
  if (a[i] !== b[i]) return false;
@@ -378,9 +382,18 @@ export const convertExternalMessageChunk = <T>(
378
382
  isRunning: boolean,
379
383
  error: ReadonlyJSONValue | undefined,
380
384
  cache?: ExternalMessageConversionCache,
385
+ cancelledMessageIds?: ReadonlySet<string>,
381
386
  ) => {
382
387
  const isLast = idx === chunkCount - 1;
383
388
  const joined = joinExternalMessages(message.outputs);
389
+ const isCancelled =
390
+ cancelledMessageIds !== undefined &&
391
+ message.outputs.some(
392
+ (output) =>
393
+ output.role !== "tool" &&
394
+ output.id != null &&
395
+ cancelledMessageIds.has(output.id),
396
+ );
384
397
  const hasInterruptedToolCalls =
385
398
  typeof joined.content === "object" &&
386
399
  joined.content.some(isInterruptedToolCall);
@@ -393,6 +406,7 @@ export const convertExternalMessageChunk = <T>(
393
406
  hasInterruptedToolCalls,
394
407
  hasPendingToolCalls,
395
408
  isLast ? error : undefined,
409
+ isCancelled,
396
410
  );
397
411
  const fallbackId = `${FALLBACK_ID_PREFIX}${idx}`;
398
412
 
@@ -488,6 +502,8 @@ export const convertExternalMessages = <T extends WeakKey>(
488
502
  chunks.length,
489
503
  isRunning,
490
504
  metadata.error,
505
+ undefined,
506
+ metadata.cancelledMessageIds,
491
507
  ),
492
508
  );
493
509
  return completeExternalMessageConversion(result, metadata.error);
@@ -88,4 +88,77 @@ describe("resolveToolApprovalResponse", () => {
88
88
  resolveToolApprovalResponse({ id: "a2" }, { optionId: "once" }),
89
89
  ).toThrow('no option with id "once"');
90
90
  });
91
+
92
+ it("resolves a free-form answer as approved and carries the text", () => {
93
+ expect(
94
+ resolveToolApprovalResponse(
95
+ { id: "q1", display: "text" },
96
+ { text: "staging" },
97
+ ),
98
+ ).toEqual({ approvalId: "q1", approved: true, text: "staging" });
99
+ });
100
+
101
+ it("carries the text alongside a chosen option", () => {
102
+ expect(
103
+ resolveToolApprovalResponse(
104
+ {
105
+ id: "q2",
106
+ display: "select",
107
+ allowFreeform: true,
108
+ options: [{ id: "other", kind: "_other" }],
109
+ },
110
+ { optionId: "other", approved: true, text: "somewhere else" },
111
+ ),
112
+ ).toEqual({
113
+ approvalId: "q2",
114
+ approved: true,
115
+ optionId: "other",
116
+ text: "somewhere else",
117
+ });
118
+ });
119
+
120
+ it("throws when the request does not accept a free-form answer", () => {
121
+ expect(() =>
122
+ resolveToolApprovalResponse(approval, { text: "anything" }),
123
+ ).toThrow("does not accept a free-form answer");
124
+ expect(() =>
125
+ resolveToolApprovalResponse(
126
+ { id: "a4", display: "select" },
127
+ { approved: true, text: "anything" },
128
+ ),
129
+ ).toThrow("does not accept a free-form answer");
130
+ });
131
+
132
+ it("refuses to infer approval from a bare answer on a decision", () => {
133
+ expect(() =>
134
+ resolveToolApprovalResponse(
135
+ { id: "a5", display: "decision", allowFreeform: true },
136
+ { text: "not this one" },
137
+ ),
138
+ ).toThrow("is a decision, not a question");
139
+ expect(() =>
140
+ resolveToolApprovalResponse(
141
+ { id: "a6", allowFreeform: true },
142
+ { text: "not this one" },
143
+ ),
144
+ ).toThrow("is a decision, not a question");
145
+ });
146
+
147
+ it("records the answer alongside an explicit decision on a gate", () => {
148
+ expect(
149
+ resolveToolApprovalResponse(
150
+ { id: "a7", display: "decision", allowFreeform: true },
151
+ { approved: false, text: "not this one" },
152
+ ),
153
+ ).toEqual({ approvalId: "a7", approved: false, text: "not this one" });
154
+ });
155
+
156
+ it("keeps a refusal a refusal when the request accepts text", () => {
157
+ expect(
158
+ resolveToolApprovalResponse(
159
+ { id: "q3", allowFreeform: true },
160
+ { approved: false, reason: "not answering" },
161
+ ),
162
+ ).toEqual({ approvalId: "q3", approved: false, reason: "not answering" });
163
+ });
91
164
  });
@@ -1,6 +1,8 @@
1
- import type {
2
- ToolApprovalOption,
3
- ToolApprovalResponse,
1
+ import {
2
+ toolApprovalAcceptsText,
3
+ type ToolApprovalDisplay,
4
+ type ToolApprovalOption,
5
+ type ToolApprovalResponse,
4
6
  } from "../../types/message";
5
7
  import type { RespondToToolApprovalOptions } from "../interfaces/thread-runtime-core";
6
8
 
@@ -12,16 +14,25 @@ const APPROVED_BY_KIND: Record<string, boolean> = {
12
14
  };
13
15
 
14
16
  /**
15
- * Resolves a renderer-facing approval response (boolean or optionId form)
16
- * against the approval's option list into the runtime-facing decision shape.
17
+ * Resolves a renderer-facing approval response (boolean, optionId, or
18
+ * free-form answer) against the approval's request shape into the
19
+ * runtime-facing decision shape.
17
20
  */
18
21
  export const resolveToolApprovalResponse = (
19
22
  approval: {
20
23
  readonly id: string;
24
+ readonly display?: ToolApprovalDisplay;
25
+ readonly allowFreeform?: boolean;
21
26
  readonly options?: readonly ToolApprovalOption[];
22
27
  },
23
28
  response: ToolApprovalResponse,
24
29
  ): RespondToToolApprovalOptions => {
30
+ const text = response.text;
31
+ if (text !== undefined && !toolApprovalAcceptsText(approval))
32
+ throw new Error(
33
+ `Tool approval "${approval.id}" does not accept a free-form answer; the request must declare display "text" or allowFreeform`,
34
+ );
35
+
25
36
  let approved: boolean;
26
37
  let optionId: string | undefined;
27
38
 
@@ -42,14 +53,24 @@ export const resolveToolApprovalResponse = (
42
53
  approved = APPROVED_BY_KIND[option.kind]!;
43
54
  }
44
55
  optionId = option.id;
45
- } else {
56
+ } else if ("approved" in response) {
46
57
  approved = response.approved;
58
+ } else {
59
+ // A bare answer resolves a question, where answering is not refusing. On a
60
+ // decision the approval itself is the authorization, so inferring one from
61
+ // a typed note would let the note authorize the call.
62
+ if (approval.display !== "text" && approval.display !== "select")
63
+ throw new Error(
64
+ `Tool approval "${approval.id}" is a decision, not a question; respond with an explicit approved value, optionally alongside the answer`,
65
+ );
66
+ approved = true;
47
67
  }
48
68
 
49
69
  return {
50
70
  approvalId: approval.id,
51
71
  approved,
52
72
  ...(optionId !== undefined && { optionId }),
73
+ ...(text !== undefined && { text }),
53
74
  ...(response.reason != null && { reason: response.reason }),
54
75
  };
55
76
  };
@@ -23,8 +23,8 @@ import type {
23
23
  MessageTiming,
24
24
  PartProviderMetadata,
25
25
  TextMessagePart,
26
- ToolApprovalOption,
27
26
  ToolCallTiming,
27
+ ToolCallMessagePart,
28
28
  ToolCallMessagePartMcpMetadata,
29
29
  } from "../../types/message";
30
30
  import type {
@@ -66,15 +66,7 @@ export type ThreadMessageLike = {
66
66
  readonly timing?: ToolCallTiming;
67
67
  readonly mcp?: ToolCallMessagePartMcpMetadata;
68
68
  readonly providerMetadata?: PartProviderMetadata;
69
- readonly approval?: {
70
- readonly id: string;
71
- readonly approved?: boolean;
72
- readonly reason?: string;
73
- readonly isAutomatic?: boolean;
74
- readonly options?: readonly ToolApprovalOption[];
75
- readonly optionId?: string;
76
- readonly resolution?: "cancelled" | "expired";
77
- };
69
+ readonly approval?: NonNullable<ToolCallMessagePart["approval"]>;
78
70
  }
79
71
  )[];
80
72
  readonly id?: string | undefined;
@@ -0,0 +1,191 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type {
3
+ ThreadAssistantMessage,
4
+ ThreadAssistantMessagePart,
5
+ ThreadMessage,
6
+ ToolCallMessagePart,
7
+ } from "../../types/message";
8
+ import {
9
+ iterateToolCallParts,
10
+ mapToolCallPartsDeep,
11
+ walkToolCallTree,
12
+ } from "./tool-call-tree";
13
+
14
+ const toolCall = (
15
+ toolCallId: string,
16
+ messages?: readonly ThreadMessage[],
17
+ ): ToolCallMessagePart => ({
18
+ type: "tool-call",
19
+ toolCallId,
20
+ toolName: "t",
21
+ argsText: "{}",
22
+ args: {},
23
+ ...(messages !== undefined && { messages }),
24
+ });
25
+
26
+ const assistant = (
27
+ id: string,
28
+ content: readonly ThreadAssistantMessagePart[],
29
+ ): ThreadAssistantMessage =>
30
+ ({
31
+ id,
32
+ role: "assistant",
33
+ content,
34
+ status: { type: "complete", reason: "unknown" },
35
+ metadata: {
36
+ unstable_state: null,
37
+ unstable_annotations: [],
38
+ unstable_data: [],
39
+ steps: [],
40
+ custom: {},
41
+ },
42
+ createdAt: new Date(0),
43
+ attachments: [],
44
+ }) as unknown as ThreadAssistantMessage;
45
+
46
+ const user = (id: string): ThreadMessage =>
47
+ ({
48
+ id,
49
+ role: "user",
50
+ content: [{ type: "text", text: "hi" }],
51
+ metadata: { custom: {} },
52
+ createdAt: new Date(0),
53
+ attachments: [],
54
+ }) as unknown as ThreadMessage;
55
+
56
+ // root
57
+ // └─ a ─ nested message "m2"
58
+ // ├─ b ─ nested message "m3"
59
+ // │ └─ c
60
+ // └─ d
61
+ const tree = () => {
62
+ const c = toolCall("c");
63
+ const b = toolCall("b", [assistant("m3", [c])]);
64
+ const d = toolCall("d");
65
+ const a = toolCall("a", [assistant("m2", [b, d])]);
66
+ return { a, b, c, d };
67
+ };
68
+
69
+ describe("walkToolCallTree", () => {
70
+ it("yields every part in document order, each ahead of its descendants", () => {
71
+ const { a } = tree();
72
+ const entries = [
73
+ ...walkToolCallTree([user("m0"), assistant("m1", [a, toolCall("e")])]),
74
+ ];
75
+
76
+ expect(entries.map((entry) => entry.part.toolCallId)).toEqual([
77
+ "a",
78
+ "b",
79
+ "c",
80
+ "d",
81
+ "e",
82
+ ]);
83
+ });
84
+
85
+ it("reports the message that directly holds each part", () => {
86
+ const { a } = tree();
87
+ const entries = [...walkToolCallTree([assistant("m1", [a])])];
88
+
89
+ expect(
90
+ Object.fromEntries(
91
+ entries.map((entry) => [entry.part.toolCallId, entry.messageId]),
92
+ ),
93
+ ).toEqual({ a: "m1", b: "m2", c: "m3", d: "m2" });
94
+ });
95
+
96
+ it("skips messages whose content is not an array", () => {
97
+ const malformed = {
98
+ id: "m1",
99
+ role: "assistant",
100
+ } as unknown as ThreadMessage;
101
+ expect([...walkToolCallTree([malformed])]).toEqual([]);
102
+ });
103
+
104
+ it("skips a non-assistant message carrying a tool-call part", () => {
105
+ const malformed = {
106
+ ...user("m2"),
107
+ content: [toolCall("b")],
108
+ } as unknown as ThreadMessage;
109
+ const a = toolCall("a", [malformed]);
110
+
111
+ expect(
112
+ [...walkToolCallTree([assistant("m1", [a])])].map(
113
+ (entry) => entry.part.toolCallId,
114
+ ),
115
+ ).toEqual(["a"]);
116
+ });
117
+ });
118
+
119
+ describe("iterateToolCallParts", () => {
120
+ it("matches walkToolCallTree over the same content", () => {
121
+ const { a } = tree();
122
+ const content = [a, toolCall("e")];
123
+
124
+ expect([...iterateToolCallParts(content)].map((p) => p.toolCallId)).toEqual(
125
+ [...walkToolCallTree([assistant("m1", content)])].map(
126
+ (entry) => entry.part.toolCallId,
127
+ ),
128
+ );
129
+ });
130
+
131
+ it("ignores non tool-call parts", () => {
132
+ expect([
133
+ ...iterateToolCallParts([
134
+ { type: "text", text: "hi" } as ThreadAssistantMessagePart,
135
+ toolCall("a"),
136
+ ]),
137
+ ]).toHaveLength(1);
138
+ });
139
+ });
140
+
141
+ describe("mapToolCallPartsDeep", () => {
142
+ it("rewrites nested parts and reports the change", () => {
143
+ const { a } = tree();
144
+ const content = [a];
145
+ const { content: next, changed } = mapToolCallPartsDeep(content, (part) =>
146
+ part.toolCallId === "c" ? { ...part, isError: true } : part,
147
+ );
148
+
149
+ expect(changed).toBe(true);
150
+ const parts = [...iterateToolCallParts(next)];
151
+ expect(parts.find((part) => part.toolCallId === "c")?.isError).toBe(true);
152
+ expect(
153
+ parts.find((part) => part.toolCallId === "d")?.isError,
154
+ ).toBeUndefined();
155
+ });
156
+
157
+ it("skips a nested message whose content is not an array", () => {
158
+ const malformed = {
159
+ id: "m2",
160
+ role: "assistant",
161
+ } as unknown as ThreadMessage;
162
+ const a = toolCall("a", [malformed]);
163
+
164
+ expect(() =>
165
+ mapToolCallPartsDeep([a], (part) => ({ ...part, isError: true })),
166
+ ).not.toThrow();
167
+ });
168
+
169
+ it("keeps the original content identity when nothing changed", () => {
170
+ const { a } = tree();
171
+ const content = [a, toolCall("e")];
172
+ const result = mapToolCallPartsDeep(content, (part) => part);
173
+
174
+ expect(result.changed).toBe(false);
175
+ expect(result.content).toBe(content);
176
+ });
177
+
178
+ it("preserves untouched sibling messages by identity", () => {
179
+ const nested = assistant("m2", [toolCall("b")]);
180
+ const sibling = user("m3");
181
+ const a = toolCall("a", [nested, sibling]);
182
+ const { content: next } = mapToolCallPartsDeep([a], (part) =>
183
+ part.toolCallId === "b" ? { ...part, isError: true } : part,
184
+ );
185
+
186
+ const mapped = next[0] as ToolCallMessagePart;
187
+ expect(mapped).not.toBe(a);
188
+ expect(mapped.messages?.[1]).toBe(sibling);
189
+ expect(mapped.messages?.[0]).not.toBe(nested);
190
+ });
191
+ });
@@ -0,0 +1,97 @@
1
+ import type {
2
+ ThreadAssistantMessage,
3
+ ThreadAssistantMessagePart,
4
+ ThreadMessage,
5
+ ToolCallMessagePart,
6
+ } from "../../types/message";
7
+
8
+ export type ToolCallTreeEntry = {
9
+ readonly part: ToolCallMessagePart;
10
+ /** Id of the message whose content directly holds `part`. */
11
+ readonly messageId: string;
12
+ };
13
+
14
+ /**
15
+ * Walk every tool-call part reachable from `messages`. A tool call projects a
16
+ * child run as nested messages on `ToolCallMessagePart.messages`, so a
17
+ * subagent's own calls live below the top-level content and a seam that only
18
+ * scans the top level cannot reach them.
19
+ *
20
+ * Parts arrive in document order, each one ahead of its own descendants.
21
+ * `messageId` names the message that directly holds the part, which for a
22
+ * nested call is the child run's message rather than the top-level message the
23
+ * tree hangs from. Only assistant messages are descended, the same rule
24
+ * {@link mapToolCallPartsDeep} rewrites under, so a part this reports is always
25
+ * a part that can be written back.
26
+ */
27
+ export function* walkToolCallTree(
28
+ messages: readonly ThreadMessage[],
29
+ ): Generator<ToolCallTreeEntry> {
30
+ for (const message of messages) {
31
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) {
32
+ continue;
33
+ }
34
+ for (const part of message.content) {
35
+ if (!part || part.type !== "tool-call") continue;
36
+ yield { part, messageId: message.id };
37
+ if (part.messages?.length) yield* walkToolCallTree(part.messages);
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Content-level form of {@link walkToolCallTree}, for a caller that already
44
+ * holds one assistant message's content and does not need the owning id.
45
+ */
46
+ export function* iterateToolCallParts(
47
+ content: readonly ThreadAssistantMessagePart[],
48
+ ): Generator<ToolCallMessagePart> {
49
+ for (const part of content) {
50
+ if (!part || part.type !== "tool-call") continue;
51
+ yield part;
52
+ if (part.messages?.length) {
53
+ for (const entry of walkToolCallTree(part.messages)) {
54
+ yield entry.part;
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Rebuild `content` with `fn` applied to every tool-call part in the tree.
62
+ * Arrays and nested messages are reused wherever `fn` returned the part it was
63
+ * given, so an unchanged subtree keeps its identity and `changed` reports
64
+ * whether anything moved.
65
+ */
66
+ export function mapToolCallPartsDeep(
67
+ content: readonly ThreadAssistantMessagePart[],
68
+ fn: (part: ToolCallMessagePart) => ToolCallMessagePart,
69
+ ): { content: readonly ThreadAssistantMessagePart[]; changed: boolean } {
70
+ let changed = false;
71
+ const next = content.map((part): ThreadAssistantMessagePart => {
72
+ if (part.type !== "tool-call") return part;
73
+ let mapped = fn(part);
74
+ if (mapped.messages !== undefined) {
75
+ let nestedChanged = false;
76
+ const nestedMessages = mapped.messages.map((nested) => {
77
+ if (nested.role !== "assistant" || !Array.isArray(nested.content)) {
78
+ return nested;
79
+ }
80
+ const assistant = nested as ThreadAssistantMessage;
81
+ const result = mapToolCallPartsDeep(assistant.content, fn);
82
+ if (!result.changed) return nested;
83
+ nestedChanged = true;
84
+ return { ...assistant, content: result.content };
85
+ });
86
+ if (nestedChanged) {
87
+ mapped =
88
+ mapped === part
89
+ ? { ...part, messages: nestedMessages }
90
+ : { ...mapped, messages: nestedMessages };
91
+ }
92
+ }
93
+ if (mapped !== part) changed = true;
94
+ return mapped;
95
+ });
96
+ return changed ? { content: next, changed } : { content, changed };
97
+ }