@assistant-ui/core 0.2.22 → 0.2.23

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 (214) hide show
  1. package/dist/adapters/thread-history.d.ts +8 -0
  2. package/dist/adapters/thread-history.d.ts.map +1 -1
  3. package/dist/internal.d.ts +3 -2
  4. package/dist/internal.js +3 -2
  5. package/dist/model-context/frame/host.d.ts.map +1 -1
  6. package/dist/model-context/frame/host.js +36 -21
  7. package/dist/model-context/frame/host.js.map +1 -1
  8. package/dist/model-context/types.d.ts.map +1 -1
  9. package/dist/model-context/types.js +13 -4
  10. package/dist/model-context/types.js.map +1 -1
  11. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  12. package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -0
  13. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  14. package/dist/react/client/interactable-model-context.js +1 -0
  15. package/dist/react/client/interactable-model-context.js.map +1 -1
  16. package/dist/react/interactables-legacy/useInteractableState.js +1 -0
  17. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
  18. package/dist/react/model-context/useInteractableState.js +1 -0
  19. package/dist/react/model-context/useInteractableState.js.map +1 -1
  20. package/dist/react/model-context/useInteractableVersions.js +1 -0
  21. package/dist/react/model-context/useInteractableVersions.js.map +1 -1
  22. package/dist/react/model-context/useToolArgsStatus.js +1 -0
  23. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  24. package/dist/react/primitive-hooks/useActionBarCopy.js +1 -0
  25. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  26. package/dist/react/primitive-hooks/useActionBarEdit.js +1 -0
  27. package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
  28. package/dist/react/primitive-hooks/useActionBarFeedback.js +1 -0
  29. package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
  30. package/dist/react/primitive-hooks/useActionBarReload.js +1 -0
  31. package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
  32. package/dist/react/primitive-hooks/useActionBarSpeak.js +1 -0
  33. package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
  34. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +1 -0
  35. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
  36. package/dist/react/primitive-hooks/useBranchPickerNext.js +1 -0
  37. package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
  38. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +1 -0
  39. package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
  40. package/dist/react/primitive-hooks/useComposerAddAttachment.js +1 -0
  41. package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
  42. package/dist/react/primitive-hooks/useComposerCancel.js +1 -0
  43. package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
  44. package/dist/react/primitive-hooks/useComposerDictate.js +1 -0
  45. package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
  46. package/dist/react/primitive-hooks/useComposerSend.js +1 -0
  47. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  48. package/dist/react/primitive-hooks/useEditComposerCancel.js +1 -0
  49. package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
  50. package/dist/react/primitive-hooks/useEditComposerSend.js +1 -0
  51. package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
  52. package/dist/react/primitive-hooks/useMessageBranching.js +1 -0
  53. package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
  54. package/dist/react/primitive-hooks/useMessageReload.js +1 -0
  55. package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
  56. package/dist/react/primitive-hooks/useSuggestionTrigger.js +1 -0
  57. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  58. package/dist/react/primitive-hooks/useThreadListItemArchive.js +1 -0
  59. package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
  60. package/dist/react/primitive-hooks/useThreadListItemDelete.js +1 -0
  61. package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
  62. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +1 -0
  63. package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
  64. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +1 -0
  65. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
  66. package/dist/react/primitive-hooks/useThreadListLoadMore.js +1 -0
  67. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  68. package/dist/react/primitive-hooks/useThreadListNew.js +1 -0
  69. package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
  70. package/dist/react/providers/TextMessagePartProvider.js +1 -0
  71. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  72. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  73. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +6 -4
  74. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  75. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
  76. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  77. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +11 -1
  78. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  79. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
  80. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  81. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +1 -1
  82. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  83. package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
  84. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  85. package/dist/react/runtimes/cloud/auiV0.js +2 -1
  86. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  87. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
  88. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  89. package/dist/react/runtimes/external-message-converter.js +1 -1
  90. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  91. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
  92. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
  93. package/dist/react/runtimes/useLocalRuntime.d.ts +0 -1
  94. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  95. package/dist/runtime/api/assistant-runtime.d.ts +1 -1
  96. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  97. package/dist/runtime/api/assistant-runtime.js +1 -1
  98. package/dist/runtime/api/assistant-runtime.js.map +1 -1
  99. package/dist/runtime/api/attachment-runtime.d.ts +1 -1
  100. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  101. package/dist/runtime/api/attachment-runtime.js +1 -1
  102. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  103. package/dist/runtime/api/composer-runtime.d.ts +2 -2
  104. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  105. package/dist/runtime/api/composer-runtime.js +2 -2
  106. package/dist/runtime/api/composer-runtime.js.map +1 -1
  107. package/dist/runtime/api/message-part-runtime.d.ts +4 -4
  108. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  109. package/dist/runtime/api/message-part-runtime.js +3 -3
  110. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  111. package/dist/runtime/api/message-runtime.d.ts +1 -1
  112. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  113. package/dist/runtime/api/message-runtime.js +2 -2
  114. package/dist/runtime/api/message-runtime.js.map +1 -1
  115. package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
  116. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  117. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  118. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  119. package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
  120. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  121. package/dist/runtime/api/thread-list-runtime.js +1 -1
  122. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  123. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
  124. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  125. package/dist/runtime/base/base-thread-runtime-core.js +1 -1
  126. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  127. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
  128. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  129. package/dist/runtime/base/default-edit-composer-runtime-core.js +2 -2
  130. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  131. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  132. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  133. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  134. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  135. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  136. package/dist/runtime/utils/message-repository.js +4 -2
  137. package/dist/runtime/utils/message-repository.js.map +1 -1
  138. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
  139. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  140. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
  141. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  142. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  143. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -0
  144. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  145. package/dist/runtimes/internal.d.ts +2 -1
  146. package/dist/runtimes/internal.js +2 -1
  147. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
  148. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  149. package/dist/runtimes/local/local-thread-runtime-core.js +44 -13
  150. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  151. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
  152. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  153. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  154. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  155. package/dist/store/clients/no-op-composer-client.js +1 -0
  156. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  157. package/dist/store/runtime-clients/thread-list-runtime-client.js +1 -0
  158. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  159. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  160. package/dist/store/runtime-clients/thread-runtime-client.js +17 -15
  161. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  162. package/dist/store/scopes/thread.d.ts +1 -0
  163. package/dist/store/scopes/thread.d.ts.map +1 -1
  164. package/dist/subscribable/subscribable.d.ts +3 -3
  165. package/dist/subscribable/subscribable.d.ts.map +1 -1
  166. package/dist/subscribable/subscribable.js +3 -3
  167. package/dist/subscribable/subscribable.js.map +1 -1
  168. package/package.json +7 -7
  169. package/src/adapters/thread-history.ts +8 -0
  170. package/src/internal.ts +1 -1
  171. package/src/model-context/frame/host.test.ts +47 -0
  172. package/src/model-context/frame/host.ts +46 -19
  173. package/src/model-context/types.ts +16 -6
  174. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +15 -5
  175. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +20 -6
  176. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
  177. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +26 -5
  178. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +5 -1
  179. package/src/react/runtimes/cloud/auiV0.ts +14 -0
  180. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
  181. package/src/react/runtimes/external-message-converter.test.ts +346 -0
  182. package/src/react/runtimes/external-message-converter.ts +1 -1
  183. package/src/react/runtimes/useLocalRuntime.test.tsx +43 -0
  184. package/src/runtime/api/assistant-runtime.ts +4 -1
  185. package/src/runtime/api/attachment-runtime.ts +8 -2
  186. package/src/runtime/api/composer-runtime.ts +9 -5
  187. package/src/runtime/api/message-part-runtime.ts +10 -3
  188. package/src/runtime/api/message-runtime.ts +7 -2
  189. package/src/runtime/api/thread-list-item-runtime.ts +7 -2
  190. package/src/runtime/api/thread-list-runtime.ts +10 -2
  191. package/src/runtime/base/base-thread-runtime-core.ts +5 -1
  192. package/src/runtime/base/default-edit-composer-runtime-core.ts +14 -2
  193. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  194. package/src/runtime/utils/message-repository.ts +19 -13
  195. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
  196. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
  197. package/src/runtimes/internal.ts +3 -0
  198. package/src/runtimes/local/local-thread-runtime-core.test.ts +596 -0
  199. package/src/runtimes/local/local-thread-runtime-core.ts +85 -21
  200. package/src/runtimes/local/should-continue.test.ts +122 -0
  201. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +9 -10
  202. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +38 -32
  203. package/src/store/clients/model-context-client.test.ts +21 -1
  204. package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
  205. package/src/store/scopes/thread.ts +1 -0
  206. package/src/subscribable/subscribable.ts +16 -8
  207. package/src/tests/MessageRepository.test.ts +12 -0
  208. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
  209. package/src/tests/auiV0Encode.test.ts +74 -0
  210. package/src/tests/base-composer-runtime-core.test.ts +422 -0
  211. package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
  212. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
  213. package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
  214. package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
