@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
@@ -0,0 +1,138 @@
1
+ import { resource } from "@assistant-ui/tap";
2
+ import type { ClientOutput } from "@assistant-ui/store";
3
+ import type {
4
+ ThreadMessage,
5
+ ToolCallMessagePart,
6
+ ToolCallMessagePartStatus,
7
+ } from "../../types/message";
8
+ import {
9
+ COMPLETE_STATUS,
10
+ toMessagePartStatus,
11
+ } from "../../utils/normalizePartStatus";
12
+ import type { TaskState } from "../scopes/task";
13
+
14
+ type TaskEntry = {
15
+ readonly task: TaskState;
16
+ readonly part: ToolCallMessagePart;
17
+ readonly statusType: ToolCallMessagePartStatus["type"];
18
+ readonly statusReason: string | undefined;
19
+ readonly statusError: unknown;
20
+ readonly messages: readonly ThreadMessage[];
21
+ };
22
+
23
+ const getStatusReason = (status: ToolCallMessagePartStatus) =>
24
+ "reason" in status ? status.reason : undefined;
25
+
26
+ const getStatusError = (status: ToolCallMessagePartStatus) =>
27
+ "error" in status ? status.error : undefined;
28
+
29
+ const MAX_TASK_DEPTH = 32;
30
+
31
+ const taskKeys = new WeakMap<TaskState, string>();
32
+
33
+ /** Lookup key for a task client: its document-order index path, unique by construction where ids from nested payloads are not. */
34
+ export const getTaskKey = (task: TaskState) => taskKeys.get(task) ?? task.id;
35
+
36
+ const resolveStatus = (
37
+ message: ThreadMessage,
38
+ partIndex: number,
39
+ part: ToolCallMessagePart,
40
+ ): ToolCallMessagePartStatus =>
41
+ "status" in message && message.status
42
+ ? toMessagePartStatus(message, partIndex, part)
43
+ : COMPLETE_STATUS;
44
+
45
+ export const createTaskDeriver = () => {
46
+ let previous: readonly TaskState[] = [];
47
+ let previousEntries = new Map<string, TaskEntry>();
48
+
49
+ return (messages: readonly ThreadMessage[]): readonly TaskState[] => {
50
+ const tasks: TaskState[] = [];
51
+ const entries = new Map<string, TaskEntry>();
52
+ let allEntriesReused = true;
53
+
54
+ const visit = (
55
+ threadMessages: readonly ThreadMessage[],
56
+ parentTaskId: string | null,
57
+ depth: number,
58
+ path: string,
59
+ ) => {
60
+ if (depth > MAX_TASK_DEPTH) return;
61
+ for (const [messageIndex, message] of threadMessages.entries()) {
62
+ for (const [partIndex, part] of message.content.entries()) {
63
+ if (part.type !== "tool-call" || part.messages === undefined)
64
+ continue;
65
+
66
+ const nestedMessages = part.messages;
67
+ const status = resolveStatus(message, partIndex, part);
68
+ const statusReason = getStatusReason(status);
69
+ const statusError = getStatusError(status);
70
+ const entryKey = `${path}${messageIndex}.${partIndex}`;
71
+ const previousEntry = previousEntries.get(entryKey);
72
+ const task =
73
+ previousEntry?.part === part &&
74
+ previousEntry.statusType === status.type &&
75
+ previousEntry.statusReason === statusReason &&
76
+ Object.is(previousEntry.statusError, statusError) &&
77
+ previousEntry.messages === nestedMessages &&
78
+ previousEntry.task.messageId === message.id &&
79
+ previousEntry.task.parentTaskId === parentTaskId &&
80
+ previousEntry.task.depth === depth
81
+ ? previousEntry.task
82
+ : {
83
+ id: part.toolCallId,
84
+ toolName: part.toolName,
85
+ args: part.args,
86
+ result: part.result,
87
+ ...(part.isError === undefined
88
+ ? undefined
89
+ : { isError: part.isError }),
90
+ status,
91
+ timing: part.timing,
92
+ messageId: message.id,
93
+ parentTaskId,
94
+ depth,
95
+ messages: nestedMessages,
96
+ };
97
+
98
+ if (task !== previousEntry?.task) {
99
+ allEntriesReused = false;
100
+ taskKeys.set(task, entryKey);
101
+ }
102
+ tasks.push(task);
103
+ entries.set(entryKey, {
104
+ task,
105
+ part,
106
+ statusType: status.type,
107
+ statusReason,
108
+ statusError,
109
+ messages: nestedMessages,
110
+ });
111
+ visit(nestedMessages, task.id, depth + 1, `${entryKey}.`);
112
+ }
113
+ }
114
+ };
115
+
116
+ visit(messages, null, 0, "");
117
+
118
+ const result =
119
+ allEntriesReused &&
120
+ tasks.length === previous.length &&
121
+ tasks.every((task, index) => task === previous[index])
122
+ ? previous
123
+ : tasks;
124
+ previous = result;
125
+ previousEntries = entries;
126
+ return result;
127
+ };
128
+ };
129
+
130
+ const useTaskClient = ({
131
+ task,
132
+ }: {
133
+ task: TaskState;
134
+ }): ClientOutput<"task"> => ({
135
+ getState: () => task,
136
+ });
137
+
138
+ export const TaskClient = resource(useTaskClient);
@@ -48,6 +48,12 @@ export type {
48
48
  QueueItemMeta,
49
49
  QueueItemClientSchema,
50
50
  } from "./scopes/queue-item";
