@assistant-ui/core 0.3.18 → 0.3.19

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 (274) hide show
  1. package/dist/adapters/speech.d.ts.map +1 -1
  2. package/dist/adapters/speech.js +13 -3
  3. package/dist/adapters/speech.js.map +1 -1
  4. package/dist/adapters/thread-history.d.ts +4 -0
  5. package/dist/adapters/thread-history.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal.d.ts +1 -1
  9. package/dist/model-context/frame/host.d.ts.map +1 -1
  10. package/dist/model-context/frame/host.js +26 -11
  11. package/dist/model-context/frame/host.js.map +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js +11 -9
  13. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  14. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  15. package/dist/react/adapters/LocalStorageThreadListAdapter.js +45 -7
  16. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  17. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  18. package/dist/react/client/DataRenderers.js +20 -15
  19. package/dist/react/client/DataRenderers.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +11 -5
  22. package/dist/react/client/Interactables.js.map +1 -1
  23. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  24. package/dist/react/client/RemoteThreadList.js +3 -2
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  27. package/dist/react/client/interactable-model-context.js +3 -2
  28. package/dist/react/client/interactable-model-context.js.map +1 -1
  29. package/dist/react/index.d.ts +2 -2
  30. package/dist/react/index.js +2 -2
  31. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
  32. package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
  33. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
  34. package/dist/react/model-context/useInteractable.d.ts.map +1 -1
  35. package/dist/react/model-context/useInteractable.js +4 -1
  36. package/dist/react/model-context/useInteractable.js.map +1 -1
  37. package/dist/react/model-context/useInteractableVersions.js +1 -1
  38. package/dist/react/primitives/message/MessageGroupedParts.d.ts +11 -2
  39. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  40. package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
  41. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  42. package/dist/react/primitives/part/PartMessages.d.ts +1 -0
  43. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  44. package/dist/react/primitives/part/PartMessages.js +1 -0
  45. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  46. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  47. package/dist/react/primitives/thread/ThreadMessages.js +6 -5
  48. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  49. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  50. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +2 -1
  51. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  52. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +2 -24
  53. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  54. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +135 -374
  55. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  56. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -0
  57. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  58. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
  59. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  60. package/dist/react/runtimes/cloud/auiV0.d.ts +40 -15
  61. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  62. package/dist/react/runtimes/cloud/auiV0.js +62 -15
  63. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  64. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +2 -1
  65. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  66. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +4 -0
  67. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  68. package/dist/react/runtimes/cloud/sdkIdentity.d.ts +6 -0
  69. package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
  70. package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
  71. package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
  72. package/dist/react/runtimes/external-message-converter.d.ts +12 -2
  73. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  74. package/dist/react/runtimes/external-message-converter.js +26 -61
  75. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  76. package/dist/react/runtimes/useRemoteThreadListRuntime.js +1 -1
  77. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  78. package/dist/react/utils/groupParts.d.ts +6 -1
  79. package/dist/react/utils/groupParts.d.ts.map +1 -1
  80. package/dist/react/utils/groupParts.js +6 -0
  81. package/dist/react/utils/groupParts.js.map +1 -1
  82. package/dist/react/utils/useJSONEqual.d.ts +3 -1
  83. package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
  84. package/dist/react/utils/useJSONEqual.js +6 -1
  85. package/dist/react/utils/useJSONEqual.js.map +1 -1
  86. package/dist/react/utils/useJSONSchemaDependency.d.ts +7 -0
  87. package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
  88. package/dist/react/utils/useJSONSchemaDependency.js +20 -0
  89. package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
  90. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  91. package/dist/runtime/api/thread-runtime.js +2 -5
  92. package/dist/runtime/api/thread-runtime.js.map +1 -1
  93. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
  94. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.js +46 -10
  96. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  97. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -0
  98. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.js +155 -64
  100. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  101. package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
  102. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  103. package/dist/runtime/queue/message-queue.d.ts +5 -0
  104. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  105. package/dist/runtime/queue/message-queue.js +61 -6
  106. package/dist/runtime/queue/message-queue.js.map +1 -1
  107. package/dist/runtime/utils/auto-status.d.ts +6 -2
  108. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  109. package/dist/runtime/utils/auto-status.js +12 -4
  110. package/dist/runtime/utils/auto-status.js.map +1 -1
  111. package/dist/runtime/utils/external-message-conversion.d.ts +16 -4
  112. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  113. package/dist/runtime/utils/external-message-conversion.js +48 -12
  114. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  115. package/dist/runtime/utils/message-repository.js +3 -3
  116. package/dist/runtime/utils/message-repository.js.map +1 -1
  117. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  118. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  119. package/dist/runtime/utils/thread-message-like.js +4 -2
  120. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  121. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  122. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -1
  123. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  124. package/dist/runtimes/internal.d.ts +1 -1
  125. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
  126. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  127. package/dist/runtimes/local/local-thread-runtime-core.js +4 -1
  128. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  129. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  130. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
  131. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  132. package/dist/store/clients/external-thread.d.ts.map +1 -1
  133. package/dist/store/clients/external-thread.js +241 -200
  134. package/dist/store/clients/external-thread.js.map +1 -1
  135. package/dist/store/clients/thread-tasks.d.ts +13 -0
  136. package/dist/store/clients/thread-tasks.d.ts.map +1 -0
  137. package/dist/store/clients/thread-tasks.js +69 -0
  138. package/dist/store/clients/thread-tasks.js.map +1 -0
  139. package/dist/store/index.d.ts +3 -2
  140. package/dist/store/index.js +3 -3
  141. package/dist/store/primitive-predicates.d.ts.map +1 -1
  142. package/dist/store/primitive-predicates.js +4 -4
  143. package/dist/store/primitive-predicates.js.map +1 -1
  144. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  145. package/dist/store/runtime-clients/thread-runtime-client.js +176 -128
  146. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  147. package/dist/store/scope-registration.d.ts +2 -0
  148. package/dist/store/scope-registration.d.ts.map +1 -1
  149. package/dist/store/scopes/task.d.ts +36 -0
  150. package/dist/store/scopes/task.d.ts.map +1 -0
  151. package/dist/store/scopes/task.js +0 -0
  152. package/dist/store/scopes/thread.d.ts +20 -0
  153. package/dist/store/scopes/thread.d.ts.map +1 -1
  154. package/dist/subscribable/subscribable.d.ts +2 -1
  155. package/dist/subscribable/subscribable.d.ts.map +1 -1
  156. package/dist/subscribable/subscribable.js +23 -15
  157. package/dist/subscribable/subscribable.js.map +1 -1
  158. package/dist/types/index.d.ts +2 -2
  159. package/dist/types/message.d.ts +8 -1
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/abortable-iterable.d.ts.map +1 -1
  163. package/dist/utils/abortable-iterable.js +30 -14
  164. package/dist/utils/abortable-iterable.js.map +1 -1
  165. package/dist/utils/getGroupStatus.d.ts +18 -2
  166. package/dist/utils/getGroupStatus.d.ts.map +1 -1
  167. package/dist/utils/getGroupStatus.js +32 -7
  168. package/dist/utils/getGroupStatus.js.map +1 -1
  169. package/dist/utils/wire-media.d.ts.map +1 -1
  170. package/dist/utils/wire-media.js +2 -2
  171. package/dist/utils/wire-media.js.map +1 -1
  172. package/package.json +6 -6
  173. package/src/adapters/speech.test.ts +236 -0
  174. package/src/adapters/speech.ts +17 -10
  175. package/src/adapters/thread-history.ts +3 -0
  176. package/src/index.ts +1 -0
  177. package/src/model-context/frame/host.test.ts +113 -1
  178. package/src/model-context/frame/host.ts +39 -12
  179. package/src/model-context/interactable-composer-metadata.test.ts +30 -0
  180. package/src/model-context/interactable-composer-metadata.ts +10 -10
  181. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +244 -0
  182. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +104 -7
  183. package/src/react/client/DataRenderers.test.tsx +45 -0
  184. package/src/react/client/DataRenderers.ts +13 -9
  185. package/src/react/client/Interactables.test.ts +44 -1
  186. package/src/react/client/Interactables.ts +7 -1
  187. package/src/react/client/RemoteThreadList.test.ts +41 -1
  188. package/src/react/client/RemoteThreadList.ts +15 -2
  189. package/src/react/client/interactable-model-context.test.ts +44 -0
  190. package/src/react/client/interactable-model-context.ts +3 -2
  191. package/src/react/index.ts +5 -1
  192. package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +132 -0
  193. package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
  194. package/src/react/model-context/useInteractable.test.tsx +177 -0
  195. package/src/react/model-context/useInteractable.ts +3 -0
  196. package/src/react/primitive-hooks/useActionBarCopy.test.ts +0 -1
  197. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
  198. package/src/react/primitives/message/MessageGroupedParts.test.tsx +96 -0
  199. package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
  200. package/src/react/primitives/part/PartMessages.test.tsx +104 -1
  201. package/src/react/primitives/part/PartMessages.tsx +1 -0
  202. package/src/react/primitives/thread/ThreadMessages.test.tsx +142 -0
  203. package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
  204. package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
  205. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +7 -1
  206. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
  207. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +221 -1
  208. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +166 -512
  209. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +77 -0
  210. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
  211. package/src/react/runtimes/cloud/auiV0.ts +160 -25
  212. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +21 -0
  213. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +7 -1
  214. package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
  215. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
  216. package/src/react/runtimes/external-message-converter.test.ts +136 -1
  217. package/src/react/runtimes/external-message-converter.ts +39 -93
  218. package/src/react/runtimes/useRemoteThreadListRuntime.ts +1 -1
  219. package/src/react/utils/groupParts.ts +12 -1
  220. package/src/react/utils/useJSONEqual.ts +9 -0
  221. package/src/react/utils/useJSONSchemaDependency.ts +22 -0
  222. package/src/runtime/api/thread-runtime.test.ts +58 -0
  223. package/src/runtime/api/thread-runtime.ts +2 -4
  224. package/src/runtime/base/base-composer-runtime-core.ts +69 -16
  225. package/src/runtime/base/base-thread-runtime-core.test.ts +1008 -1
  226. package/src/runtime/base/base-thread-runtime-core.ts +222 -83
  227. package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
  228. package/src/runtime/queue/message-queue.ts +68 -6
  229. package/src/runtime/utils/auto-status.test.ts +112 -1
  230. package/src/runtime/utils/auto-status.ts +31 -5
  231. package/src/runtime/utils/external-message-conversion.test.ts +134 -0
  232. package/src/runtime/utils/external-message-conversion.ts +122 -31
  233. package/src/runtime/utils/message-repository.test.ts +49 -0
  234. package/src/runtime/utils/message-repository.ts +3 -3
  235. package/src/runtime/utils/thread-message-like.test.ts +52 -0
  236. package/src/runtime/utils/thread-message-like.ts +4 -0
  237. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +31 -1
  238. package/src/runtimes/local/local-thread-runtime-core.test.ts +21 -0
  239. package/src/runtimes/local/local-thread-runtime-core.ts +11 -1
  240. package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
  241. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
  242. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
  243. package/src/store/clients/external-thread.ts +19 -1
  244. package/src/store/clients/thread-tasks.test.ts +274 -0
  245. package/src/store/clients/thread-tasks.ts +138 -0
  246. package/src/store/index.ts +8 -0
  247. package/src/store/primitive-predicates.test.ts +48 -5
  248. package/src/store/primitive-predicates.ts +7 -2
  249. package/src/store/runtime-clients/thread-runtime-client.ts +31 -1
  250. package/src/store/scope-registration.ts +2 -0
  251. package/src/store/scopes/task.ts +34 -0
  252. package/src/store/scopes/thread.ts +16 -0
  253. package/src/store/thread-tasks.test.tsx +209 -0
  254. package/src/subscribable/subscribable.test.ts +156 -1
  255. package/src/subscribable/subscribable.ts +25 -15
  256. package/src/tests/MessageRepository.test.ts +1 -5
  257. package/src/tests/auiV0Encode.test.ts +300 -0
  258. package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
  259. package/src/tests/base-composer-runtime-core.test.ts +314 -2
  260. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +151 -0
  261. package/src/tests/external-thread-attachments.test.tsx +69 -0
  262. package/src/tests/external-thread-parity.test.tsx +34 -0
  263. package/src/tests/groupParts.test.ts +71 -0
  264. package/src/tests/message-queue.test.ts +253 -0
  265. package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
  266. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +43 -0
  267. package/src/types/index.ts +1 -0
  268. package/src/types/message.ts +8 -0
  269. package/src/utils/abortable-iterable.test.ts +115 -0
  270. package/src/utils/abortable-iterable.ts +34 -23
  271. package/src/utils/getGroupStatus.test.ts +70 -4
  272. package/src/utils/getGroupStatus.ts +35 -12
  273. package/src/utils/wire-media.test.ts +10 -0
  274. package/src/utils/wire-media.ts +6 -2
