@assistant-ui/core 0.2.18 → 0.2.20

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 (231) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.js +3 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +2 -1
  5. package/dist/internal.js +2 -1
  6. package/dist/model-context/interactable-composer-metadata.d.ts +105 -0
  7. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -0
  8. package/dist/model-context/interactable-composer-metadata.js +259 -0
  9. package/dist/model-context/interactable-composer-metadata.js.map +1 -0
  10. package/dist/model-context/registry.d.ts.map +1 -1
  11. package/dist/model-context/registry.js +1 -0
  12. package/dist/model-context/registry.js.map +1 -1
  13. package/dist/model-context/types.d.ts +6 -0
  14. package/dist/model-context/types.d.ts.map +1 -1
  15. package/dist/model-context/types.js +4 -0
  16. package/dist/model-context/types.js.map +1 -1
  17. package/dist/react/AssistantProvider.d.ts +1 -1
  18. package/dist/react/AssistantProvider.js +1 -1
  19. package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
  20. package/dist/react/RuntimeAdapter.js +1 -1
  21. package/dist/react/client/Interactables.d.ts +8 -2
  22. package/dist/react/client/Interactables.d.ts.map +1 -1
  23. package/dist/react/client/Interactables.js +190 -94
  24. package/dist/react/client/Interactables.js.map +1 -1
  25. package/dist/react/client/Tools.js +1 -1
  26. package/dist/react/client/interactable-model-context.d.ts +9 -6
  27. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  28. package/dist/react/client/interactable-model-context.js +154 -32
  29. package/dist/react/client/interactable-model-context.js.map +1 -1
  30. package/dist/react/index.d.ts +15 -6
  31. package/dist/react/index.js +12 -5
  32. package/dist/react/interactables-legacy/Interactables.d.ts +12 -0
  33. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -0
  34. package/dist/react/interactables-legacy/Interactables.js +264 -0
  35. package/dist/react/interactables-legacy/Interactables.js.map +1 -0
  36. package/dist/react/interactables-legacy/interactable-model-context.d.ts +12 -0
  37. package/dist/react/interactables-legacy/interactable-model-context.d.ts.map +1 -0
  38. package/dist/react/interactables-legacy/interactable-model-context.js +54 -0
  39. package/dist/react/interactables-legacy/interactable-model-context.js.map +1 -0
  40. package/dist/react/interactables-legacy/scopes.d.ts +58 -0
  41. package/dist/react/interactables-legacy/scopes.d.ts.map +1 -0
  42. package/dist/react/interactables-legacy/scopes.js +0 -0
  43. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts +31 -0
  44. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -0
  45. package/dist/react/{model-context → interactables-legacy}/useAssistantInteractable.js +6 -2
  46. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -0
  47. package/dist/react/interactables-legacy/useInteractableState.d.ts +21 -0
  48. package/dist/react/interactables-legacy/useInteractableState.d.ts.map +1 -0
  49. package/dist/react/interactables-legacy/useInteractableState.js +84 -0
  50. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -0
  51. package/dist/react/model-context/interactableTool.d.ts +53 -0
  52. package/dist/react/model-context/interactableTool.d.ts.map +1 -0
  53. package/dist/react/model-context/interactableTool.js +188 -0
  54. package/dist/react/model-context/interactableTool.js.map +1 -0
  55. package/dist/react/model-context/useAssistantContext.js +1 -1
  56. package/dist/react/model-context/useAssistantDataUI.js +1 -1
  57. package/dist/react/model-context/useAssistantInstructions.js +1 -1
  58. package/dist/react/model-context/useAssistantTool.js +1 -1
  59. package/dist/react/model-context/useAssistantToolUI.js +1 -1
  60. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  61. package/dist/react/model-context/useInteractable.d.ts +73 -0
  62. package/dist/react/model-context/useInteractable.d.ts.map +1 -0
  63. package/dist/react/model-context/useInteractable.js +100 -0
  64. package/dist/react/model-context/useInteractable.js.map +1 -0
  65. package/dist/react/model-context/useInteractableState.d.ts +8 -5
  66. package/dist/react/model-context/useInteractableState.d.ts.map +1 -1
  67. package/dist/react/model-context/useInteractableState.js +48 -48
  68. package/dist/react/model-context/useInteractableState.js.map +1 -1
  69. package/dist/react/model-context/useInteractableVersions.d.ts +19 -0
  70. package/dist/react/model-context/useInteractableVersions.d.ts.map +1 -0
  71. package/dist/react/model-context/useInteractableVersions.js +51 -0
  72. package/dist/react/model-context/useInteractableVersions.js.map +1 -0
  73. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts +15 -0
  74. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts.map +1 -0
  75. package/dist/react/primitive-hooks/useThreadMessageIds.js +26 -0
  76. package/dist/react/primitive-hooks/useThreadMessageIds.js.map +1 -0
  77. package/dist/react/primitive-hooks/useVoice.js +1 -1
  78. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  79. package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
  80. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  81. package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
  82. package/dist/react/primitives/generativeUI/GenerativeUI.js +2 -2
  83. package/dist/react/primitives/generativeUI/GenerativeUI.js.map +1 -1
  84. package/dist/react/primitives/message/MessageAttachments.js +1 -1
  85. package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
  86. package/dist/react/primitives/message/MessageParts.js +1 -1
  87. package/dist/react/primitives/message/MessageQuote.js +1 -1
  88. package/dist/react/primitives/part/PartMessages.d.ts +5 -2
  89. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  90. package/dist/react/primitives/part/PartMessages.js +8 -8
  91. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  92. package/dist/react/primitives/thread/ThreadMessages.d.ts +31 -1
  93. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  94. package/dist/react/primitives/thread/ThreadMessages.js +64 -2
  95. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  96. package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
  97. package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
  98. package/dist/react/providers/MessageByIdProvider.d.ts +9 -0
  99. package/dist/react/providers/MessageByIdProvider.d.ts.map +1 -0
  100. package/dist/react/providers/MessageByIdProvider.js +57 -0
  101. package/dist/react/providers/MessageByIdProvider.js.map +1 -0
  102. package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
  103. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -1
  104. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
  105. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  106. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +16 -1
  107. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  108. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
  109. package/dist/react/runtimes/cloud/auiV0.js +1 -1
  110. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +1 -1
  111. package/dist/react/runtimes/createRuntimeExtras.d.ts +20 -0
  112. package/dist/react/runtimes/createRuntimeExtras.d.ts.map +1 -0
  113. package/dist/react/runtimes/createRuntimeExtras.js +43 -0
  114. package/dist/react/runtimes/createRuntimeExtras.js.map +1 -0
  115. package/dist/react/runtimes/useLocalRuntime.js +1 -1
  116. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  117. package/dist/react/runtimes/useRemoteThreadListRuntime.js +6 -2
  118. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  119. package/dist/react/runtimes/useStreamingTiming.d.ts +26 -0
  120. package/dist/react/runtimes/useStreamingTiming.d.ts.map +1 -0
  121. package/dist/react/runtimes/useStreamingTiming.js +43 -0
  122. package/dist/react/runtimes/useStreamingTiming.js.map +1 -0
  123. package/dist/react/types/scopes/interactables.d.ts +40 -24
  124. package/dist/react/types/scopes/interactables.d.ts.map +1 -1
  125. package/dist/react/types/store-augmentation.d.ts +3 -1
  126. package/dist/react/types/store-augmentation.d.ts.map +1 -1
  127. package/dist/react/utils/useJSONEqual.d.ts +11 -0
  128. package/dist/react/utils/useJSONEqual.d.ts.map +1 -0
  129. package/dist/react/utils/useJSONEqual.js +30 -0
  130. package/dist/react/utils/useJSONEqual.js.map +1 -0
  131. package/dist/runtime/base/base-composer-runtime-core.d.ts +5 -0
  132. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  133. package/dist/runtime/base/base-composer-runtime-core.js +13 -0
  134. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  135. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  136. package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -1
  137. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  138. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  139. package/dist/runtime/base/default-thread-composer-runtime-core.js +4 -1
  140. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  141. package/dist/runtime/utils/message-repository.d.ts +1 -0
  142. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  143. package/dist/runtime/utils/message-repository.js +6 -3
  144. package/dist/runtime/utils/message-repository.js.map +1 -1
  145. package/dist/runtime/utils/streaming-timing.d.ts +59 -0
  146. package/dist/runtime/utils/streaming-timing.d.ts.map +1 -0
  147. package/dist/runtime/utils/streaming-timing.js +76 -0
  148. package/dist/runtime/utils/streaming-timing.js.map +1 -0
  149. package/dist/runtime/utils/thread-message-like.js +2 -1
  150. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  151. package/dist/runtimes/external-store/external-store-adapter.d.ts +26 -1
  152. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +9 -0
  154. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  155. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +19 -0
  156. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  157. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  158. package/dist/runtimes/local/local-thread-runtime-core.js +1 -1
  159. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  160. package/dist/runtimes/remote-thread-list/types.d.ts +12 -0
  161. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  162. package/dist/store/clients/suggestions.js +1 -1
  163. package/dist/store/clients/thread-message-client.js +1 -1
  164. package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
  165. package/dist/store/runtime-clients/message-runtime-client.js +1 -1
  166. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  167. package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
  168. package/dist/tests/remote-thread-list-test-helpers.d.ts +1 -1
  169. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  170. package/dist/tests/remote-thread-list-test-helpers.js +3 -2
  171. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  172. package/package.json +9 -9
  173. package/src/index.ts +17 -0
  174. package/src/internal.ts +7 -0
  175. package/src/model-context/interactable-composer-metadata.test.ts +550 -0
  176. package/src/model-context/interactable-composer-metadata.ts +395 -0
  177. package/src/model-context/registry.ts +5 -0
  178. package/src/model-context/types.ts +12 -0
  179. package/src/react/client/Interactables.test.ts +358 -0
  180. package/src/react/client/Interactables.ts +339 -86
  181. package/src/react/client/interactable-model-context.test.ts +418 -0
  182. package/src/react/client/interactable-model-context.ts +252 -58
  183. package/src/react/index.ts +72 -3
  184. package/src/react/interactables-legacy/Interactables.ts +332 -0
  185. package/src/react/interactables-legacy/interactable-model-context.test.ts +54 -0
  186. package/src/react/interactables-legacy/interactable-model-context.ts +80 -0
  187. package/src/react/interactables-legacy/scopes.ts +66 -0
  188. package/src/react/{model-context → interactables-legacy}/useAssistantInteractable.ts +10 -1
  189. package/src/react/interactables-legacy/useInteractableState.ts +67 -0
  190. package/src/react/model-context/interactableTool.tsx +175 -0
  191. package/src/react/model-context/useInteractable.ts +233 -0
  192. package/src/react/model-context/useInteractableState.ts +31 -23
  193. package/src/react/model-context/useInteractableVersions.ts +48 -0
  194. package/src/react/primitive-hooks/useThreadMessageIds.ts +27 -0
  195. package/src/react/primitives/generativeUI/GenerativeUI.tsx +5 -1
  196. package/src/react/primitives/part/PartMessages.test.tsx +17 -0
  197. package/src/react/primitives/part/PartMessages.tsx +13 -8
  198. package/src/react/primitives/thread/ThreadMessages.tsx +67 -0
  199. package/src/react/providers/MessageByIdProvider.tsx +23 -0
  200. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +21 -1
  201. package/src/react/runtimes/createRuntimeExtras.test.ts +58 -0
  202. package/src/react/runtimes/createRuntimeExtras.ts +68 -0
  203. package/src/react/runtimes/createRuntimeExtras.use.test.ts +50 -0
  204. package/src/react/runtimes/useRemoteThreadListRuntime.ts +13 -1
  205. package/src/react/runtimes/useStreamingTiming.ts +68 -0
  206. package/src/react/types/scopes/interactables.ts +45 -23
  207. package/src/react/types/store-augmentation.ts +4 -2
  208. package/src/react/utils/useJSONEqual.ts +20 -0
  209. package/src/runtime/base/base-composer-runtime-core.ts +13 -0
  210. package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -1
  211. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  212. package/src/runtime/utils/message-repository.ts +11 -7
  213. package/src/runtime/utils/streaming-timing.test.ts +301 -0
  214. package/src/runtime/utils/streaming-timing.ts +161 -0
  215. package/src/runtime/utils/thread-message-like.ts +2 -1
  216. package/src/runtimes/external-store/external-store-adapter.ts +28 -0
  217. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +33 -1
  218. package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
  219. package/src/runtimes/remote-thread-list/types.ts +13 -0
  220. package/src/tests/MessageRepository.test.ts +2 -0
  221. package/src/tests/composer-can-send.test.ts +1 -0
  222. package/src/tests/default-edit-composer-runtime-core.test.ts +78 -1
  223. package/src/tests/default-thread-composer-runtime-core.test.ts +82 -0
  224. package/src/tests/external-store-thread-runtime-core.test.ts +337 -0
  225. package/src/tests/groupParts.test.ts +32 -0
  226. package/src/tests/remote-thread-list-reactive-threadId.test.ts +90 -0
  227. package/src/tests/remote-thread-list-test-helpers.ts +2 -1
  228. package/src/tests/thread-message-like.test.ts +50 -0
  229. package/dist/react/model-context/useAssistantInteractable.d.ts +0 -22
  230. package/dist/react/model-context/useAssistantInteractable.d.ts.map +0 -1
  231. package/dist/react/model-context/useAssistantInteractable.js.map +0 -1
