@assistant-ui/core 0.3.15 → 0.3.16

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 (251) hide show
  1. package/dist/internal.d.ts +7 -2
  2. package/dist/internal.js +8 -3
  3. package/dist/model-context/frame/host.d.ts.map +1 -1
  4. package/dist/model-context/frame/host.js +3 -2
  5. package/dist/model-context/frame/host.js.map +1 -1
  6. package/dist/model-context/frame/provider.d.ts +2 -0
  7. package/dist/model-context/frame/provider.d.ts.map +1 -1
  8. package/dist/model-context/frame/provider.js +88 -25
  9. package/dist/model-context/frame/provider.js.map +1 -1
  10. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  11. package/dist/model-context/interactable-composer-metadata.js +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  13. package/dist/model-context/registry.d.ts +2 -2
  14. package/dist/model-context/registry.d.ts.map +1 -1
  15. package/dist/model-context/registry.js +9 -28
  16. package/dist/model-context/registry.js.map +1 -1
  17. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  18. package/dist/react/client/InMemoryThreadList.js +24 -20
  19. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +59 -177
  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 +23 -56
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/index.d.ts +2 -1
  27. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  28. package/dist/react/interactables-legacy/Interactables.js +56 -174
  29. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  30. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  31. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  32. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  33. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  34. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  35. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  36. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  37. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  38. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +1 -1
  39. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +1 -1
  40. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  41. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +23 -55
  42. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  43. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  44. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  45. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  46. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  47. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  48. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  49. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  50. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  51. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  52. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  53. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  54. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  55. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  56. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  57. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  58. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  59. package/dist/react/runtimes/external-message-converter.js +33 -233
  60. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  61. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  62. package/dist/runtime/api/composer-runtime.js.map +1 -1
  63. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  64. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  65. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  66. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  67. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  68. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  69. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  70. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  72. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  73. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  74. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  75. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  76. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  77. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  78. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  79. package/dist/runtime/internal.d.ts +2 -1
  80. package/dist/runtime/internal.js +2 -1
  81. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  82. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  83. package/dist/runtime/queue/message-queue.js +1 -1
  84. package/dist/runtime/queue/message-queue.js.map +1 -1
  85. package/dist/runtime/queue/queue-item.d.ts +12 -0
  86. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  87. package/dist/runtime/queue/queue-item.js +6 -0
  88. package/dist/runtime/queue/queue-item.js.map +1 -0
  89. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  90. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  91. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  92. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  93. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  94. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  95. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  96. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  97. package/dist/runtime/utils/auto-status.d.ts +6 -1
  98. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  99. package/dist/runtime/utils/auto-status.js +7 -1
  100. package/dist/runtime/utils/auto-status.js.map +1 -1
  101. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  102. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  103. package/dist/runtime/utils/external-message-conversion.js +221 -0
  104. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  105. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  106. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  107. package/dist/runtime/utils/external-store-message.js +2 -1
  108. package/dist/runtime/utils/external-store-message.js.map +1 -1
  109. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  110. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  111. package/dist/runtime/utils/message-repository-session.js +130 -0
  112. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  113. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  114. package/dist/runtime/utils/message-repository.js +3 -4
  115. package/dist/runtime/utils/message-repository.js.map +1 -1
  116. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  117. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  118. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  119. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  120. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  121. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  122. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  123. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  124. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  125. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  126. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  127. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  128. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  129. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +122 -20
  130. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  131. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  132. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  133. package/dist/runtimes/local/local-thread-runtime-core.js +20 -2
  134. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  135. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  136. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  137. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  138. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  139. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  140. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  141. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  142. package/dist/store/clients/external-thread.d.ts.map +1 -1
  143. package/dist/store/clients/external-thread.js +74 -99
  144. package/dist/store/clients/external-thread.js.map +1 -1
  145. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  146. package/dist/store/clients/single-thread-list.js +1 -1
  147. package/dist/store/clients/single-thread-list.js.map +1 -1
  148. package/dist/store/clients/suggestions.d.ts.map +1 -1
  149. package/dist/store/clients/suggestions.js +16 -7
  150. package/dist/store/clients/suggestions.js.map +1 -1
  151. package/dist/store/index.d.ts +4 -2
  152. package/dist/store/scope-registration.d.ts +1 -1
  153. package/dist/store/scopes/composer.d.ts +2 -1
  154. package/dist/store/scopes/composer.d.ts.map +1 -1
  155. package/dist/store/scopes/message.d.ts +1 -1
  156. package/dist/store/scopes/queue-item.d.ts +2 -9
  157. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  158. package/dist/store/scopes/queue-item.js +0 -6
  159. package/dist/types/message.d.ts +7 -3
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/data-url.d.ts +14 -1
  163. package/dist/utils/data-url.d.ts.map +1 -1
  164. package/dist/utils/data-url.js +13 -1
  165. package/dist/utils/data-url.js.map +1 -1
  166. package/dist/utils/invoke-user-callback.d.ts +5 -0
  167. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  168. package/dist/utils/invoke-user-callback.js +19 -0
  169. package/dist/utils/invoke-user-callback.js.map +1 -0
  170. package/dist/utils/normalizePartStatus.js.map +1 -1
  171. package/package.json +10 -10
  172. package/src/internal.ts +12 -0
  173. package/src/model-context/frame/host.test.ts +91 -7
  174. package/src/model-context/frame/host.ts +7 -2
  175. package/src/model-context/frame/provider.test.ts +267 -12
  176. package/src/model-context/frame/provider.ts +113 -31
  177. package/src/model-context/interactable-composer-metadata.ts +1 -1
  178. package/src/model-context/registry.test.ts +44 -0
  179. package/src/model-context/registry.ts +13 -35
  180. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  181. package/src/react/client/InMemoryThreadList.ts +7 -1
  182. package/src/react/client/Interactables.ts +19 -178
  183. package/src/react/client/RemoteThreadList.test.ts +96 -0
  184. package/src/react/client/RemoteThreadList.ts +27 -59
  185. package/src/react/interactables-legacy/Interactables.ts +20 -177
  186. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  187. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  188. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +26 -56
  189. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  190. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  191. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  192. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  193. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  194. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  195. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  196. package/src/react/runtimes/external-message-converter.ts +58 -481
  197. package/src/runtime/api/composer-runtime.ts +1 -1
  198. package/src/runtime/api/message-runtime.test.ts +28 -1
  199. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  200. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  201. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  202. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  203. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  204. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  205. package/src/runtime/internal.ts +5 -0
  206. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  207. package/src/runtime/queue/message-queue.ts +1 -4
  208. package/src/runtime/queue/queue-item.ts +10 -0
  209. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  210. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  211. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  212. package/src/runtime/utils/auto-status.ts +33 -0
  213. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  214. package/src/runtime/utils/external-message-conversion.ts +494 -0
  215. package/src/runtime/utils/external-store-message.ts +2 -0
  216. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  217. package/src/runtime/utils/message-repository-session.ts +176 -0
  218. package/src/runtime/utils/message-repository.ts +7 -4
  219. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  220. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  221. package/src/runtime/utils/thread-message-like.ts +2 -0
  222. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  223. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +175 -28
  224. package/src/runtimes/local/local-thread-runtime-core.test.ts +172 -1
  225. package/src/runtimes/local/local-thread-runtime-core.ts +48 -3
  226. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  227. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  228. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  229. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  230. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  231. package/src/store/clients/external-thread.ts +16 -61
  232. package/src/store/clients/single-thread-list.ts +4 -1
  233. package/src/store/clients/suggestions.test.ts +31 -0
  234. package/src/store/clients/suggestions.ts +18 -19
  235. package/src/store/scopes/message.ts +1 -1
  236. package/src/store/scopes/queue-item.ts +2 -9
  237. package/src/tests/MessageRepository.test.ts +77 -0
  238. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  239. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  240. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +164 -0
  241. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  242. package/src/tests/external-thread-attachments.test.tsx +173 -0
  243. package/src/tests/single-thread-list-item.test.tsx +47 -0
  244. package/src/tests/thread-message-like.test.ts +39 -1
  245. package/src/types/message.ts +8 -3
  246. package/src/utils/data-url.test.ts +52 -1
  247. package/src/utils/data-url.ts +21 -0
  248. package/src/utils/invoke-user-callback.test.ts +82 -0
  249. package/src/utils/invoke-user-callback.ts +31 -0
  250. package/src/utils/normalizePartStatus.ts +1 -1
  251. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -0,0 +1,494 @@