@@ -76,94 +76,96 @@ export const useCloudThreadListAdapter = (
76
76
  );
77
77
 
78
78
  const cloud = adapter.cloud ?? autoCloud;
79
- if (!cloud) {
80
- const ref = adapterRef;
81
- const inMemory = new InMemoryThreadListAdapter();
82
- inMemory.initialize = async (threadId: string) => {
83
- const result = await ref.current.create?.();
84
- return { remoteId: threadId, externalId: result?.externalId };
85
- };
86
- return inMemory;
87
- }
88
-
89
- return {
90
- list: async () => {
91
- const { threads } = await cloud.threads.list();
92
- return {
93
- threads: threads.map((t) => ({
94
- status: t.is_archived ? "archived" : "regular",
95
- remoteId: t.id,
96
- title: t.title,
97
- lastMessageAt: t.last_message_at
98
- ? new Date(t.last_message_at)
99
- : undefined,
100
- externalId: t.external_id ?? undefined,
101
- custom: toCustom(t.metadata),
102
- })),
79
+ return useMemo<RemoteThreadListAdapter>(() => {
80
+ if (!cloud) {
81
+ const ref = adapterRef;
82
+ const inMemory = new InMemoryThreadListAdapter();
83
+ inMemory.initialize = async (threadId: string) => {
84
+ const result = await ref.current.create?.();
85
+ return { remoteId: threadId, externalId: result?.externalId };
103
86
  };
104
- },
105
-
106
- initialize: async () => {
107
- const createTask = adapter.create?.() ?? Promise.resolve();
108
- const t = await createTask;
109
- const external_id = t ? t.externalId : undefined;
110
- const { thread_id: remoteId } = await cloud.threads.create({
111
- last_message_at: new Date(),
112
- external_id,
113
- });
114
-
115
- return { externalId: external_id, remoteId: remoteId };
116
- },
117
-
118
- rename: async (threadId, newTitle) => {
119
- return cloud.threads.update(threadId, { title: newTitle });
120
- },
121
- updateCustom: async (threadId, custom) => {
122
- return cloud.threads.update(threadId, { metadata: custom ?? null });
123
- },
124
- archive: async (threadId) => {
125
- return cloud.threads.update(threadId, { is_archived: true });
126
- },
127
- unarchive: async (threadId) => {
128
- return cloud.threads.update(threadId, { is_archived: false });
129
- },
130
- delete: async (threadId) => {
131
- await adapter.delete?.(threadId);
132
- return cloud.threads.delete(threadId);
133
- },
134
-
135
- generateTitle: async (threadId, messages) => {
136
- // Filter messages to only include content types the title generator understands
137
- // (reasoning, source, etc. are not needed for title generation)
138
- // TODO serialize these to a more efficient format
139
- const filteredMessages = messages.map((msg) => ({
140
- ...msg,
141
- content: msg.content.filter(
142
- (part) => part.type === "text" || part.type === "tool-call",
143
- ),
144
- }));
145
-
146
- return cloud.runs.stream({
147
- thread_id: threadId,
148
- assistant_id: "system/thread_title",
149
- messages: filteredMessages,
150
- });
151
- },
152
-
153
- fetch: async (threadId: string) => {
154
- const thread = await cloud.threads.get(threadId);
155
- return {
156
- status: thread.is_archived ? "archived" : "regular",
157
- remoteId: thread.id,
158
- title: thread.title,
159
- lastMessageAt: thread.last_message_at
160
- ? new Date(thread.last_message_at)
161
- : undefined,
162
- externalId: thread.external_id ?? undefined,
163
- custom: toCustom(thread.metadata),
164
- };
165
- },
87
+ return inMemory;
88
+ }
89
+
90
+ return {
91
+ list: async () => {
92
+ const { threads } = await cloud.threads.list();
93
+ return {
94
+ threads: threads.map((t) => ({
95
+ status: t.is_archived ? "archived" : "regular",
96
+ remoteId: t.id,
97
+ title: t.title,
98
+ lastMessageAt: t.last_message_at
99
+ ? new Date(t.last_message_at)
100
+ : undefined,
101
+ externalId: t.external_id ?? undefined,
102
+ custom: toCustom(t.metadata),
103
+ })),
104
+ };
105
+ },
106
+
107
+ initialize: async () => {
108
+ const createTask = adapterRef.current.create?.() ?? Promise.resolve();
109
+ const t = await createTask;
110
+ const external_id = t ? t.externalId : undefined;
111
+ const { thread_id: remoteId } = await cloud.threads.create({
112
+ last_message_at: new Date(),
113
+ external_id,
114
+ });
115
+
116
+ return { externalId: external_id, remoteId: remoteId };
117
+ },
118
+
119
+ rename: async (threadId, newTitle) => {
120
+ return cloud.threads.update(threadId, { title: newTitle });
121
+ },
122
+ updateCustom: async (threadId, custom) => {
123
+ return cloud.threads.update(threadId, { metadata: custom ?? null });
124
+ },
125
+ archive: async (threadId) => {
126
+ return cloud.threads.update(threadId, { is_archived: true });
127
+ },
128
+ unarchive: async (threadId) => {
129
+ return cloud.threads.update(threadId, { is_archived: false });
130
+ },
131
+ delete: async (threadId) => {
132
+ await adapterRef.current.delete?.(threadId);
133
+ return cloud.threads.delete(threadId);
134
+ },
135
+
136
+ generateTitle: async (threadId, messages) => {
137
+ // Filter messages to only include content types the title generator understands
138
+ // (reasoning, source, etc. are not needed for title generation)
139
+ // TODO serialize these to a more efficient format
140
+ const filteredMessages = messages.map((msg) => ({
141
+ ...msg,
142
+ content: msg.content.filter(
143
+ (part) => part.type === "text" || part.type === "tool-call",
144
+ ),
145
+ }));
146
+
147
+ return cloud.runs.stream({
148
+ thread_id: threadId,
149
+ assistant_id: "system/thread_title",
150
+ messages: filteredMessages,
151
+ });
152
+ },
153
+
154
+ fetch: async (threadId: string) => {
155
+ const thread = await cloud.threads.get(threadId);
156
+ return {
157
+ status: thread.is_archived ? "archived" : "regular",
158
+ remoteId: thread.id,
159
+ title: thread.title,
160
+ lastMessageAt: thread.last_message_at
161
+ ? new Date(thread.last_message_at)
162
+ : undefined,
163
+ externalId: thread.external_id ?? undefined,
164
+ custom: toCustom(thread.metadata),
165
+ };
166
+ },
166
167
 
167
- unstable_Provider,
168
- };
168
+ unstable_Provider,
169
+ };
170
+ }, [cloud, unstable_Provider]);
169
171
  };