@@ -0,0 +1,395 @@
1
+ import { isJSONValueEqual } from "../utils/json/is-json-equal";
2
+ import { isJSONValue, isRecord } from "../utils/json/is-json";
3
+
4
+ /**
5
+ * Unstable / Experimental — the interactables API is still evolving and may change in any release.
6
+ * @deprecated Unstable / Experimental (not actually removed).
7
+ */
8
+ export type Unstable_InteractableSnapshotEntry = {
9
+ id: string;
10
+ name: string;
11
+ state: unknown;
12
+ /**
13
+ * When true, `state` carries only the top-level fields that changed since
14
+ * the model's last known state; omitted fields are unchanged. The first
15
+ * snapshot of an instance is always full.
16
+ */
17
+ partial?: boolean | undefined;
18
+ };
19
+
20
+ /**
21
+ * Minimal message shape needed to read snapshots and `update_*` tool calls out
22
+ * of history. Structurally compatible with both `ThreadMessage` and the AI
23
+ * SDK's `UIMessage`.
24
+ */
25
+ type SnapshotCarrierMessage = {
26
+ role: string;
27
+ metadata?: unknown;
28
+ content?: readonly unknown[] | undefined;
29
+ };
30
+
31
+ /**
32
+ * Reads the interactable snapshots stamped on a message's
33
+ * `metadata.custom.interactables`, or `undefined` if none. This is the read
34
+ * half of the snapshot channel — integrations use it to surface interactable
35
+ * state to the model (see `unstable_injectInteractableContext` in
36
+ * `@assistant-ui/react-ai-sdk` for the AI SDK implementation).
37
+ *
38
+ * @deprecated Unstable / Experimental (not actually removed).
39
+ */
40
+ export function unstable_getInteractableSnapshots(message: {
41
+ metadata?: unknown;
42
+ }): Unstable_InteractableSnapshotEntry[] | undefined {
43
+ const metadata = message.metadata;
44
+ if (!metadata || typeof metadata !== "object") return undefined;
45
+ const custom = (metadata as Record<string, unknown>).custom;
46
+ if (!custom || typeof custom !== "object") return undefined;
47
+ const items = (custom as Record<string, unknown>).interactables;
48
+ return Array.isArray(items)
49
+ ? (items as Unstable_InteractableSnapshotEntry[])
50
+ : undefined;
51
+ }
52
+
53
+ /**
54
+ * Canonical model-facing wording for one snapshot entry.
55
+ *
56
+ * @deprecated Unstable / Experimental (not actually removed).
57
+ */
58
+ export function unstable_formatInteractableSnapshot(
59
+ entry: Unstable_InteractableSnapshotEntry,
60
+ ): string {
61
+ if (entry.partial) {
62
+ return `[State of "${entry.name}" (id: ${JSON.stringify(entry.id)}) changed — updated fields: ${JSON.stringify(entry.state)}; fields not listed are unchanged]`;
63
+ }
64
+ return `[Current state of "${entry.name}" (id: ${JSON.stringify(entry.id)}): ${JSON.stringify(entry.state)}]`;
65
+ }
66
+
67
+ /** The model-facing tool name for an interactable `name`. One tool per name. */
68
+ export function interactableToolName(name: string): string {
69
+ return `update_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
70
+ }
71
+
72
+ const getArrayItemId = (item: unknown): string | number | undefined => {
73
+ if (!isRecord(item)) return undefined;
74
+ const id = item.id;
75
+ return typeof id === "string" || typeof id === "number" ? id : undefined;
76
+ };
77
+
78
+ function applyArrayUpdate(
79
+ prev: unknown[],
80
+ update: Record<string, unknown>,
81
+ mintId?: () => string | undefined,
82
+ ) {
83
+ let next = Array.isArray(update.set) ? [...update.set] : [...prev];
84
+
85
+ if (update.clear === true) next = [];
86
+
87
+ if (Array.isArray(update.remove) && update.remove.length > 0) {
88
+ const ids = new Set(update.remove);
89
+ next = next.filter((item) => {
90
+ const id = getArrayItemId(item);
91
+ return id !== undefined ? !ids.has(id) : !ids.has(item);
92
+ });
93
+ }
94
+
95
+ const patches = update.update;
96
+ if (Array.isArray(patches) && patches.length > 0) {
97
+ next = next.map((item) => {
98
+ const id = getArrayItemId(item);
99
+ if (id === undefined || !isRecord(item)) return item;
100
+ const patch = patches.find(
101
+ (candidate) => isRecord(candidate) && candidate.id === id,
102
+ );
103
+ return patch ? { ...item, ...patch } : item;
104
+ });
105
+ }
106
+
107
+ if (Array.isArray(update.add) && update.add.length > 0) {
108
+ const added = mintId
109
+ ? update.add.map((item) => {
110
+ if (!isRecord(item) || item.id !== undefined) return item;
111
+ const id = mintId();
112
+ return id === undefined ? item : { ...item, id };
113
+ })
114
+ : update.add;
115
+ next = [...next, ...added];
116
+ }
117
+
118
+ return next;
119
+ }
120
+
121
+ /**
122
+ * The merge `update_*` tools apply: top-level keys replace, rest preserved.
123
+ * Array fields also accept operation objects (`add`, `update`, `remove`,
124
+ * `clear`, `set`) so collection edits do not need a companion manage_* tool.
125
+ */
126
+ export function shallowMergeInteractableState(
127
+ prev: unknown,
128
+ partial: unknown,
129
+ options?:
130
+ | {
131
+ arrayBaseline?: unknown;
132
+ /**
133
+ * Supplies ids for added items in id-keyed array fields. The live
134
+ * execute path mints fresh ids; history folds replay ids previously
135
+ * returned by execute.
136
+ */
137
+ idFactory?: (field: string) => string | undefined;
138
+ idKeyedFields?: ReadonlySet<string>;
139
+ }
140
+ | undefined,
141
+ ): unknown {
142
+ if (!isRecord(prev) || !isRecord(partial)) return partial;
143
+ const baseline = isRecord(options?.arrayBaseline)
144
+ ? options.arrayBaseline
145
+ : prev;
146
+ const next = { ...prev };
147
+ for (const [key, value] of Object.entries(partial)) {
148
+ const baseValue = baseline[key];
149
+ if (Array.isArray(baseValue) && isRecord(value)) {
150
+ const mintId =
151
+ options?.idFactory &&
152
+ (options.idKeyedFields === undefined || options.idKeyedFields.has(key))
153
+ ? () => options.idFactory?.(key)
154
+ : undefined;
155
+ next[key] = applyArrayUpdate(baseValue, value, mintId);
156
+ } else {
157
+ next[key] = value;
158
+ }
159
+ }
160
+ return next;
161
+ }
162
+
163
+ /**
164
+ * The top-level fields of `next` that differ from `known`, for stamping a
165
+ * partial snapshot. Returns `undefined` when a partial stamp has no benefit:
166
+ * the change is not expressible as a shallow merge (non-object states, or a
167
+ * top-level key was removed), or every field changed anyway — the caller
168
+ * falls back to a full snapshot.
169
+ */
170
+ function shallowDiffInteractableState(
171
+ known: unknown,
172
+ next: unknown,
173
+ ): Record<string, unknown> | undefined {
174
+ if (!isRecord(known) || !isRecord(next)) return undefined;
175
+ for (const key of Object.keys(known)) {
176
+ if (!(key in next)) return undefined;
177
+ }
178
+ const diff: Record<string, unknown> = {};
179
+ for (const [key, value] of Object.entries(next)) {
180
+ if (!(key in known) || !isJSONValueEqual(known[key], value)) {
181
+ diff[key] = value;
182
+ }
183
+ }
184
+ const changed = Object.keys(diff).length;
185
+ if (changed === 0 || changed === Object.keys(next).length) return undefined;
186
+ return diff;
187
+ }
188
+
189
+ type ToolCallLikePart = {
190
+ type?: string;
191
+ toolCallId?: string;
192
+ toolName?: string;
193
+ args?: unknown;
194
+ result?: unknown;
195
+ };
196
+
197
+ const asToolCallPart = (part: unknown): ToolCallLikePart | undefined => {
198
+ if (!part || typeof part !== "object") return undefined;
199
+ const p = part as ToolCallLikePart;
200
+ return p.type === "tool-call" ? p : undefined;
201
+ };
202
+
203
+ /** Whether an accepted `update_*` call addressed instance `id`. */
204
+ const updateCallTargets = (p: ToolCallLikePart, id: string): boolean => {
205
+ if (!p.args || typeof p.args !== "object") return false;
206
+ const result = isRecord(p.result) ? p.result : undefined;
207
+ if (result?.success === false) return false;
208
+ if (typeof result?.id === "string") return result.id === id;
209
+ const argsId = (p.args as Record<string, unknown>).id;
210
+ return argsId === id || argsId === undefined;
211
+ };
212
+
213
+ const createAddedItemIdFactory = (result: unknown) => {
214
+ const value = isRecord(result) ? result.addedItemIds : undefined;
215
+ if (!isRecord(value)) return undefined;
216
+
217
+ const idsByField = new Map<string, string[]>();
218
+ for (const [field, ids] of Object.entries(value)) {
219
+ if (!Array.isArray(ids)) continue;
220
+ const stringIds = ids.filter(
221
+ (item): item is string => typeof item === "string",
222
+ );
223
+ if (stringIds.length > 0) idsByField.set(field, stringIds);
224
+ }
225
+ if (idsByField.size === 0) return undefined;
226
+
227
+ return (field: string) => idsByField.get(field)?.shift();
228
+ };
229
+
230
+ /**
231
+ * Unstable / Experimental — the interactables API is still evolving and may change in any release.
232
+ * @deprecated Unstable / Experimental (not actually removed).
233
+ */
234
+ export type Unstable_InteractableVersion = {
235
+ /** The full state as of this version. */
236
+ state: unknown;
237
+ origin: "create" | "update" | "user-edit";
238
+ /** For create/update versions, the tool call that produced this version. */
239
+ toolCallId?: string | undefined;
240
+ };
241
+
242
+ // Without this, every mounted anchor re-folds the whole thread on each streaming
243
+ // token, since the repository hands out a new messages array per token.
244
+ const versionsCache = new WeakMap<
245
+ readonly SnapshotCarrierMessage[],
246
+ Map<string, Map<string, Unstable_InteractableVersion[]>>
247
+ >();
248
+
249
+ /**
250
+ * Every version of interactable `id` recorded in the thread, oldest first,
251
+ * folded chronologically:
252
+ *
253
+ * - the tool call whose `toolCallId` equals `id` seeds the baseline with its
254
+ * args (`origin: "create"`) — the `id: toolCallId` convention for
255
+ * tool-created interactables,
256
+ * - a snapshot stamped on a user message is a `"user-edit"` version (a full
257
+ * snapshot replaces the state; a partial one shallow-merges),
258
+ * - each accepted `update_*` call shallow-merges an `"update"` version.
259
+ *
260
+ * The last entry is the state the model knows. Partial snapshots and update
261
+ * calls with no baseline to merge into are skipped.
262
+ *
263
+ * @deprecated Unstable / Experimental (not actually removed).
264
+ */
265
+ export function unstable_getInteractableVersions(
266
+ messages: readonly SnapshotCarrierMessage[],
267
+ id: string,
268
+ name: string,
269
+ ): Unstable_InteractableVersion[] {
270
+ let byName = versionsCache.get(messages);
271
+ if (!byName) {
272
+ byName = new Map();
273
+ versionsCache.set(messages, byName);
274
+ }
275
+ let byId = byName.get(name);
276
+ if (!byId) {
277
+ byId = new Map();
278
+ byName.set(name, byId);
279
+ }
280
+ const cached = byId.get(id);
281
+ if (cached) return cached;
282
+
283
+ const toolName = interactableToolName(name);
284
+ const versions: Unstable_InteractableVersion[] = [];
285
+ const current = () => versions[versions.length - 1];
286
+
287
+ for (const msg of messages) {
288
+ if (msg.role === "user") {
289
+ const entry = unstable_getInteractableSnapshots(msg)?.find(
290
+ (it) => it.id === id,
291
+ );
292
+ if (!entry) continue;
293
+ if (entry.partial) {
294
+ const prev = current();
295
+ if (prev) {
296
+ versions.push({
297
+ state: shallowMergeInteractableState(prev.state, entry.state),
298
+ origin: "user-edit",
299
+ });
300
+ }
301
+ } else {
302
+ versions.push({ state: entry.state, origin: "user-edit" });
303
+ }
304
+ continue;
305
+ }
306
+
307
+ if (msg.role !== "assistant") continue;
308
+ for (const part of msg.content ?? []) {
309
+ const p = asToolCallPart(part);
310
+ if (!p) continue;
311
+ if (p.toolCallId === id && p.toolName === name) {
312
+ if (p.args && typeof p.args === "object") {
313
+ versions.push({ state: p.args, origin: "create", toolCallId: id });
314
+ }
315
+ } else if (p.toolName === toolName && updateCallTargets(p, id)) {
316
+ const prev = current();
317
+ if (prev) {
318
+ const { id: _id, ...partial } = p.args as Record<string, unknown>;
319
+ const idFactory = createAddedItemIdFactory(p.result);
320
+ versions.push({
321
+ state: idFactory
322
+ ? shallowMergeInteractableState(prev.state, partial, {
323
+ idFactory,
324
+ })
325
+ : shallowMergeInteractableState(prev.state, partial),
326
+ origin: "update",
327
+ toolCallId: p.toolCallId,
328
+ });
329
+ }
330
+ }
331
+ }
332
+ }
333
+ byId.set(id, versions);
334
+ return versions;
335
+ }
336
+
337
+ /**
338
+ * The state the model knows for interactable `id` — its latest recorded
339
+ * version, or `undefined` when no baseline exists yet.
340
+ */
341
+ export function findModelKnownState(
342
+ messages: readonly SnapshotCarrierMessage[],
343
+ id: string,
344
+ name: string,
345
+ ): { state: unknown } | undefined {
346
+ const versions = unstable_getInteractableVersions(messages, id, name);
347
+ const last = versions[versions.length - 1];
348
+ return last ? { state: last.state } : undefined;
349
+ }
350
+
351
+ /**
352
+ * Snapshot gate. Stamps an interactable only when the model doesn't already
353
+ * know its state (no baseline yet, or the live state differs from the folded
354
+ * thread record). A change that fits a shallow merge is stamped as a partial
355
+ * snapshot carrying just the changed fields; otherwise the full state is
356
+ * stamped. Other metadata keys pass through untouched.
357
+ */
358
+ export function gateInteractableComposerMetadata(
359
+ meta: Record<string, unknown> | undefined,
360
+ messages: readonly SnapshotCarrierMessage[],
361
+ ): Record<string, unknown> | undefined {
362
+ if (!meta) return undefined;
363
+ const { interactables, ...rest } = meta as {
364
+ interactables?: Unstable_InteractableSnapshotEntry[];
365
+ } & Record<string, unknown>;
366
+
367
+ const gated: Record<string, unknown> = { ...rest };
368
+ if (Array.isArray(interactables)) {
369
+ const pending: Unstable_InteractableSnapshotEntry[] = [];
370
+ for (const it of interactables) {
371
+ if (process.env.NODE_ENV !== "production" && !isJSONValue(it.state)) {
372
+ console.warn(
373
+ `[Interactables] state for "${it.name}" (${it.id}) is not JSON-equatable ` +
374
+ `(an undefined, NaN, Infinity, function, or symbol value?). It will be ` +
375
+ `re-snapshotted on every send, recreating per-message growth. Use plain ` +
376
+ `JSON values.`,
377
+ );
378
+ }
379
+ const known = findModelKnownState(messages, it.id, it.name);
380
+ if (!known) {
381
+ pending.push({ id: it.id, name: it.name, state: it.state });
382
+ continue;
383
+ }
384
+ if (isJSONValueEqual(it.state, known.state)) continue;
385
+ const diff = shallowDiffInteractableState(known.state, it.state);
386
+ pending.push(
387
+ diff
388
+ ? { id: it.id, name: it.name, state: diff, partial: true }
389
+ : { id: it.id, name: it.name, state: it.state },
390
+ );
391
+ }
392
+ if (pending.length) gated.interactables = pending;
393
+ }
394
+ return Object.keys(gated).length ? gated : undefined;
395
+ }
@@ -61,6 +61,11 @@ export class ModelContextRegistry implements ModelContextProvider {
61
61
  context.config = providerContexts.config;
62
62
  }
63
63
 
64
+ if (providerContexts.unstable_composerMetadata) {
65
+ context.unstable_composerMetadata =
66
+ providerContexts.unstable_composerMetadata;
67
+ }
68
+
64
69
  return context;
65
70
  }
66
71
 
@@ -24,6 +24,12 @@ export type ModelContext = {
24
24
  tools?: Record<string, Tool<any, any>> | undefined;
25
25
  callSettings?: LanguageModelV1CallSettings | undefined;
26
26
  config?: LanguageModelConfig | undefined;
27
+ /**
28
+ * Persisted message metadata pulled at send time and merged into the outgoing
29
+ * user message's `metadata.custom` (not forwarded to the model directly).
30
+ * Ignored by the transport, which only reads system/tools/callSettings/config.
31
+ */
32
+ unstable_composerMetadata?: Record<string, unknown> | undefined;
27
33
  };
28
34
 
29
35
  export type ModelContextProvider = {
@@ -107,6 +113,12 @@ export const mergeModelContexts = (
107
113
  ...config.callSettings,
108
114
  };
109
115
  }
116
+ if (config.unstable_composerMetadata) {
117
+ acc.unstable_composerMetadata = {
118
+ ...acc.unstable_composerMetadata,
119
+ ...config.unstable_composerMetadata,
120
+ };
121
+ }
110
122
  return acc;
111
123
  }, {} as ModelContext);
112
124
  };