51
+ export type {
52
+ TaskState,
53
+ TaskMethods,
54
+ TaskMeta,
55
+ TaskClientSchema,
56
+ } from "./scopes/task";
51
57
  export type {
52
58
  AttachmentState,
53
59
  AttachmentMethods,
@@ -96,6 +102,8 @@ export {
96
102
  export {
97
103
  useExternalMessageConverter,
98
104
  convertExternalMessages,
105
+ createExternalMessageConversionCache,
106
+ type ExternalMessageConversionCache,
99
107
  type JoinStrategy,
100
108
  } from "../react/runtimes/external-message-converter";
101
109
  export {
@@ -50,8 +50,12 @@ describe("primitive predicates", () => {
50
50
  ).toBe(true);
51
51
  });
52
52
 
53
- it("actionBarReloadDisabled rejects user messages and busy threads", () => {
54
- const thread = { isRunning: false, isDisabled: false };
53
+ it("actionBarReloadDisabled rejects user messages, busy threads, and runtimes without reload", () => {
54
+ const thread = {
55
+ isRunning: false,
56
+ isDisabled: false,
57
+ capabilities: { reload: true },
58
+ };
55
59
  expect(
56
60
  actionBarReloadDisabled(
57
61
  state({ thread, message: { role: "assistant" } }),
@@ -68,6 +72,14 @@ describe("primitive predicates", () => {
68
72
  }),
69
73
  ),
70
74
  ).toBe(true);
75
+ expect(
76
+ actionBarReloadDisabled(
77
+ state({
78
+ thread: { ...thread, capabilities: { reload: false } },
79
+ message: { role: "assistant" },
80
+ }),
81
+ ),
82
+ ).toBe(true);
71
83
  });
72
84
 
73
85
  it("actionBarCopyDisabled requires settled content with non-empty text", () => {
@@ -109,7 +121,7 @@ describe("primitive predicates", () => {
109
121
  it("branch picker predicates respect bounds and run capabilities", () => {
110
122
  const thread = {
111
123
  isRunning: false,
112
- capabilities: { switchBranchDuringRun: false },
124
+ capabilities: { switchToBranch: true, switchBranchDuringRun: false },
113
125
  };
114
126
  const message = { branchNumber: 2, branchCount: 3 };
115
127
  expect(branchPickerPreviousDisabled(state({ thread, message }))).toBe(
@@ -131,6 +143,16 @@ describe("primitive predicates", () => {
131
143
  state({ thread: { ...thread, isRunning: true }, message }),
132
144
  ),
133
145
  ).toBe(true);
146
+ const noSwitching = {
147
+ ...thread,
148
+ capabilities: { ...thread.capabilities, switchToBranch: false },
149
+ };
150
+ expect(
151
+ branchPickerPreviousDisabled(state({ thread: noSwitching, message })),
152
+ ).toBe(true);
153
+ expect(
154
+ branchPickerNextDisabled(state({ thread: noSwitching, message })),
155
+ ).toBe(true);
134
156
  });
135
157
 
136
158
  it("suggestionTriggerDisabled gates on send only for queueless runs", () => {
@@ -174,11 +196,32 @@ describe("primitive predicates", () => {
174
196
  ),
175
197
  ).toBe(true);
176
198
 
199
+ const editable = { optional: { thread: { capabilities: { edit: true } } } };
177
200
  expect(
178
- actionBarEditDisabled(state({ composer: { isEditing: false } })),
201
+ actionBarEditDisabled(
202
+ state({ ...editable, composer: { isEditing: false } }),
203
+ ),
179
204
  ).toBe(false);
180
205
  expect(
181
- actionBarEditDisabled(state({ composer: { isEditing: true } })),
206
+ actionBarEditDisabled(
207
+ state({ ...editable, composer: { isEditing: true } }),
208
+ ),
209
+ ).toBe(true);
210
+ expect(
211
+ actionBarEditDisabled(
212
+ state({
213
+ optional: { thread: { capabilities: { edit: false } } },
214
+ composer: { isEditing: false },
215
+ }),
216
+ ),
182
217
  ).toBe(true);
218
+ expect(
219
+ actionBarEditDisabled(
220
+ state({
221
+ optional: { thread: undefined },
222
+ composer: { isEditing: false },
223
+ }),
224
+ ),
225
+ ).toBe(false);
183
226
  });
184
227
  });
@@ -20,10 +20,13 @@ export const composerInputDisabled = (s: AssistantState): boolean =>
20
20
  s.thread.isDisabled || s.composer.dictation?.inputDisabled === true;
21
21
 
22
22
  export const actionBarEditDisabled = (s: AssistantState): boolean =>
23
- s.composer.isEditing;
23
+ s.composer.isEditing || s.optional.thread?.capabilities.edit === false;
24
24
 
25
25
  export const actionBarReloadDisabled = (s: AssistantState): boolean =>
26
- s.thread.isRunning || s.thread.isDisabled || s.message.role !== "assistant";
26
+ s.thread.isRunning ||
27
+ s.thread.isDisabled ||
28
+ s.message.role !== "assistant" ||
29
+ !s.thread.capabilities.reload;
27
30
 
28
31
  export const actionBarCopyDisabled = (s: AssistantState): boolean =>
29
32
  !(
@@ -33,10 +36,12 @@ export const actionBarCopyDisabled = (s: AssistantState): boolean =>
33
36
 
34
37
  export const branchPickerPreviousDisabled = (s: AssistantState): boolean =>
35
38
  s.message.branchNumber <= 1 ||
39
+ !s.thread.capabilities.switchToBranch ||
36
40
  (s.thread.isRunning && !s.thread.capabilities.switchBranchDuringRun);
37
41
 
38
42
  export const branchPickerNextDisabled = (s: AssistantState): boolean =>
39
43
  s.message.branchNumber >= s.message.branchCount ||
44
+ !s.thread.capabilities.switchToBranch ||
40
45
  (s.thread.isRunning && !s.thread.capabilities.switchBranchDuringRun);
41
46
 
42
47
  export const suggestionTriggerDisabled = (
@@ -16,6 +16,11 @@ import {
16
16
  import { ComposerClient } from "./composer-runtime-client";
17
17
  import { MessageClient } from "./message-runtime-client";
18
18
  import { ThreadSuggestions } from "../clients/suggestions";
19
+ import {
20
+ createTaskDeriver,
21
+ getTaskKey,
22
+ TaskClient,
23
+ } from "../clients/thread-tasks";
19
24
  import { useSubscribable } from "./useSubscribable";
20
25
  import type { ThreadState } from "../scopes/thread";
21
26
 
@@ -70,6 +75,13 @@ const useThreadClient = ({
70
75
  unsubscribers.push(unsubscribe);
71
76
  }
72
77
 
78
+ unsubscribers.push(
79
+ runtime.unstable_on("toolApprovalAnswered", (payload) => {
80
+ const threadId = runtime.getState()?.threadId || "unknown";
81
+ emit("thread.toolApprovalAnswered", { threadId, ...payload });
82
+ }),
83
+ );
84
+
73
85
  return () => {
74
86
  for (const unsub of unsubscribers) unsub();
75
87
  };
@@ -102,6 +114,16 @@ const useThreadClient = ({
102
114
  const suggestions = useClientResource(
103
115
  ThreadSuggestions(runtimeState.suggestions),
104
116
  );
117
+ const taskDeriver = useMemo(() => createTaskDeriver(), []);
118
+ const tasks = useMemo(
119
+ () => taskDeriver(runtimeState.messages),
120
+ [taskDeriver, runtimeState.messages],
121
+ );
122
+ const taskClients = useClientLookup(
123
+ tasks.map((task) =>
124
+ withKey(getTaskKey(task), TaskClient({ task }), [task]),
125
+ ),
126
+ );
105
127
  const messages = useClientLookup(
106
128
  runtimeState.messages.map((m) =>
107
129
  withKey(
@@ -132,13 +154,21 @@ const useThreadClient = ({
132
154
 
133
155
  composer: composer.state,
134
156
  messages: messages.state,
157
+ tasks,
135
158
  };
136
- }, [runtimeState, messages, composer.state]);
159
+ }, [runtimeState, messages, composer.state, tasks]);
137
160
 
138
161
  return {
139
162
  getState: () => state,
140
163
  composer: () => composer.methods,
141
164
  suggestions: () => suggestions.methods,
165
+ task: (selector) => {
166
+ if ("id" in selector) {
167
+ const task = tasks.find((candidate) => candidate.id === selector.id);
168
+ return taskClients.get({ key: task ? getTaskKey(task) : selector.id });
169
+ }
170
+ return taskClients.get(selector);
171
+ },
142
172
  append: (message) => {
143
173
  const appended: Exclude<CreateAppendMessage, string> =
144
174
  typeof message === "string"
@@ -10,6 +10,7 @@ import type { SuggestionsClientSchema } from "./scopes/suggestions";
10
10
  import type { SuggestionClientSchema } from "./scopes/suggestion";
11
11
  import type { ChainOfThoughtClientSchema } from "./scopes/chain-of-thought";
12
12
  import type { QueueItemClientSchema } from "./scopes/queue-item";
13
+ import type { TaskClientSchema } from "./scopes/task";
13
14
 
14
15
  declare module "@assistant-ui/store" {
15
16
  interface ScopeRegistry {
@@ -25,5 +26,6 @@ declare module "@assistant-ui/store" {
25
26
  suggestion: SuggestionClientSchema;
26
27
  chainOfThought: ChainOfThoughtClientSchema;
27
28
  queueItem: QueueItemClientSchema;
29
+ task: TaskClientSchema;
28
30
  }
29
31
  }
@@ -0,0 +1,34 @@
1
+ import type {
2
+ ThreadMessage,
3
+ ToolCallMessagePart,
4
+ ToolCallMessagePartStatus,
5
+ } from "../../types/message";
6
+
7
+ /** A tool call that carries a nested conversation, read as one unit of child work; `id` is its toolCallId. */
8
+ export type TaskState = {
9
+ readonly id: string;
10
+ readonly toolName: string;
11
+ readonly args: ToolCallMessagePart["args"];
12
+ readonly result?: ToolCallMessagePart["result"];
13
+ readonly isError?: boolean;
14
+ readonly status: ToolCallMessagePartStatus;
15
+ readonly timing?: ToolCallMessagePart["timing"];
16
+ readonly messageId: string;
17
+ readonly parentTaskId: string | null;
18
+ readonly depth: number;
19
+ readonly messages: readonly ThreadMessage[];
20
+ };
21
+
22
+ export type TaskMethods = {
23
+ getState(): TaskState;
24
+ };
25
+
26
+ export type TaskMeta = {
27
+ source: "thread";
28
+ query: { type: "index"; index: number } | { type: "id"; id: string };
29
+ };
30
+
31
+ export type TaskClientSchema = {
32
+ methods: TaskMethods;
33
+ meta: TaskMeta;
34
+ };
@@ -18,6 +18,7 @@ import type { ModelContext } from "../../model-context/types";
18
18
  import type { MessageMethods, MessageState } from "./message";
19
19
  import type { ComposerMethods, ComposerState } from "./composer";
20
20
  import type { SuggestionsMethods } from "./suggestions";
21
+ import type { TaskMethods, TaskState } from "./task";
21
22
 
22
23
  export type ThreadState = {
23
24
  /**
@@ -44,6 +45,10 @@ export type ThreadState = {
44
45
  * The messages in the currently selected branch of the thread.
45
46
  */
46
47
  readonly messages: readonly MessageState[];
48
+ /**
49
+ * Child work derived from the thread's tool calls: every tool call that carries a nested conversation, in document order with nested tasks after their parent. The array keeps its identity while no task changed.
50
+ */
51
+ readonly tasks: readonly TaskState[];
47
52
  /**
48
53
  * The thread state.
49
54
  * @deprecated This feature is experimental
@@ -76,6 +81,10 @@ export type ThreadMethods = {
76
81
  * The suggestions shown for this thread.
77
82
  */
78
83
  suggestions(): SuggestionsMethods;
84
+ /**
85
+ * Access a task by index or toolCallId; an id resolves the first task with that toolCallId in document order.
86
+ */
87
+ task(selector: { index: number } | { id: string }): TaskMethods;
79
88
  /**
80
89
  * Append a new message to the thread.
81
90
  *
@@ -144,6 +153,13 @@ export type ThreadMeta = {
144
153
  };
145
154
 
146
155
  export type ThreadEvents = {
156
+ "thread.toolApprovalAnswered": {
157
+ threadId: string;
158
+ messageId: string;
159
+ toolCallId: string;
160
+ toolName: string;
161
+ approved: boolean;
162
+ };
147
163
  /**
148
164
  * A run started on this thread. Also observable as `isRunning` flipping to
149
165
  * `true` in thread state.
@@ -0,0 +1,209 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, render, waitFor } from "@testing-library/react";
4
+ import { useAui, useAuiState } from "@assistant-ui/store";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import type { ThreadMessage } from "../types/message";
7
+ import { AssistantRuntimeProvider } from "../react/AssistantRuntimeProvider";
8
+ import { useExternalStoreRuntime } from "../react/runtimes/useExternalStoreRuntime";
9
+ import type { TaskState } from "./scopes/task";
10
+
11
+ const taskMessage = (
12
+ nestedStatus: { type: "running" } | { type: "complete"; reason: "stop" },
13
+ result?: unknown,
14
+ ) =>
15
+ ({
16
+ id: "message-1",
17
+ role: "assistant",
18
+ createdAt: new Date(0),
19
+ content: [
20
+ {
21
+ type: "tool-call",
22
+ toolCallId: "delegate-1",
23
+ toolName: "delegate",
24
+ args: { topic: "research" },
25
+ argsText: '{"topic":"research"}',
26
+ ...(result !== undefined ? { result } : undefined),
27
+ messages: [
28
+ {
29
+ id: "nested-message-1",
30
+ role: "assistant",
31
+ createdAt: new Date(0),
32
+ content: [],
33
+ status: nestedStatus,
34
+ metadata: {
35
+ unstable_state: {},
36
+ unstable_annotations: [],
37
+ unstable_data: [],
38
+ steps: [],
39
+ custom: {},
40
+ },
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ status: nestedStatus,
46
+ metadata: {
47
+ unstable_state: {},
48
+ unstable_annotations: [],
49
+ unstable_data: [],
50
+ steps: [],
51
+ custom: {},
52
+ },
53
+ }) as ThreadMessage;
54
+
55
+ afterEach(cleanup);
56
+
57
+ describe("thread tasks", () => {
58
+ it("exposes task state and accessors through an external-store runtime", async () => {
59
+ const captured: {
60
+ tasks?: readonly TaskState[];
61
+ byId?: TaskState;
62
+ byIndex?: TaskState;
63
+ } = {};
64
+
65
+ const Probe = () => {
66
+ const tasks = useAuiState((s) => s.thread.tasks);
67
+ const aui = useAui();
68
+ captured.tasks = tasks;
69
+ if (tasks.length > 0) {
70
+ captured.byId = aui.thread.task({ id: "delegate-1" }).getState();
71
+ captured.byIndex = aui.thread.task({ index: 0 }).getState();
72
+ }
73
+ return null;
74
+ };
75
+
76
+ const App = ({ messages }: { messages: readonly ThreadMessage[] }) => {
77
+ const runtime = useExternalStoreRuntime({
78
+ messages,
79
+ convertMessage: (message) => message,
80
+ onNew: async () => {},
81
+ });
82
+ return (
83
+ <AssistantRuntimeProvider runtime={runtime}>
84
+ <Probe />
85
+ </AssistantRuntimeProvider>
86
+ );
87
+ };
88
+
89
+ const view = render(<App messages={[taskMessage({ type: "running" })]} />);
90
+
91
+ await waitFor(() => expect(captured.tasks).toHaveLength(1));
92
+ expect(captured.tasks?.[0]).toMatchObject({
93
+ status: { type: "running" },
94
+ toolName: "delegate",
95
+ });
96
+ expect(captured.byId).toMatchObject({ messageId: "message-1" });
97
+ expect(captured.byIndex).toBe(captured.byId);
98
+
99
+ view.rerender(
100
+ <App
101
+ messages={[taskMessage({ type: "complete", reason: "stop" }, "done")]}
102
+ />,
103
+ );
104
+
105
+ await waitFor(() =>
106
+ expect(captured.tasks?.[0]?.status).toEqual({ type: "complete" }),
107
+ );
108
+ expect(captured.tasks?.[0]).toMatchObject({
109
+ toolName: "delegate",
110
+ messageId: "message-1",
111
+ result: "done",
112
+ });
113
+ expect(captured.tasks?.[0]?.messages).toHaveLength(1);
114
+ });
115
+
116
+ it("survives nested payloads that repeat ids and resolves the first task in document order", async () => {
117
+ const nested = (id: string, toolCallId: string) =>
118
+ ({
119
+ id,
120
+ role: "assistant",
121
+ createdAt: new Date(0),
122
+ content: [
123
+ {
124
+ type: "tool-call",
125
+ toolCallId,
126
+ toolName: "search",
127
+ args: {},
128
+ argsText: "{}",
129
+ result: "ok",
130
+ messages: [],
131
+ },
132
+ ],
133
+ status: { type: "complete", reason: "stop" },
134
+ metadata: {
135
+ unstable_state: {},
136
+ unstable_annotations: [],
137
+ unstable_data: [],
138
+ steps: [],
139
+ custom: {},
140
+ },
141
+ }) as ThreadMessage;
142
+ const message = {
143
+ id: "message-1",
144
+ role: "assistant",
145
+ createdAt: new Date(0),
146
+ content: [
147
+ {
148
+ type: "tool-call",
149
+ toolCallId: "delegate-a",
150
+ toolName: "delegate",
151
+ args: {},
152
+ argsText: "{}",
153
+ result: "ok",
154
+ messages: [nested("nested", "call_1")],
155
+ },
156
+ {
157
+ type: "tool-call",
158
+ toolCallId: "delegate-b",
159
+ toolName: "delegate",
160
+ args: {},
161
+ argsText: "{}",
162
+ result: "ok",
163
+ messages: [nested("nested", "call_1")],
164
+ },
165
+ ],
166
+ status: { type: "complete", reason: "stop" },
167
+ metadata: {
168
+ unstable_state: {},
169
+ unstable_annotations: [],
170
+ unstable_data: [],
171
+ steps: [],
172
+ custom: {},
173
+ },
174
+ } as ThreadMessage;
175
+ const captured: { ids?: readonly string[]; first?: TaskState } = {};
176
+ const Probe = () => {
177
+ const tasks = useAuiState((s) => s.thread.tasks);
178
+ const aui = useAui();
179
+ captured.ids = tasks.map((task) => task.id);
180
+ if (tasks.length === 4) {
181
+ captured.first = aui.thread.task({ id: "call_1" }).getState();
182
+ }
183
+ return null;
184
+ };
185
+ const App = () => {
186
+ const runtime = useExternalStoreRuntime({
187
+ messages: [message],
188
+ convertMessage: (m) => m,
189
+ onNew: async () => {},
190
+ });
191
+ return (
192
+ <AssistantRuntimeProvider runtime={runtime}>
193
+ <Probe />
194
+ </AssistantRuntimeProvider>
195
+ );
196
+ };
197
+
198
+ render(<App />);
199
+
200
+ await waitFor(() => expect(captured.ids).toHaveLength(4));
201
+ expect(captured.ids).toEqual([
202
+ "delegate-a",
203
+ "call_1",
204
+ "delegate-b",
205
+ "call_1",
206
+ ]);
207
+ expect(captured.first).toMatchObject({ parentTaskId: "delegate-a" });
208
+ });
209
+ });