1
+ import type { ReadonlyJSONValue } from "assistant-stream/utils";
2
+ import type { ToolExecutionStatus } from "../../runtimes/tool-invocations/ToolInvocationTracker";
3
+ import type {
4
+ ThreadAssistantMessage,
5
+ ThreadMessage,
6
+ ToolCallMessagePart,
7
+ } from "../../types/message";
8
+ import type { MessageTiming } from "../../types/message";
9
+ import { generateErrorMessageId } from "../../utils/id";
10
+ import { isJSONValueEqual } from "../../utils/json/is-json-equal";
11
+ import {
12
+ getAutoStatus,
13
+ isAutoStatus,
14
+ isInterruptedToolCall,
15
+ isPendingToolCall,
16
+ } from "./auto-status";
17
+ import {
18
+ bindExternalStoreMessage,
19
+ FALLBACK_ID_PREFIX,
20
+ getExternalStoreMessages,
21
+ symbolInnerMessage,
22
+ } from "./external-store-message";
23
+ import {
24
+ fromThreadMessageLike,
25
+ type ThreadMessageLike,
26
+ } from "./thread-message-like";
27
+
28
+ export type JoinStrategy = "concat-content" | "none";
29
+
30
+ export type ExternalMessageConverterMessage =
31
+ | (ThreadMessageLike & {
32
+ readonly convertConfig?: {
33
+ readonly joinStrategy?: JoinStrategy;
34
+ };
35
+ })
36
+ | {
37
+ role: "tool";
38
+ toolCallId: string;
39
+ toolName?: string | undefined;
40
+ result: any;
41
+ artifact?: any;
42
+ isError?: boolean;
43
+ messages?: readonly ThreadMessage[];
44
+ };
45
+
46
+ export type ExternalMessageConverterMetadata = {
47
+ readonly toolStatuses?: Record<string, ToolExecutionStatus>;
48
+ readonly error?: ReadonlyJSONValue;
49
+ readonly messageTiming?: Record<string, MessageTiming>;
50
+ };
51
+
52
+ export type ExternalMessageConverterCallback<T> = (
53
+ message: T,
54
+ metadata: ExternalMessageConverterMetadata,
55
+ ) => ExternalMessageConverterMessage | ExternalMessageConverterMessage[];
56
+
57
+ export type ExternalMessageConverterCallbackResult<T> = {
58
+ input: T;
59
+ outputs: ExternalMessageConverterMessage[];
60
+ };
61
+
62
+ export type ExternalMessageConverterChunk<T> = {
63
+ inputs: T[];
64
+ outputs: ExternalMessageConverterMessage[];
65
+ };
66
+
67
+ type Mutable<T> = {
68
+ -readonly [P in keyof T]: T[P];
69
+ };
70
+
71
+ const stringifyForError = (value: unknown) => {
72
+ let text;
73
+ try {
74
+ text = value instanceof Error ? String(value) : JSON.stringify(value);
75
+ } catch {
76
+ /* circular */
77
+ }
78
+ text ??= String(value);
79
+ return text.length > 200 ? `${text.slice(0, 200)}…` : text;
80
+ };
81
+
82
+ const toCallbackOutputs = (
83
+ output: ExternalMessageConverterMessage | ExternalMessageConverterMessage[],
84
+ input: unknown,
85
+ ): ExternalMessageConverterMessage[] => {
86
+ const outputs = Array.isArray(output) ? output : [output];
87
+ for (const o of outputs) {
88
+ const valid =
89
+ typeof o === "object" &&
90
+ o !== null &&
91
+ (o.role === "tool" ||
92
+ ((o.role === "assistant" || o.role === "user" || o.role === "system") &&
93
+ (typeof o.content === "string" || Array.isArray(o.content))));
94
+ if (!valid)
95
+ throw new Error(
96
+ `External message converter: the converter callback returned an invalid message (${stringifyForError(o)}) for input ${stringifyForError(input)}. Return an empty array to skip a message.`,
97
+ );
98
+ }
99
+ // Providers can emit tool results without a usable id (react-langchain maps
100
+ // a missing tool_call_id to ""); converters must not throw on those, so the
101
+ // result is dropped like any other orphaned tool output.
102
+ return outputs.filter((o) => {
103
+ if (o.role !== "tool") return true;
104
+ if (typeof o.toolCallId === "string" && o.toolCallId.length > 0)
105
+ return true;
106
+ if (!warnedDroppedToolResults.has(o)) {
107
+ warnedDroppedToolResults.add(o);
108
+ console.warn(
109
+ `External message converter: dropping a tool result without a toolCallId (${stringifyForError(o)}) for input ${stringifyForError(input)}.`,
110
+ );
111
+ }
112
+ return false;
113
+ });
114
+ };
115
+
116
+ const warnedDroppedToolResults = new WeakSet<object>();
117
+
118
+ export const convertExternalMessageCallback = <T>(
119
+ input: T,
120
+ callback: ExternalMessageConverterCallback<T>,
121
+ metadata: ExternalMessageConverterMetadata,
122
+ ): ExternalMessageConverterCallbackResult<T> => ({
123
+ input,
124
+ outputs: toCallbackOutputs(callback(input, metadata), input),
125
+ });
126
+
127
+ const mergeInnerMessages = (existing: object, incoming: object) => ({
128
+ [symbolInnerMessage]: [
129
+ ...((existing as any)[symbolInnerMessage] ?? []),
130
+ ...((incoming as any)[symbolInnerMessage] ?? []),
131
+ ],
132
+ });
133
+
134
+ export const joinExternalMessages = (
135
+ messages: readonly ExternalMessageConverterMessage[],
136
+ ): ThreadMessageLike => {
137
+ const assistantMessage: Mutable<Omit<ThreadMessageLike, "metadata">> & {
138
+ content: Exclude<ThreadMessageLike["content"][0], string>[];
139
+ metadata?: Mutable<ThreadMessageLike["metadata"]>;
140
+ } = {
141
+ role: "assistant",
142
+ content: [],
143
+ };
144
+ for (const output of messages) {
145
+ if (output.role === "tool") {
146
+ const toolCallIdx = assistantMessage.content.findIndex(
147
+ (c) => c.type === "tool-call" && c.toolCallId === output.toolCallId,
148
+ );
149
+ // Ignore orphaned tool results so one bad tool message does not
150
+ // prevent rendering the rest of the conversation.
151
+ if (toolCallIdx !== -1) {
152
+ const toolCall = assistantMessage.content[
153
+ toolCallIdx
154
+ ]! as ToolCallMessagePart;
155
+ if (output.toolName != null) {
156
+ if (toolCall.toolName !== output.toolName)
157
+ throw new Error(
158
+ `Tool call name ${output.toolCallId} ${output.toolName} does not match existing tool call ${toolCall.toolName}`,
159
+ );
160
+ }
161
+ assistantMessage.content[toolCallIdx] = {
162
+ ...toolCall,
163
+ ...{
164
+ [symbolInnerMessage]: [
165
+ ...((toolCall as any)[symbolInnerMessage] ?? []),
166
+ output,
167
+ ],
168
+ },
169
+ result: output.result,
170
+ artifact: output.artifact,
171
+ isError: output.isError,
172
+ messages: output.messages,
173
+ };
174
+ }
175
+ } else {
176
+ const role = output.role;
177
+ const content = (
178
+ typeof output.content === "string"
179
+ ? [{ type: "text" as const, text: output.content }]
180
+ : output.content
181
+ ).map((c) => ({
182
+ ...c,
183
+ ...{ [symbolInnerMessage]: [output] },
184
+ }));
185
+ switch (role) {
186
+ case "system":
187
+ case "user":
188
+ return {
189
+ ...output,
190
+ content,
191
+ };
192
+ case "assistant":
193
+ if (assistantMessage.content.length === 0) {
194
+ assistantMessage.id = output.id;
195
+ assistantMessage.createdAt ??= output.createdAt;
196
+ assistantMessage.status ??= output.status;
197
+
198
+ if (output.attachments) {
199
+ assistantMessage.attachments = [
200
+ ...(assistantMessage.attachments ?? []),
201
+ ...output.attachments,
202
+ ];
203
+ }
204
+ }
205
+
206
+ if (output.metadata) {
207
+ assistantMessage.metadata ??= {};
208
+ if (output.metadata.unstable_state !== undefined) {
209
+ assistantMessage.metadata.unstable_state =
210
+ output.metadata.unstable_state;
211
+ }
212
+ if (output.metadata.unstable_annotations) {
213
+ assistantMessage.metadata.unstable_annotations = [
214
+ ...(assistantMessage.metadata.unstable_annotations ?? []),
215
+ ...output.metadata.unstable_annotations,
216
+ ];
217
+ }
218
+ if (output.metadata.unstable_data) {
219
+ assistantMessage.metadata.unstable_data = [
220
+ ...(assistantMessage.metadata.unstable_data ?? []),
221
+ ...output.metadata.unstable_data,
222
+ ];
223
+ }
224
+ if (output.metadata.steps) {
225
+ assistantMessage.metadata.steps = [
226
+ ...(assistantMessage.metadata.steps ?? []),
227
+ ...output.metadata.steps,
228
+ ];
229
+ }
230
+ if (output.metadata.custom) {
231
+ assistantMessage.metadata.custom = {
232
+ ...(assistantMessage.metadata.custom ?? {}),
233
+ ...output.metadata.custom,
234
+ };
235
+ }
236
+
237
+ if (output.metadata.timing) {
238
+ assistantMessage.metadata.timing = output.metadata.timing;
239
+ }
240
+
241
+ if (output.metadata.submittedFeedback) {
242
+ assistantMessage.metadata.submittedFeedback =
243
+ output.metadata.submittedFeedback;
244
+ }
245
+
246
+ if (output.metadata.isOptimistic) {
247
+ assistantMessage.metadata.isOptimistic = true;
248
+ }
249
+ // TODO keep this in sync with ThreadMessageLike["metadata"] / fromThreadMessageLike
250
+ }
251
+
252
+ // Add content parts, merging reasoning parts with same parentId
253
+ for (const part of content) {
254
+ if (part.type === "tool-call") {
255
+ const existingIdx = assistantMessage.content.findIndex(
256
+ (c) =>
257
+ c.type === "tool-call" && c.toolCallId === part.toolCallId,
258
+ );
259
+ if (existingIdx !== -1) {
260
+ const existing = assistantMessage.content[
261
+ existingIdx
262
+ ] as typeof part;
263
+ assistantMessage.content[existingIdx] = {
264
+ ...existing,
265
+ ...part,
266
+ ...mergeInnerMessages(existing, part),
267
+ };
268
+ continue;
269
+ }
270
+ }
271
+
272
+ if (
273
+ part.type === "reasoning" &&
274
+ "parentId" in part &&
275
+ part.parentId
276
+ ) {
277
+ const existingIdx = assistantMessage.content.findIndex(
278
+ (c) =>
279
+ c.type === "reasoning" &&
280
+ "parentId" in c &&
281
+ c.parentId === part.parentId,
282
+ );
283
+ if (existingIdx !== -1) {
284
+ const existing = assistantMessage.content[
285
+ existingIdx
286
+ ] as typeof part;
287
+ assistantMessage.content[existingIdx] = {
288
+ ...existing,
289
+ text: `${existing.text}\n\n${part.text}`,
290
+ ...mergeInnerMessages(existing, part),
291
+ };
292
+ continue;
293
+ }
294
+ }
295
+ assistantMessage.content.push(part);
296
+ }
297
+ break;
298
+ default: {
299
+ const unsupportedRole: never = role;
300
+ throw new Error(`Unknown message role: ${unsupportedRole}`);
301
+ }
302
+ }
303
+ }
304
+ }
305
+ return assistantMessage;
306
+ };
307
+
308
+ export const chunkExternalMessages = <T>(
309
+ callbackResults: ExternalMessageConverterCallbackResult<T>[],
310
+ joinStrategy?: JoinStrategy,
311
+ ) => {
312
+ const results: ExternalMessageConverterChunk<T>[] = [];
313
+ let isAssistant = false;
314
+ let pendingNone = false; // true if the previous assistant message had joinStrategy "none"
315
+ let inputs: T[] = [];
316
+ let outputs: ExternalMessageConverterMessage[] = [];
317
+
318
+ const flush = () => {
319
+ if (outputs.length) {
320
+ results.push({
321
+ inputs,
322
+ outputs,
323
+ });
324
+ }
325
+ inputs = [];
326
+ outputs = [];
327
+ isAssistant = false;
328
+ pendingNone = false;
329
+ };
330
+
331
+ for (const callbackResult of callbackResults) {
332
+ for (const output of callbackResult.outputs) {
333
+ if (
334
+ (pendingNone && output.role !== "tool") ||
335
+ !isAssistant ||
336
+ output.role === "user" ||
337
+ output.role === "system"
338
+ ) {
339
+ flush();
340
+ }
341
+ isAssistant = output.role === "assistant" || output.role === "tool";
342
+
343
+ if (inputs.at(-1) !== callbackResult.input) {
344
+ inputs.push(callbackResult.input);
345
+ }
346
+ outputs.push(output);
347
+
348
+ if (
349
+ output.role === "assistant" &&
350
+ (output.convertConfig?.joinStrategy === "none" ||
351
+ joinStrategy === "none")
352
+ ) {
353
+ pendingNone = true;
354
+ }
355
+ }
356
+ }
357
+ flush();
358
+ return results;
359
+ };
360
+
361
+ export const shallowArrayEqual = (a: unknown[], b: unknown[]) => {
362
+ if (a.length !== b.length) return false;
363
+ for (let i = 0; i < a.length; i++) {
364
+ if (a[i] !== b[i]) return false;
365
+ }
366
+ return true;
367
+ };
368
+
369
+ type ExternalMessageConversionCache = {
370
+ message: ThreadMessage | undefined;
371
+ generatedFallbackMessages: WeakSet<object>;
372
+ };
373
+
374
+ export const convertExternalMessageChunk = <T>(
375
+ message: ExternalMessageConverterChunk<T>,
376
+ idx: number,
377
+ chunkCount: number,
378
+ isRunning: boolean,
379
+ error: ReadonlyJSONValue | undefined,
380
+ cache?: ExternalMessageConversionCache,
381
+ ) => {
382
+ const isLast = idx === chunkCount - 1;
383
+ const joined = joinExternalMessages(message.outputs);
384
+ const hasInterruptedToolCalls =
385
+ typeof joined.content === "object" &&
386
+ joined.content.some(isInterruptedToolCall);
387
+ const hasPendingToolCalls =
388
+ typeof joined.content === "object" &&
389
+ joined.content.some(isPendingToolCall);
390
+ const autoStatus = getAutoStatus(
391
+ isLast,
392
+ isRunning,
393
+ hasInterruptedToolCalls,
394
+ hasPendingToolCalls,
395
+ isLast ? error : undefined,
396
+ );
397
+ const fallbackId = `${FALLBACK_ID_PREFIX}${idx}`;
398
+
399
+ const cachedMessage = cache?.message;
400
+ if (
401
+ cachedMessage &&
402
+ (cachedMessage.role !== "assistant" ||
403
+ !isAutoStatus(cachedMessage.status) ||
404
+ cachedMessage.status === autoStatus ||
405
+ (cachedMessage.status.type === "incomplete" &&
406
+ cachedMessage.status.reason === "error" &&
407
+ cachedMessage.status.error !== undefined &&
408
+ autoStatus.type === "incomplete" &&
409
+ autoStatus.reason === "error" &&
410
+ autoStatus.error !== undefined &&
411
+ (cachedMessage.status.error === autoStatus.error ||
412
+ isJSONValueEqual(cachedMessage.status.error, autoStatus.error))))
413
+ ) {
414
+ const inputs = getExternalStoreMessages<T>(cachedMessage);
415
+ if (shallowArrayEqual(inputs, message.inputs)) {
416
+ // A positional fallback id goes stale when messages are prepended
417
+ // or reordered; serving it unchanged makes two messages collide on
418
+ // one id and the dedup downstream drops one of them.
419
+ if (
420
+ cache.generatedFallbackMessages.has(cachedMessage) &&
421
+ cachedMessage.id !== fallbackId
422
+ ) {
423
+ const updated = { ...cachedMessage, id: fallbackId };
424
+ cache.generatedFallbackMessages.add(updated);
425
+ bindExternalStoreMessage(updated, message.inputs);
426
+ return updated;
427
+ }
428
+ return cachedMessage;
429
+ }
430
+ }
431
+
432
+ const newMessage = fromThreadMessageLike(joined, fallbackId, autoStatus);
433
+ if (cache && joined.id == null) {
434
+ cache.generatedFallbackMessages.add(newMessage);
435
+ }
436
+ bindExternalStoreMessage(newMessage, message.inputs);
437
+ return newMessage;
438
+ };
439
+
440
+ function createErrorAssistantMessage(
441
+ error: ReadonlyJSONValue,
442
+ ): ThreadAssistantMessage {
443
+ const msg: ThreadAssistantMessage = {
444
+ id: generateErrorMessageId(),
445
+ role: "assistant",
446
+ content: [],
447
+ status: { type: "incomplete", reason: "error", error },
448
+ createdAt: new Date(),
449
+ metadata: {
450
+ unstable_state: null,
451
+ unstable_annotations: [],
452
+ unstable_data: [],
453
+ custom: {},
454
+ steps: [],
455
+ },
456
+ };
457
+ bindExternalStoreMessage(msg, []);
458
+ return msg;
459
+ }
460
+
461
+ export const completeExternalMessageConversion = (
462
+ messages: ThreadMessage[],
463
+ error: ReadonlyJSONValue | undefined,
464
+ ) => {
465
+ if (error) {
466
+ const lastMessage = messages.at(-1);
467
+ if (!lastMessage || lastMessage.role !== "assistant") {
468
+ messages.push(createErrorAssistantMessage(error));
469
+ }
470
+ }
471
+ return messages;
472
+ };
473
+
474
+ export const convertExternalMessages = <T extends WeakKey>(
475
+ messages: T[],
476
+ callback: ExternalMessageConverterCallback<T>,
477
+ isRunning: boolean,
478
+ metadata: ExternalMessageConverterMetadata,
479
+ ) => {
480
+ const callbackResults = messages.map((message) =>
481
+ convertExternalMessageCallback(message, callback, metadata),
482
+ );
483
+ const chunks = chunkExternalMessages(callbackResults);
484
+ const result = chunks.map((message, idx) =>
485
+ convertExternalMessageChunk(
486
+ message,
487
+ idx,
488
+ chunks.length,
489
+ isRunning,
490
+ metadata.error,
491
+ ),
492
+ );
493
+ return completeExternalMessageConversion(result, metadata.error);
494
+ };
@@ -42,3 +42,5 @@ export const getExternalStoreMessages = <T>(
42
42
  container[symbolInnerMessages] = [value];
43
43
  return container[symbolInnerMessages];
44
44
  };