@@ -0,0 +1,346 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { convertExternalMessages } from "./external-message-converter";
3
+ import type { useExternalMessageConverter } from "./external-message-converter";
4
+ import { isErrorMessageId } from "../../utils/id";
5
+
6
+ describe("convertExternalMessages", () => {
7
+ describe("reasoning part merging", () => {
8
+ it("should merge reasoning parts with the same parentId", () => {
9
+ const messages = [
10
+ {
11
+ id: "msg1",
12
+ role: "assistant" as const,
13
+ content: [
14
+ {
15
+ type: "reasoning" as const,
16
+ text: "First reasoning",
17
+ parentId: "parent1",
18
+ },
19
+ ],
20
+ },
21
+ {
22
+ id: "msg2",
23
+ role: "assistant" as const,
24
+ content: [
25
+ {
26
+ type: "reasoning" as const,
27
+ text: "Second reasoning",
28
+ parentId: "parent1",
29
+ },
30
+ ],
31
+ },
32
+ ];
33
+
34
+ const callback: useExternalMessageConverter.Callback<
35
+ (typeof messages)[number]
36
+ > = (msg) => msg;
37
+
38
+ const result = convertExternalMessages(messages, callback, false, {});
39
+
40
+ expect(result).toHaveLength(1);
41
+ expect(result[0]!.role).toBe("assistant");
42
+
43
+ const reasoningParts = result[0]!.content.filter(
44
+ (p) => p.type === "reasoning",
45
+ );
46
+ expect(reasoningParts).toHaveLength(1);
47
+ expect((reasoningParts[0] as any).text).toBe(
48
+ "First reasoning\n\nSecond reasoning",
49
+ );
50
+ expect((reasoningParts[0] as any).parentId).toBe("parent1");
51
+ });
52
+
53
+ it("should keep reasoning parts without parentId separate", () => {
54
+ const messages = [
55
+ {
56
+ id: "msg1",
57
+ role: "assistant" as const,
58
+ content: [{ type: "reasoning" as const, text: "First reasoning" }],
59
+ },
60
+ {
61
+ id: "msg2",
62
+ role: "assistant" as const,
63
+ content: [{ type: "reasoning" as const, text: "Second reasoning" }],
64
+ },
65
+ ];
66
+
67
+ const callback: useExternalMessageConverter.Callback<
68
+ (typeof messages)[number]
69
+ > = (msg) => msg;
70
+
71
+ const result = convertExternalMessages(messages, callback, false, {});
72
+
73
+ expect(result).toHaveLength(1);
74
+
75
+ const reasoningParts = result[0]!.content.filter(
76
+ (p) => p.type === "reasoning",
77
+ );
78
+ expect(reasoningParts).toHaveLength(2);
79
+ expect((reasoningParts[0] as any).text).toBe("First reasoning");
80
+ expect((reasoningParts[1] as any).text).toBe("Second reasoning");
81
+ });
82
+
83
+ it("should keep reasoning parts with different parentIds separate", () => {
84
+ const messages = [
85
+ {
86
+ id: "msg1",
87
+ role: "assistant" as const,
88
+ content: [
89
+ {
90
+ type: "reasoning" as const,
91
+ text: "Reasoning for parent1",
92
+ parentId: "parent1",
93
+ },
94
+ ],
95
+ },
96
+ {
97
+ id: "msg2",
98
+ role: "assistant" as const,
99
+ content: [
100
+ {
101
+ type: "reasoning" as const,
102
+ text: "Reasoning for parent2",
103
+ parentId: "parent2",
104
+ },
105
+ ],
106
+ },
107
+ ];
108
+
109
+ const callback: useExternalMessageConverter.Callback<
110
+ (typeof messages)[number]
111
+ > = (msg) => msg;
112
+
113
+ const result = convertExternalMessages(messages, callback, false, {});
114
+
115
+ expect(result).toHaveLength(1);
116
+
117
+ const reasoningParts = result[0]!.content.filter(
118
+ (p) => p.type === "reasoning",
119
+ );
120
+ expect(reasoningParts).toHaveLength(2);
121
+ expect((reasoningParts[0] as any).parentId).toBe("parent1");
122
+ expect((reasoningParts[1] as any).parentId).toBe("parent2");
123
+ });
124
+
125
+ it("should still merge tool results with matching tool calls", () => {
126
+ const messages = [
127
+ {
128
+ id: "msg1",
129
+ role: "assistant" as const,
130
+ content: [
131
+ {
132
+ type: "tool-call" as const,
133
+ toolCallId: "tc1",
134
+ toolName: "search",
135
+ args: { query: "test" },
136
+ argsText: '{"query":"test"}',
137
+ },
138
+ ],
139
+ },
140
+ {
141
+ role: "tool" as const,
142
+ toolCallId: "tc1",
143
+ result: { data: "result" },
144
+ },
145
+ ];
146
+
147
+ const callback: useExternalMessageConverter.Callback<
148
+ (typeof messages)[number]
149
+ > = (msg) => msg;
150
+
151
+ const result = convertExternalMessages(messages, callback, false, {});
152
+
153
+ expect(result).toHaveLength(1);
154
+
155
+ const toolCallParts = result[0]!.content.filter(
156
+ (p) => p.type === "tool-call",
157
+ );
158
+ expect(toolCallParts).toHaveLength(1);
159
+ expect((toolCallParts[0] as any).result).toEqual({ data: "result" });
160
+ });
161
+
162
+ it("should merge duplicate tool calls by toolCallId across assistant messages", () => {
163
+ const messages = [
164
+ {
165
+ id: "msg1",
166
+ role: "assistant" as const,
167
+ content: [
168
+ {
169
+ type: "tool-call" as const,
170
+ toolCallId: "tc1",
171
+ toolName: "search",
172
+ args: { query: "old" },
173
+ argsText: '{"query":"old"',
174
+ },
175
+ ],
176
+ },
177
+ {
178
+ id: "msg2",
179
+ role: "assistant" as const,
180
+ content: [
181
+ {
182
+ type: "tool-call" as const,
183
+ toolCallId: "tc1",
184
+ toolName: "search",
185
+ args: { query: "new" },
186
+ argsText: '{"query":"new"}',
187
+ },
188
+ ],
189
+ },
190
+ ];
191
+
192
+ const callback: useExternalMessageConverter.Callback<
193
+ (typeof messages)[number]
194
+ > = (msg) => msg;
195
+
196
+ const result = convertExternalMessages(messages, callback, false, {});
197
+
198
+ expect(result).toHaveLength(1);
199
+ expect(result[0]!.role).toBe("assistant");
200
+ const toolCallParts = result[0]!.content.filter(
201
+ (p) => p.type === "tool-call",
202
+ );
203
+ expect(toolCallParts).toHaveLength(1);
204
+ expect((toolCallParts[0] as any).args).toEqual({ query: "new" });
205
+ expect((toolCallParts[0] as any).argsText).toBe('{"query":"new"}');
206
+ });
207
+
208
+ it("should ignore orphaned tool results without throwing", () => {
209
+ const messages = [
210
+ {
211
+ id: "msg1",
212
+ role: "assistant" as const,
213
+ content: "First response",
214
+ },
215
+ {
216
+ role: "tool" as const,
217
+ toolCallId: "missing-tool-call",
218
+ toolName: "search",
219
+ result: { data: "orphan result" },
220
+ },
221
+ {
222
+ id: "msg2",
223
+ role: "assistant" as const,
224
+ content: "Second response",
225
+ },
226
+ ];
227
+
228
+ const callback: useExternalMessageConverter.Callback<
229
+ (typeof messages)[number]
230
+ > = (msg) => msg;
231
+
232
+ const result = convertExternalMessages(messages, callback, false, {});
233
+ expect(result).toHaveLength(1);
234
+ expect(result[0]!.role).toBe("assistant");
235
+
236
+ const textParts = result[0]!.content.filter((p) => p.type === "text");
237
+ expect(textParts).toHaveLength(2);
238
+ expect((textParts[0] as any).text).toBe("First response");
239
+ expect((textParts[1] as any).text).toBe("Second response");
240
+ });
241
+ });
242
+
243
+ describe("synthetic error message", () => {
244
+ it("should create synthetic error message when error exists and no messages", () => {
245
+ const messages: never[] = [];
246
+ const callback: useExternalMessageConverter.Callback<never> = (msg) =>
247
+ msg;
248
+
249
+ const result = convertExternalMessages(messages, callback, false, {
250
+ error: "API key is missing",
251
+ });
252
+
253
+ expect(result).toHaveLength(1);
254
+ expect(result[0]!.role).toBe("assistant");
255
+ expect(result[0]!.content).toHaveLength(0);
256
+ expect(result[0]!.status).toEqual({
257
+ type: "incomplete",
258
+ reason: "error",
259
+ error: "API key is missing",
260
+ });
261
+ expect(isErrorMessageId(result[0]!.id)).toBe(true);
262
+ });
263
+
264
+ it("should create synthetic error message when error exists and last message is user", () => {
265
+ const messages = [
266
+ {
267
+ id: "user1",
268
+ role: "user" as const,
269
+ content: "Hello",
270
+ },
271
+ ];
272
+
273
+ const callback: useExternalMessageConverter.Callback<
274
+ (typeof messages)[number]
275
+ > = (msg) => msg;
276
+
277
+ const result = convertExternalMessages(messages, callback, false, {
278
+ error: { message: "Invalid API key" },
279
+ });
280
+
281
+ expect(result).toHaveLength(2);
282
+ expect(result[0]!.role).toBe("user");
283
+ expect(result[1]!.role).toBe("assistant");
284
+ expect(result[1]!.content).toHaveLength(0);
285
+ expect(result[1]!.status).toEqual({
286
+ type: "incomplete",
287
+ reason: "error",
288
+ error: { message: "Invalid API key" },
289
+ });
290
+ expect(isErrorMessageId(result[1]!.id)).toBe(true);
291
+ });
292
+
293
+ it("should not create synthetic error message when last message is assistant", () => {
294
+ const messages = [
295
+ {
296
+ id: "user1",
297
+ role: "user" as const,
298
+ content: "Hello",
299
+ },
300
+ {
301
+ id: "assistant1",
302
+ role: "assistant" as const,
303
+ content: "Hi there",
304
+ },
305
+ ];
306
+
307
+ const callback: useExternalMessageConverter.Callback<
308
+ (typeof messages)[number]
309
+ > = (msg) => msg;
310
+
311
+ const result = convertExternalMessages(messages, callback, false, {
312
+ error: "Connection error",
313
+ });
314
+
315
+ expect(result).toHaveLength(2);
316
+ expect(result[0]!.role).toBe("user");
317
+ expect(result[1]!.role).toBe("assistant");
318
+ expect(result[1]!.id).toBe("assistant1");
319
+ expect(result[1]!.status).toMatchObject({
320
+ type: "incomplete",
321
+ reason: "error",
322
+ error: "Connection error",
323
+ });
324
+ expect(isErrorMessageId(result[1]!.id)).toBe(false);
325
+ });
326
+
327
+ it("should not create synthetic message when no error", () => {
328
+ const messages = [
329
+ {
330
+ id: "user1",
331
+ role: "user" as const,
332
+ content: "Hello",
333
+ },
334
+ ];
335
+
336
+ const callback: useExternalMessageConverter.Callback<
337
+ (typeof messages)[number]
338
+ > = (msg) => msg;
339
+
340
+ const result = convertExternalMessages(messages, callback, false, {});
341
+
342
+ expect(result).toHaveLength(1);
343
+ expect(result[0]!.role).toBe("user");
344
+ });
345
+ });
346
+ });
@@ -118,7 +118,7 @@ const joinExternalMessages = (
118
118
  const toolCall = assistantMessage.content[
119
119
  toolCallIdx
120
120
  ]! as ToolCallMessagePart;
121
- if (output.toolName !== undefined) {
121
+ if (output.toolName != null) {
122
122
  if (toolCall.toolName !== output.toolName)
123
123
  throw new Error(
124
124
  `Tool call name ${output.toolCallId} ${output.toolName} does not match existing tool call ${toolCall.toolName}`,
@@ -3,6 +3,7 @@
3
3
  import { render, waitFor } from "@testing-library/react";
4
4
  import { StrictMode } from "react";
5
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import type { AssistantCloud } from "assistant-cloud";
6
7
  import type { ChatModelAdapter } from "../../runtime/utils/chat-model-adapter";
7
8
  import { AssistantRuntimeProvider } from "../AssistantRuntimeProvider";
8
9
  import { useLocalRuntime } from "./useLocalRuntime";
@@ -11,11 +12,53 @@ const chatModel: ChatModelAdapter = {
11
12
  run: async () => ({ content: [] }),
12
13
  };
13
14
 
15
+ const makeCloud = () =>
16
+ ({
17
+ threads: {
18
+ list: vi.fn().mockResolvedValue({ threads: [] }),
19
+ },
20
+ }) as unknown as AssistantCloud;
21
+
14
22
  afterEach(() => {
15
23
  vi.restoreAllMocks();
16
24
  });
17
25
 
18
26
  describe("useLocalRuntime", () => {
27
+ it("keeps the Cloud thread list loaded across unrelated rerenders", async () => {
28
+ const firstCloud = makeCloud();
29
+ const secondCloud = makeCloud();
30
+
31
+ const App = ({
32
+ cloud,
33
+ label,
34
+ }: {
35
+ cloud: AssistantCloud;
36
+ label: string;
37
+ }) => {
38
+ const runtime = useLocalRuntime(chatModel, { cloud });
39
+ return (
40
+ <AssistantRuntimeProvider runtime={runtime}>
41
+ <div>{label}</div>
42
+ </AssistantRuntimeProvider>
43
+ );
44
+ };
45
+
46
+ const { rerender } = render(<App cloud={firstCloud} label="first" />);
47
+
48
+ await waitFor(() => {
49
+ expect(firstCloud.threads.list).toHaveBeenCalledOnce();
50
+ });
51
+
52
+ rerender(<App cloud={firstCloud} label="second" />);
53
+ expect(firstCloud.threads.list).toHaveBeenCalledOnce();
54
+
55
+ rerender(<App cloud={secondCloud} label="third" />);
56
+ await waitFor(() => {
57
+ expect(secondCloud.threads.list).toHaveBeenCalledOnce();
58
+ });
59
+ expect(firstCloud.threads.list).toHaveBeenCalledOnce();
60
+ });
61
+
19
62
  it("handles rejected history loads", async () => {
20
63
  const error = new Error("history unavailable");
21
64
  const consoleError = vi
@@ -31,7 +31,10 @@ export class AssistantRuntimeImpl implements AssistantRuntime {
31
31
 
32
32
  public readonly _thread: ThreadRuntime;
33
33
 
34
- public constructor(private readonly _core: AssistantRuntimeCore) {
34
+ private readonly _core: AssistantRuntimeCore;
35
+
36
+ constructor(_core: AssistantRuntimeCore) {
37
+ this._core = _core;
35
38
  this.threads = new ThreadListRuntimeImpl(_core.threads);
36
39
  this._thread = this.threads.main;
37
40
 
@@ -49,7 +49,10 @@ export abstract class AttachmentRuntimeImpl<
49
49
 
50
50
  public abstract get source(): Source;
51
51
 
52
- constructor(private _core: AttachmentSnapshotBinding<Source>) {
52
+ private _core: AttachmentSnapshotBinding<Source>;
53
+
54
+ constructor(_core: AttachmentSnapshotBinding<Source>) {
55
+ this._core = _core;
53
56
  this.__internal_bindMethods();
54
57
  }
55
58
 
@@ -73,11 +76,14 @@ export abstract class AttachmentRuntimeImpl<
73
76
  abstract class ComposerAttachmentRuntime<
74
77
  Source extends "thread-composer" | "edit-composer",
75
78
  > extends AttachmentRuntimeImpl<Source> {
79
+ private _composerApi: ComposerRuntimeCoreBinding;
80
+
76
81
  constructor(
77
82
  core: AttachmentSnapshotBinding<Source>,
78
- private _composerApi: ComposerRuntimeCoreBinding,
83
+ _composerApi: ComposerRuntimeCoreBinding,
79
84
  ) {
80
85
  super(core);
86
+ this._composerApi = _composerApi;
81
87
  }
82
88
 
83
89
  public remove() {
@@ -251,7 +251,11 @@ export abstract class ComposerRuntimeImpl implements ComposerRuntime {
251
251
 
252
252
  public abstract get type(): "edit" | "thread";
253
253
 
254
- constructor(protected _core: ComposerRuntimeCoreBinding) {}
254
+ protected _core: ComposerRuntimeCoreBinding;
255
+
256
+ constructor(_core: ComposerRuntimeCoreBinding) {
257
+ this._core = _core;
258
+ }
255
259
 
256
260
  protected __internal_bindMethods() {
257
261
  this.setText = this.setText.bind(this);
@@ -483,10 +487,9 @@ export class EditComposerRuntimeImpl
483
487
  }
484
488
 
485
489
  private _getState;
486
- constructor(
487
- core: EditComposerRuntimeCoreBinding,
488
- private _beginEdit: () => void,
489
- ) {
490
+ private _beginEdit: () => void;
491
+
492
+ constructor(core: EditComposerRuntimeCoreBinding, _beginEdit: () => void) {
490
493
  const stateBinding = new LazyMemoizeSubject({
491
494
  path: core.path,
492
495
  getState: () => getEditComposerState(core.getState()),
@@ -498,6 +501,7 @@ export class EditComposerRuntimeImpl
498
501
  getState: () => core.getState(),
499
502
  subscribe: (callback) => stateBinding.subscribe(callback),
500
503
  });
504
+ this._beginEdit = _beginEdit;
501
505
 
502
506
  this._getState = stateBinding.getState.bind(stateBinding);
503
507