@@ -10,6 +10,15 @@ type ThreadMessageLikeContentItem = Exclude<
10
10
  export const isPendingToolCall = (c: ThreadMessageLikeContentItem): boolean =>
11
11
  c.type === "tool-call" && c.result === undefined;
12
12
 
13
+ /** A pending tool call whose nested conversation is still streaming; the outer message stays running until the nested run settles. */
14
+ export const isBackgroundToolCall = (
15
+ c: ThreadMessageLikeContentItem,
16
+ ): boolean => {
17
+ if (c.type !== "tool-call" || c.result !== undefined) return false;
18
+ const last = c.messages?.at(-1);
19
+ return last?.role === "assistant" && last.status.type === "running";
20
+ };
21
+
13
22
  export const isInterruptedToolCall = (
14
23
  c: ThreadMessageLikeContentItem,
15
24
  ): boolean => {
@@ -76,6 +85,7 @@ export const getAutoStatus = (
76
85
  hasPendingToolCalls: boolean,
77
86
  error?: ReadonlyJSONValue,
78
87
  isCancelled?: boolean,
88
+ hasBackgroundToolCalls?: boolean,
79
89
  ): MessageStatus => {
80
90
  if (isLast && error) {
81
91
  return Object.assign(
@@ -92,13 +102,26 @@ export const getAutoStatus = (
92
102
  ? AUTO_STATUS_RUNNING
93
103
  : hasInterruptedToolCalls
94
104
  ? AUTO_STATUS_INTERRUPT
95
- : hasPendingToolCalls
96
- ? AUTO_STATUS_PENDING
97
- : isCancelled
98
- ? AUTO_STATUS_CANCELLED
99
- : AUTO_STATUS_COMPLETE;
105
+ : hasBackgroundToolCalls && !isCancelled
106
+ ? AUTO_STATUS_RUNNING
107
+ : hasPendingToolCalls
108
+ ? AUTO_STATUS_PENDING
109
+ : isCancelled
110
+ ? AUTO_STATUS_CANCELLED
111
+ : AUTO_STATUS_COMPLETE;
100
112
  };
101
113
 
114
+ /** Status for a message entering the repository: the nested conversation is not being fed here, so it never keeps the message running. */
115
+ export const getRepositoryContentAutoStatus = (
116
+ content: ThreadMessageLike["content"],
117
+ ): MessageStatus =>
118
+ getAutoStatus(
119
+ false,
120
+ false,
121
+ typeof content !== "string" && content.some(isInterruptedToolCall),
122
+ typeof content !== "string" && content.some(isPendingToolCall),
123
+ );
124
+
102
125
  export const getContentAutoStatus = (
103
126
  content: ThreadMessageLike["content"],
104
127
  isLast: boolean,
@@ -109,4 +132,7 @@ export const getContentAutoStatus = (
109
132
  isRunning,
110
133
  typeof content !== "string" && content.some(isInterruptedToolCall),
111
134
  typeof content !== "string" && content.some(isPendingToolCall),
135
+ undefined,
136
+ undefined,
137
+ typeof content !== "string" && content.some(isBackgroundToolCall),
112
138
  );
@@ -1,10 +1,13 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
+ import type { ThreadMessage } from "../../types/message";
2
3
  import {
3
4
  chunkExternalMessages,
4
5
  convertExternalMessageCallback,
5
6
  convertExternalMessageChunk,
7
+ joinExternalMessages,
6
8
  type ExternalMessageConverterCallback,
7
9
  type ExternalMessageConverterCallbackResult,
10
+ type ExternalMessageConverterMessage,
8
11
  } from "./external-message-conversion";
9
12
 
10
13
  describe("convertExternalMessageCallback", () => {
@@ -30,6 +33,75 @@ describe("convertExternalMessageCallback", () => {
30
33
  });
31
34
  });
32
35
 
36
+ describe("joinExternalMessages", () => {
37
+ it("preserves strict equality for malformed numeric tool-call IDs", () => {
38
+ const messages = [
39
+ {
40
+ role: "assistant",
41
+ content: [
42
+ {
43
+ type: "tool-call",
44
+ toolCallId: 1,
45
+ toolName: "search",
46
+ args: { query: "old" },
47
+ },
48
+ ],
49
+ },
50
+ {
51
+ role: "assistant",
52
+ content: [
53
+ {
54
+ type: "tool-call",
55
+ toolCallId: 1,
56
+ toolName: "search",
57
+ args: { query: "new" },
58
+ },
59
+ ],
60
+ },
61
+ ] as unknown as ExternalMessageConverterMessage[];
62
+
63
+ expect(joinExternalMessages(messages).content).toMatchObject([
64
+ {
65
+ type: "tool-call",
66
+ toolCallId: 1,
67
+ args: { query: "new" },
68
+ },
69
+ ]);
70
+ });
71
+
72
+ it("does not merge malformed NaN tool-call IDs", () => {
73
+ const messages = [
74
+ {
75
+ role: "assistant",
76
+ content: [
77
+ {
78
+ type: "tool-call",
79
+ toolCallId: Number.NaN,
80
+ toolName: "search",
81
+ args: { query: "first" },
82
+ },
83
+ {
84
+ type: "tool-call",
85
+ toolCallId: Number.NaN,
86
+ toolName: "search",
87
+ args: { query: "second" },
88
+ },
89
+ ],
90
+ },
91
+ { role: "tool", toolCallId: Number.NaN, result: "found" },
92
+ ] as unknown as ExternalMessageConverterMessage[];
93
+
94
+ const result = joinExternalMessages(messages);
95
+
96
+ expect(result.content).toMatchObject([
97
+ { type: "tool-call", args: { query: "first" } },
98
+ { type: "tool-call", args: { query: "second" } },
99
+ ]);
100
+ expect(result.content[0]).not.toHaveProperty("result");
101
+ expect(result.content[1]).not.toHaveProperty("result");
102
+ });
103
+ });
104
+
33
105
  describe("chunkExternalMessages", () => {
34
106
  it("keeps a tool result with a non-joining assistant before starting the next chunk", () => {
35
107
  const toolCall = {};
@@ -83,6 +155,68 @@ describe("chunkExternalMessages", () => {
83
155
  });
84
156
 
85
157
  describe("convertExternalMessageChunk", () => {
158
+ it("keeps an ended outer run running for a background tool call", () => {
159
+ const nestedAssistant: ThreadMessage = {
160
+ id: "nested-assistant",
161
+ createdAt: new Date(0),
162
+ role: "assistant",
163
+ content: [],
164
+ status: { type: "running" },
165
+ metadata: {
166
+ unstable_state: {},
167
+ unstable_annotations: [],
168
+ unstable_data: [],
169
+ steps: [],
170
+ custom: {},
171
+ },
172
+ };
173
+ const result = convertExternalMessageChunk(
174
+ {
175
+ inputs: [{}],
176
+ outputs: [
177
+ {
178
+ role: "assistant",
179
+ content: [
180
+ {
181
+ type: "tool-call",
182
+ toolName: "delegate",
183
+ args: {},
184
+ messages: [nestedAssistant],
185
+ },
186
+ ],
187
+ },
188
+ ],
189
+ },
190
+ 0,
191
+ 1,
192
+ false,
193
+ undefined,
194
+ );
195
+
196
+ expect(result.status).toMatchObject({ type: "running" });
197
+ });
198
+
199
+ it("preserves assistant message modality", () => {
200
+ const result = convertExternalMessageChunk(
201
+ {
202
+ inputs: [{}],
203
+ outputs: [
204
+ {
205
+ role: "assistant",
206
+ content: "Spoken reply",
207
+ metadata: { modality: "voice" },
208
+ },
209
+ ],
210
+ },
211
+ 0,
212
+ 1,
213
+ false,
214
+ undefined,
215
+ );
216
+
217
+ expect(result.metadata.modality).toBe("voice");
218
+ });
219
+
86
220
  it("keeps separate tool calls without IDs", () => {
87
221
  const result = convertExternalMessageChunk(
88
222
  {
@@ -1,5 +1,6 @@
1
1
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
2
2
  import type { ToolExecutionStatus } from "../../runtimes/tool-invocations/ToolInvocationTracker";
3
+ import { ThreadMessageConverter } from "../../runtimes/external-store/thread-message-converter";
3
4
  import type {
4
5
  ThreadAssistantMessage,
5
6
  ThreadMessage,
@@ -11,6 +12,7 @@ import { isJSONValueEqual } from "../../utils/json/is-json-equal";
11
12
  import {
12
13
  getAutoStatus,
13
14
  isAutoStatus,
15
+ isBackgroundToolCall,
14
16
  isInterruptedToolCall,
15
17
  isPendingToolCall,
16
18
  } from "./auto-status";
@@ -132,6 +134,9 @@ const mergeInnerMessages = (existing: object, incoming: object) => ({
132
134
  ],
133
135
  });
134
136
 
137
+ const isNaNToolCallId = (toolCallId: unknown) =>
138
+ typeof toolCallId === "number" && Number.isNaN(toolCallId);
139
+
135
140
  export const joinExternalMessages = (
136
141
  messages: readonly ExternalMessageConverterMessage[],
137
142
  ): ThreadMessageLike => {
@@ -142,11 +147,13 @@ export const joinExternalMessages = (
142
147
  role: "assistant",
143
148
  content: [],
144
149
  };
150
+ const toolCallIndices = new Map<unknown, number>();
151
+ const reasoningIndices = new Map<string, number>();
145
152
  for (const output of messages) {
146
153
  if (output.role === "tool") {
147
- const toolCallIdx = assistantMessage.content.findIndex(
148
- (c) => c.type === "tool-call" && c.toolCallId === output.toolCallId,
149
- );
154
+ const toolCallIdx = !isNaNToolCallId(output.toolCallId)
155
+ ? (toolCallIndices.get(output.toolCallId) ?? -1)
156
+ : -1;
150
157
  // Ignore orphaned tool results so one bad tool message does not
151
158
  // prevent rendering the rest of the conversation.
152
159
  if (toolCallIdx !== -1) {
@@ -239,6 +246,10 @@ export const joinExternalMessages = (
239
246
  assistantMessage.metadata.timing = output.metadata.timing;
240
247
  }
241
248
 
249
+ if (output.metadata.modality) {
250
+ assistantMessage.metadata.modality = output.metadata.modality;
251
+ }
252
+
242
253
  if (output.metadata.submittedFeedback) {
243
254
  assistantMessage.metadata.submittedFeedback =
244
255
  output.metadata.submittedFeedback;
@@ -253,11 +264,8 @@ export const joinExternalMessages = (
253
264
  // Add content parts, merging reasoning parts with same parentId
254
265
  for (const part of content) {
255
266
  if (part.type === "tool-call" && part.toolCallId) {
256
- const existingIdx = assistantMessage.content.findIndex(
257
- (c) =>
258
- c.type === "tool-call" && c.toolCallId === part.toolCallId,
259
- );
260
- if (existingIdx !== -1) {
267
+ const existingIdx = toolCallIndices.get(part.toolCallId);
268
+ if (existingIdx !== undefined) {
261
269
  const existing = assistantMessage.content[
262
270
  existingIdx
263
271
  ] as typeof part;
@@ -275,13 +283,8 @@ export const joinExternalMessages = (
275
283
  "parentId" in part &&
276
284
  part.parentId
277
285
  ) {
278
- const existingIdx = assistantMessage.content.findIndex(
279
- (c) =>
280
- c.type === "reasoning" &&
281
- "parentId" in c &&
282
- c.parentId === part.parentId,
283
- );
284
- if (existingIdx !== -1) {
286
+ const existingIdx = reasoningIndices.get(part.parentId);
287
+ if (existingIdx !== undefined) {
285
288
  const existing = assistantMessage.content[
286
289
  existingIdx
287
290
  ] as typeof part;
@@ -293,7 +296,21 @@ export const joinExternalMessages = (
293
296
  continue;
294
297
  }
295
298
  }
299
+ const partIdx = assistantMessage.content.length;
296
300
  assistantMessage.content.push(part);
301
+ if (
302
+ part.type === "tool-call" &&
303
+ !isNaNToolCallId(part.toolCallId) &&
304
+ !toolCallIndices.has(part.toolCallId)
305
+ ) {
306
+ toolCallIndices.set(part.toolCallId, partIdx);
307
+ } else if (
308
+ part.type === "reasoning" &&
309
+ "parentId" in part &&
310
+ part.parentId
311
+ ) {
312
+ reasoningIndices.set(part.parentId, partIdx);
313
+ }
297
314
  }
298
315
  break;
299
316
  default: {
@@ -370,18 +387,46 @@ export const shallowArrayEqual = (
370
387
  return true;
371
388
  };
372
389
 
373
- type ExternalMessageConversionCache = {
390
+ type ExternalMessageChunkConversionCache = {
374
391
  message: ThreadMessage | undefined;
375
392
  generatedFallbackMessages: WeakSet<object>;
376
393
  };
377
394
 
395
+ type ExternalMessageConversionCallbackCacheEntry<T> =
396
+ ExternalMessageConverterCallbackResult<T> & {
397
+ metadata: ExternalMessageConverterMetadata;
398
+ callback: ExternalMessageConverterCallback<T>;
399
+ };
400
+
401
+ export type InternalExternalMessageConversionCache<
402
+ T extends WeakKey = WeakKey,
403
+ > = {
404
+ callbackCache: WeakMap<T, ExternalMessageConversionCallbackCacheEntry<T>>;
405
+ chunkCache: WeakMap<
406
+ ExternalMessageConverterMessage,
407
+ ExternalMessageConverterChunk<T>
408
+ >;
409
+ converterCache: ThreadMessageConverter;
410
+ // Generatedness is tracked by identity, not by id shape: a caller-supplied id that happens to match the generated pattern must never be rewritten.
411
+ generatedFallbackMessages: WeakSet<object>;
412
+ };
413
+
414
+ export const createExternalMessageConversionCache = <
415
+ T extends WeakKey = WeakKey,
416
+ >(): InternalExternalMessageConversionCache<T> => ({
417
+ callbackCache: new WeakMap(),
418
+ chunkCache: new WeakMap(),
419
+ converterCache: new ThreadMessageConverter(),
420
+ generatedFallbackMessages: new WeakSet(),
421
+ });
422
+
378
423
  export const convertExternalMessageChunk = <T>(
379
424
  message: ExternalMessageConverterChunk<T>,
380
425
  idx: number,
381
426
  chunkCount: number,
382
427
  isRunning: boolean,
383
428
  error: ReadonlyJSONValue | undefined,
384
- cache?: ExternalMessageConversionCache,
429
+ cache?: ExternalMessageChunkConversionCache,
385
430
  cancelledMessageIds?: ReadonlySet<string>,
386
431
  ) => {
387
432
  const isLast = idx === chunkCount - 1;
@@ -400,6 +445,9 @@ export const convertExternalMessageChunk = <T>(
400
445
  const hasPendingToolCalls =
401
446
  typeof joined.content === "object" &&
402
447
  joined.content.some(isPendingToolCall);
448
+ const hasBackgroundToolCalls =
449
+ typeof joined.content === "object" &&
450
+ joined.content.some(isBackgroundToolCall);
403
451
  const autoStatus = getAutoStatus(
404
452
  isLast,
405
453
  isRunning,
@@ -407,6 +455,7 @@ export const convertExternalMessageChunk = <T>(
407
455
  hasPendingToolCalls,
408
456
  isLast ? error : undefined,
409
457
  isCancelled,
458
+ hasBackgroundToolCalls,
410
459
  );
411
460
  const fallbackId = `${FALLBACK_ID_PREFIX}${idx}`;
412
461
 
@@ -490,21 +539,63 @@ export const convertExternalMessages = <T extends WeakKey>(
490
539
  callback: ExternalMessageConverterCallback<T>,
491
540
  isRunning: boolean,
492
541
  metadata: ExternalMessageConverterMetadata,
542
+ joinStrategy?: JoinStrategy,
543
+ cache?: InternalExternalMessageConversionCache<T>,
493
544
  ) => {
494
- const callbackResults = messages.map((message) =>
495
- convertExternalMessageCallback(message, callback, metadata),
496
- );
497
- const chunks = chunkExternalMessages(callbackResults);
498
- const result = chunks.map((message, idx) =>
499
- convertExternalMessageChunk(
500
- message,
501
- idx,
502
- chunks.length,
503
- isRunning,
504
- metadata.error,
505
- undefined,
506
- metadata.cancelledMessageIds,
507
- ),
545
+ const callbackResults = messages.map((message) => {
546
+ let result = cache?.callbackCache.get(message);
547
+ if (
548
+ !result ||
549
+ result.metadata !== metadata ||
550
+ result.callback !== callback
551
+ ) {
552
+ result = {
553
+ ...convertExternalMessageCallback(message, callback, metadata),
554
+ metadata,
555
+ callback,
556
+ };
557
+ cache?.callbackCache.set(message, result);
558
+ }
559
+ return result;
560
+ });
561
+ const chunks = chunkExternalMessages(callbackResults, joinStrategy).map(
562
+ (message) => {
563
+ const key = message.outputs[0];
564
+ if (!key || !cache) return message;
565
+
566
+ const cached = cache.chunkCache.get(key);
567
+ if (cached && shallowArrayEqual(cached.outputs, message.outputs)) {
568
+ return cached;
569
+ }
570
+ cache.chunkCache.set(key, message);
571
+ return message;
572
+ },
508
573
  );
574
+ const result = cache
575
+ ? cache.converterCache.convertMessages(chunks, (cached, message, idx) =>
576
+ convertExternalMessageChunk(
577
+ message,
578
+ idx,
579
+ chunks.length,
580
+ isRunning,
581
+ metadata.error,
582
+ {
583
+ message: cached,
584
+ generatedFallbackMessages: cache.generatedFallbackMessages,
585
+ },
586
+ metadata.cancelledMessageIds,
587
+ ),
588
+ )
589
+ : chunks.map((message, idx) =>
590
+ convertExternalMessageChunk(
591
+ message,
592
+ idx,
593
+ chunks.length,
594
+ isRunning,
595
+ metadata.error,
596
+ undefined,
597
+ metadata.cancelledMessageIds,
598
+ ),
599
+ );
509
600
  return completeExternalMessageConversion(result, metadata.error);
510
601
  };
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ThreadMessage } from "../../types/message";
3
+ import { ExportedMessageRepository } from "./message-repository";
4
+
5
+ const nestedRunningAssistant: ThreadMessage = {
6
+ id: "nested-assistant",
7
+ createdAt: new Date(0),
8
+ role: "assistant",
9
+ content: [],
10
+ status: { type: "running" },
11
+ metadata: {
12
+ unstable_state: {},
13
+ unstable_annotations: [],
14
+ unstable_data: [],
15
+ steps: [],
16
+ custom: {},
17
+ },
18
+ };
19
+
20
+ const delegating = {
21
+ id: "assistant-1",
22
+ role: "assistant" as const,
23
+ content: [
24
+ {
25
+ type: "tool-call" as const,
26
+ toolCallId: "delegate-1",
27
+ toolName: "delegate",
28
+ args: {},
29
+ argsText: "",
30
+ messages: [nestedRunningAssistant],
31
+ },
32
+ ],
33
+ };
34
+
35
+ describe("ExportedMessageRepository", () => {
36
+ it("imports a message saved mid-delegation as pending, not running", () => {
37
+ const fromArray = ExportedMessageRepository.fromArray([delegating]);
38
+ const fromBranchable = ExportedMessageRepository.fromBranchableArray([
39
+ { message: delegating, parentId: null },
40
+ ]);
41
+
42
+ for (const repository of [fromArray, fromBranchable]) {
43
+ expect(repository.messages[0]?.message.status).toMatchObject({
44
+ type: "requires-action",
45
+ reason: "tool-calls",
46
+ });
47
+ }
48
+ });
49
+ });
@@ -2,7 +2,7 @@ import type { ThreadMessage } from "../../types/message";
2
2
  import type { RunConfig } from "../../types/message";
3
3
  import { generateId } from "../../utils/id";
4
4
  import type { ThreadMessageLike } from "./thread-message-like";
5
- import { getContentAutoStatus } from "./auto-status";
5
+ import { getRepositoryContentAutoStatus } from "./auto-status";
6
6
  import { fromThreadMessageLike } from "./thread-message-like";
7
7
 
8
8
  export type ExportedMessageRepositoryItem = {
@@ -28,7 +28,7 @@ export const ExportedMessageRepository = {
28
28
  fromThreadMessageLike(
29
29
  m,
30
30
  generateId(),
31
- getContentAutoStatus(m.content, false, false),
31
+ getRepositoryContentAutoStatus(m.content),
32
32
  ),
33
33
  );
34
34
 
@@ -62,7 +62,7 @@ export const ExportedMessageRepository = {
62
62
  message: fromThreadMessageLike(
63
63
  message,
64
64
  message.id,
65
- getContentAutoStatus(message.content, false, false),
65
+ getRepositoryContentAutoStatus(message.content),
66
66
  ),
67
67
  };
68
68
  }),
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fromThreadMessageLike } from "./thread-message-like";
3
+
4
+ describe("fromThreadMessageLike", () => {
5
+ it("preserves modality on user and assistant messages", () => {
6
+ const user = fromThreadMessageLike(
7
+ {
8
+ role: "user",
9
+ content: "Hello",
10
+ metadata: { modality: "voice" },
11
+ },
12
+ "user-id",
13
+ { type: "complete", reason: "unknown" },
14
+ );
15
+ const assistant = fromThreadMessageLike(
16
+ {
17
+ role: "assistant",
18
+ content: "Hi",
19
+ metadata: { modality: "voice" },
20
+ },
21
+ "assistant-id",
22
+ { type: "complete", reason: "unknown" },
23
+ );
24
+
25
+ expect(user.metadata.modality).toBe("voice");
26
+ expect(assistant.metadata.modality).toBe("voice");
27
+ });
28
+
29
+ it("leaves modality absent when the input has none", () => {
30
+ const message = fromThreadMessageLike(
31
+ { role: "user", content: "Hello" },
32
+ "user-id",
33
+ { type: "complete", reason: "unknown" },
34
+ );
35
+
36
+ expect(message.metadata).not.toHaveProperty("modality");
37
+ });
38
+
39
+ it("ignores modality on system messages", () => {
40
+ const message = fromThreadMessageLike(
41
+ {
42
+ role: "system",
43
+ content: "Instructions",
44
+ metadata: { modality: "voice" },
45
+ },
46
+ "system-id",
47
+ { type: "complete", reason: "unknown" },
48
+ );
49
+
50
+ expect(message.metadata).not.toHaveProperty("modality");
51
+ });
52
+ });
@@ -20,6 +20,7 @@ import type {
20
20
  } from "../../types/message";
21
21
  import type { CompleteAttachment } from "../../types/attachment";
22
22
  import type {
23
+ MessageModality,
23
24
  MessageTiming,
24
25
  PartProviderMetadata,
25
26
  TextMessagePart,
@@ -88,6 +89,7 @@ export type ThreadMessageLike = {
88
89
  readonly timing?: MessageTiming | undefined;
89
90
  readonly submittedFeedback?: { readonly type: "positive" | "negative" };
90
91
  readonly isOptimistic?: boolean | undefined;
92
+ readonly modality?: MessageModality | undefined;
91
93
  readonly custom?: Record<string, unknown> | undefined;
92
94
  }
93
95
  | undefined;
@@ -223,6 +225,7 @@ export const fromThreadMessageLike = (
223
225
  submittedFeedback: metadata.submittedFeedback,
224
226
  }),
225
227
  ...(metadata?.isOptimistic && { isOptimistic: true }),
228
+ ...(metadata?.modality && { modality: metadata.modality }),
226
229
  },
227
230
  } satisfies ThreadAssistantMessage;
228
231
 
@@ -263,6 +266,7 @@ export const fromThreadMessageLike = (
263
266
  metadata: {
264
267
  custom: metadata?.custom ?? {},
265
268
  ...(metadata?.isOptimistic && { isOptimistic: true }),
269
+ ...(metadata?.modality && { modality: metadata.modality }),
266
270
  },
267
271
  } satisfies ThreadUserMessage;
268
272
 
@@ -637,6 +637,8 @@ export class ExternalStoreThreadRuntimeCore
637
637
  }
638
638
 
639
639
  public async append(rawMessage: AppendMessage): Promise<void> {
640
+ if (this._isVoiceMessage(rawMessage.sourceId))
641
+ throw new Error("Voice transcript messages cannot be edited");
640
642
  // sourceId marks an edit send; the parent may coincide with the head
641
643
  // after a resync (e.g. cancelRun dropped the edited message).
642
644
  const isEdit =
@@ -786,6 +788,8 @@ export class ExternalStoreThreadRuntimeCore
786
788
  public async startRun(config: StartRunConfig): Promise<void> {
787
789
  if (!this._store.onReload)
788
790
  throw new Error("Runtime does not support reloading messages.");
791
+ if (this._isVoiceMessage(config.sourceId))
792
+ throw new Error("Voice transcript messages cannot be reloaded");
789
793
 
790
794
  this._pendingDeleteEvictions.clear();
791
795
 
@@ -800,6 +804,8 @@ export class ExternalStoreThreadRuntimeCore
800
804
  public async resumeRun(config: ResumeRunConfig): Promise<void> {
801
805
  if (!this._store.onResume)
802
806
  throw new Error("Runtime does not support resuming runs.");
807
+ if (this._isVoiceMessage(config.sourceId))
808
+ throw new Error("Voice transcript messages cannot be reloaded");
803
809
 
804
810
  await this._store.onResume(config);
805
811
  }
@@ -961,8 +967,32 @@ export class ExternalStoreThreadRuntimeCore
961
967
  ): Promise<void> {
962
968
  if (!this._store.onRespondToToolApproval)
963
969
  throw new Error("Runtime does not support tool approvals.");
970
+ const message = this.messages.findLast(
971
+ (candidate) =>
972
+ candidate.role === "assistant" &&
973
+ candidate.content.some(
974
+ (part) =>
975
+ part.type === "tool-call" &&
976
+ part.approval?.id === options.approvalId,
977
+ ),
978
+ );
979
+ const toolCall = message?.content.find(
980
+ (part) =>
981
+ part.type === "tool-call" && part.approval?.id === options.approvalId,
982
+ );
964
983
  try {
965
- return Promise.resolve(this._store.onRespondToToolApproval(options));
984
+ return Promise.resolve(this._store.onRespondToToolApproval(options)).then(
985
+ () => {
986
+ if (message && toolCall?.type === "tool-call") {
987
+ this._notifyToolApprovalAnswered(
988
+ message.id,
989
+ toolCall.toolCallId,
990
+ toolCall.toolName,
991
+ options.approved,
992
+ );
993
+ }
994
+ },
995
+ );
966
996
  } catch (error) {
967
997
  return Promise.reject(error);
968
998
  }