45
+
46
+ export const FALLBACK_ID_PREFIX = "__external_store_fallback_";
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ThreadUserMessage } from "../../types/message";
3
+ import { createMessageRepositorySession } from "./message-repository-session";
4
+
5
+ const message = (id: string, text = id): ThreadUserMessage => ({
6
+ id,
7
+ role: "user",
8
+ createdAt: new Date(0),
9
+ content: [{ type: "text", text }],
10
+ attachments: [],
11
+ metadata: { custom: {} },
12
+ });
13
+
14
+ const withRootAndChild = () => {
15
+ const session = createMessageRepositorySession();
16
+ session.addOrUpdateMessage(null, message("root"));
17
+ session.addOrUpdateMessage("root", message("child"));
18
+ return session;
19
+ };
20
+
21
+ describe("createMessageRepositorySession", () => {
22
+ it("memoizes exports and invalidates them after every mutator", () => {
23
+ const cases = [
24
+ () => {
25
+ const session = createMessageRepositorySession();
26
+ return {
27
+ session,
28
+ mutate: () => session.addOrUpdateMessage(null, message("root")),
29
+ };
30
+ },
31
+ () => {
32
+ const session = createMessageRepositorySession();
33
+ session.addOrUpdateMessage(null, message("root"));
34
+ session.addOrUpdateMessage("root", message("child"));
35
+ return { session, mutate: () => session.deleteMessage("child") };
36
+ },
37
+ () => {
38
+ const session = createMessageRepositorySession();
39
+ session.addOrUpdateMessage(null, message("root"));
40
+ session.addOrUpdateMessage("root", message("child"));
41
+ return {
42
+ session,
43
+ mutate: () => expect(session.tryDeleteMessage("child")).toBe(true),
44
+ };
45
+ },
46
+ () => {
47
+ const session = createMessageRepositorySession();
48
+ session.addOrUpdateMessage(null, message("root"));
49
+ session.addOrUpdateMessage("root", message("first"));
50
+ session.addOrUpdateMessage("root", message("second"));
51
+ return { session, mutate: () => session.switchToBranch("first") };
52
+ },
53
+ () => {
54
+ const session = createMessageRepositorySession();
55
+ session.addOrUpdateMessage(null, message("root"));
56
+ session.addOrUpdateMessage("root", message("child"));
57
+ return { session, mutate: () => session.resetHead("root") };
58
+ },
59
+ () => {
60
+ const session = createMessageRepositorySession();
61
+ session.addOrUpdateMessage(null, message("root"));
62
+ return { session, mutate: () => session.clear() };
63
+ },
64
+ () => {
65
+ const session = createMessageRepositorySession();
66
+ session.addOrUpdateMessage(null, message("root"));
67
+ return {
68
+ session,
69
+ mutate: () =>
70
+ expect(
71
+ session.updateMessage("root", (current) => ({
72
+ ...current,
73
+ content: [{ type: "text", text: "updated" }],
74
+ })),
75
+ ).toBe(true),
76
+ };
77
+ },
78
+ () => {
79
+ const session = createMessageRepositorySession();
80
+ session.addOrUpdateMessage(null, message("previous"));
81
+ return {
82
+ session,
83
+ mutate: () =>
84
+ session.applyExternalMessageRepository({
85
+ headId: "root",
86
+ messages: [{ parentId: null, message: message("root") }],
87
+ }),
88
+ };
89
+ },
90
+ ];
91
+
92
+ for (const createCase of cases) {
93
+ const { session, mutate } = createCase();
94
+ const exported = session.export();
95
+ expect(session.export()).toBe(exported);
96
+
97
+ mutate();
98
+
99
+ expect(session.export()).not.toBe(exported);
100
+ }
101
+ });
102
+
103
+ it("decorates and memoizes exports", () => {
104
+ let decorations = 0;
105
+ const session = createMessageRepositorySession({
106
+ decorateExport: (exported) => {
107
+ decorations++;
108
+ return { ...exported, headId: "decorated" };
109
+ },
110
+ });
111
+ session.addOrUpdateMessage(null, message("root"));
112
+
113
+ const exported = session.export();
114
+
115
+ expect(exported.headId).toBe("decorated");
116
+ expect(session.export()).toBe(exported);
117
+ expect(decorations).toBe(1);
118
+
119
+ session.addOrUpdateMessage("root", message("child"));
120
+
121
+ expect(session.export()).not.toBe(exported);
122
+ expect(decorations).toBe(2);
123
+ });
124
+
125
+ it("preserves the export cache when an update returns the current message", () => {
126
+ const session = withRootAndChild();
127
+ const exported = session.export();
128
+
129
+ expect(session.updateMessage("child", (current) => current)).toBe(false);
130
+ expect(session.export()).toBe(exported);
131
+ });
132
+
133
+ it("imports out-of-order parent-child pairs topologically", () => {
134
+ const session = createMessageRepositorySession();
135
+
136
+ session.applyExternalMessageRepository({
137
+ headId: "child",
138
+ messages: [
139
+ { parentId: "root", message: message("child") },
140
+ { parentId: null, message: message("root") },
141
+ ],
142
+ });
143
+
144
+ expect(session.getMessages().map((item) => item.id)).toEqual([
145
+ "root",
146
+ "child",
147
+ ]);
148
+ });
149
+
150
+ it("uses the degenerate linear path for duplicate message ids", () => {
151
+ const session = createMessageRepositorySession();
152
+
153
+ session.applyExternalMessageRepository({
154
+ headId: "root",
155
+ messages: [
156
+ { parentId: null, message: message("root", "first") },
157
+ { parentId: "root", message: message("child") },
158
+ { parentId: "child", message: message("root", "replacement") },
159
+ ],
160
+ });
161
+
162
+ expect(session.tryGetMessage("root")?.message.content).toEqual([
163
+ { type: "text", text: "replacement" },
164
+ ]);
165
+ });
166
+
167
+ it("uses the degenerate linear path when the head is missing", () => {
168
+ const session = createMessageRepositorySession();
169
+
170
+ session.applyExternalMessageRepository({
171
+ headId: "missing",
172
+ messages: [
173
+ { parentId: null, message: message("root") },
174
+ { parentId: null, message: message("child") },
175
+ ],
176
+ });
177
+
178
+ expect(session.tryGetMessage("child")?.parentId).toBe("root");
179
+ });
180
+
181
+ it("preserves an already-imported message parent in the degenerate path", () => {
182
+ const session = createMessageRepositorySession();
183
+
184
+ session.applyExternalMessageRepository({
185
+ messages: [
186
+ { parentId: null, message: message("root") },
187
+ { parentId: "root", message: message("branch", "first") },
188
+ { parentId: "branch", message: message("child") },
189
+ { parentId: "child", message: message("branch", "replacement") },
190
+ ],
191
+ });
192
+
193
+ expect(session.tryGetMessage("branch")?.parentId).toBe("root");
194
+ });
195
+ });