@assistant-ui/core 0.1.9 → 0.1.10

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 (126) hide show
  1. package/dist/adapters/index.d.ts +3 -0
  2. package/dist/adapters/index.d.ts.map +1 -1
  3. package/dist/adapters/index.js +1 -0
  4. package/dist/adapters/index.js.map +1 -1
  5. package/dist/adapters/voice.d.ts +49 -0
  6. package/dist/adapters/voice.d.ts.map +1 -0
  7. package/dist/adapters/voice.js +109 -0
  8. package/dist/adapters/voice.js.map +1 -0
  9. package/dist/index.d.ts +5 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/model-context/types.d.ts +4 -0
  14. package/dist/model-context/types.d.ts.map +1 -1
  15. package/dist/model-context/types.js.map +1 -1
  16. package/dist/react/client/Interactables.d.ts.map +1 -1
  17. package/dist/react/client/Interactables.js +155 -65
  18. package/dist/react/client/Interactables.js.map +1 -1
  19. package/dist/react/client/interactable-model-context.d.ts +8 -0
  20. package/dist/react/client/interactable-model-context.d.ts.map +1 -0
  21. package/dist/react/client/interactable-model-context.js +62 -0
  22. package/dist/react/client/interactable-model-context.js.map +1 -0
  23. package/dist/react/index.d.ts +6 -3
  24. package/dist/react/index.d.ts.map +1 -1
  25. package/dist/react/index.js +5 -2
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/react/model-context/useAssistantContext.d.ts +4 -0
  28. package/dist/react/model-context/useAssistantContext.d.ts.map +1 -0
  29. package/dist/react/model-context/useAssistantContext.js +18 -0
  30. package/dist/react/model-context/useAssistantContext.js.map +1 -0
  31. package/dist/react/model-context/useAssistantInteractable.d.ts +18 -0
  32. package/dist/react/model-context/useAssistantInteractable.d.ts.map +1 -0
  33. package/dist/react/model-context/useAssistantInteractable.js +31 -0
  34. package/dist/react/model-context/useAssistantInteractable.js.map +1 -0
  35. package/dist/react/model-context/useInteractableState.d.ts +15 -0
  36. package/dist/react/model-context/useInteractableState.d.ts.map +1 -0
  37. package/dist/react/model-context/useInteractableState.js +36 -0
  38. package/dist/react/model-context/useInteractableState.js.map +1 -0
  39. package/dist/react/model-context/useToolArgsStatus.d.ts +8 -0
  40. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -0
  41. package/dist/react/model-context/useToolArgsStatus.js +31 -0
  42. package/dist/react/model-context/useToolArgsStatus.js.map +1 -0
  43. package/dist/react/primitive-hooks/useVoice.d.ts +10 -0
  44. package/dist/react/primitive-hooks/useVoice.d.ts.map +1 -0
  45. package/dist/react/primitive-hooks/useVoice.js +28 -0
  46. package/dist/react/primitive-hooks/useVoice.js.map +1 -0
  47. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +14 -0
  48. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  49. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +14 -0
  50. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  51. package/dist/react/runtimes/useLocalRuntime.d.ts +1 -0
  52. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  53. package/dist/react/types/scopes/interactables.d.ts +17 -0
  54. package/dist/react/types/scopes/interactables.d.ts.map +1 -1
  55. package/dist/runtime/api/thread-runtime.d.ts +21 -1
  56. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  57. package/dist/runtime/api/thread-runtime.js +25 -0
  58. package/dist/runtime/api/thread-runtime.js.map +1 -1
  59. package/dist/runtime/base/base-thread-runtime-core.d.ts +24 -1
  60. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  61. package/dist/runtime/base/base-thread-runtime-core.js +205 -1
  62. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  63. package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
  64. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  65. package/dist/runtimes/external-store/external-store-adapter.d.ts +2 -0
  66. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  67. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -1
  68. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  69. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +3 -1
  70. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  71. package/dist/runtimes/local/local-runtime-options.d.ts +2 -0
  72. package/dist/runtimes/local/local-runtime-options.d.ts.map +1 -1
  73. package/dist/runtimes/local/local-thread-runtime-core.d.ts +2 -0
  74. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  75. package/dist/runtimes/local/local-thread-runtime-core.js +6 -0
  76. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  77. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +8 -0
  78. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  79. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +15 -6
  80. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  81. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  82. package/dist/runtimes/remote-thread-list/empty-thread-core.js +17 -1
  83. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  84. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  85. package/dist/store/runtime-clients/thread-runtime-client.js +7 -8
  86. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  87. package/dist/store/scopes/thread.d.ts +9 -9
  88. package/dist/store/scopes/thread.d.ts.map +1 -1
  89. package/dist/types/message.d.ts +1 -0
  90. package/dist/types/message.d.ts.map +1 -1
  91. package/package.json +9 -15
  92. package/src/adapters/index.ts +5 -0
  93. package/src/adapters/voice.ts +166 -0
  94. package/src/index.ts +10 -0
  95. package/src/model-context/types.ts +5 -0
  96. package/src/react/client/Interactables.ts +221 -122
  97. package/src/react/client/interactable-model-context.ts +83 -0
  98. package/src/react/index.ts +19 -8
  99. package/src/react/model-context/useAssistantContext.ts +22 -0
  100. package/src/react/model-context/useAssistantInteractable.ts +47 -0
  101. package/src/react/model-context/useInteractableState.ts +63 -0
  102. package/src/react/model-context/useToolArgsStatus.ts +51 -0
  103. package/src/react/primitive-hooks/useVoice.ts +41 -0
  104. package/src/react/types/scopes/interactables.ts +22 -0
  105. package/src/runtime/api/thread-runtime.ts +41 -0
  106. package/src/runtime/base/base-thread-runtime-core.ts +243 -2
  107. package/src/runtime/interfaces/thread-runtime-core.ts +17 -0
  108. package/src/runtimes/external-store/external-store-adapter.ts +2 -0
  109. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +3 -1
  110. package/src/runtimes/local/local-runtime-options.ts +2 -0
  111. package/src/runtimes/local/local-thread-runtime-core.ts +7 -0
  112. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +20 -6
  113. package/src/runtimes/remote-thread-list/empty-thread-core.ts +22 -1
  114. package/src/store/runtime-clients/thread-runtime-client.ts +7 -8
  115. package/src/store/scopes/thread.ts +9 -8
  116. package/src/types/message.ts +1 -0
  117. package/dist/react/model-context/makeInteractable.d.ts +0 -10
  118. package/dist/react/model-context/makeInteractable.d.ts.map +0 -1
  119. package/dist/react/model-context/makeInteractable.js +0 -10
  120. package/dist/react/model-context/makeInteractable.js.map +0 -1
  121. package/dist/react/model-context/useInteractable.d.ts +0 -16
  122. package/dist/react/model-context/useInteractable.d.ts.map +0 -1
  123. package/dist/react/model-context/useInteractable.js +0 -36
  124. package/dist/react/model-context/useInteractable.js.map +0 -1
  125. package/src/react/model-context/makeInteractable.ts +0 -21
  126. package/src/react/model-context/useInteractable.ts +0 -73
@@ -13,34 +13,21 @@ import {
13
13
  } from "@assistant-ui/store";
14
14
  import type {
15
15
  InteractablesState,
16
- InteractableDefinition,
17
16
  InteractableRegistration,
18
17
  InteractableStateSchema,
18
+ InteractablePersistedState,
19
+ InteractablePersistenceAdapter,
19
20
  } from "../types/scopes/interactables";
20
21
  import { toJSONSchema, toPartialJSONSchema } from "assistant-stream";
21
- import type { Tool } from "assistant-stream";
22
22
  import { ModelContext } from "../../store";
23
+ import { buildInteractableModelContext } from "./interactable-model-context";
23
24
 
24
- function shallowMerge(prev: unknown, partial: unknown): unknown {
25
- if (
26
- typeof prev !== "object" ||
27
- prev === null ||
28
- typeof partial !== "object" ||
29
- partial === null ||
30
- Array.isArray(prev) ||
31
- Array.isArray(partial)
32
- ) {
33
- return partial;
34
- }
35
- return {
36
- ...(prev as Record<string, unknown>),
37
- ...(partial as Record<string, unknown>),
38
- };
39
- }
25
+ const PERSISTENCE_DEBOUNCE_MS = 500;
40
26
 
41
27
  export const Interactables = resource((): ClientOutput<"interactables"> => {
42
28
  const [state, setState] = tapState<InteractablesState>(() => ({
43
29
  definitions: {},
30
+ persistence: {},
44
31
  }));
45
32
 
46
33
  const clientRef = tapAssistantClientRef();
@@ -51,12 +38,167 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
51
38
  }, [state]);
52
39
 
53
40
  const subscribersRef = tapRef(new Set<() => void>());
54
-
55
41
  const partialSchemaCacheRef = tapRef(
56
42
  new Map<string, InteractableStateSchema>(),
57
43
  );
58
44
  const detachedStateRef = tapRef(new Map<string, unknown>());
59
45
 
46
+ const adapterRef = tapRef<InteractablePersistenceAdapter | undefined>(
47
+ undefined,
48
+ );
49
+ const debounceTimerRef = tapRef<ReturnType<typeof setTimeout> | undefined>(
50
+ undefined,
51
+ );
52
+ const syncSeqRef = tapRef(0);
53
+ const hasPendingLocalChangeRef = tapRef(false);
54
+ const flushResolversRef = tapRef<Array<() => void>>([]);
55
+ const dirtyIdsRef = tapRef(new Set<string>());
56
+
57
+ const runPersistence = tapCallback(async () => {
58
+ const adapter = adapterRef.current;
59
+ if (!adapter) {
60
+ for (const resolve of flushResolversRef.current) resolve();
61
+ flushResolversRef.current = [];
62
+ return;
63
+ }
64
+
65
+ const seq = ++syncSeqRef.current;
66
+ const dirtyIds = new Set(dirtyIdsRef.current);
67
+ dirtyIdsRef.current.clear();
68
+ hasPendingLocalChangeRef.current = true;
69
+
70
+ // Snapshot before any await so unregistered definitions are still included.
71
+ const exported = stateRef.current.definitions;
72
+ const payload: InteractablePersistedState = {};
73
+ for (const [id, def] of Object.entries(exported)) {
74
+ payload[id] = { name: def.name, state: def.state };
75
+ }
76
+
77
+ setState((prev) => ({
78
+ ...prev,
79
+ persistence: {
80
+ ...prev.persistence,
81
+ ...Object.fromEntries(
82
+ [...dirtyIds].map((id) => [
83
+ id,
84
+ { isPending: true, error: undefined },
85
+ ]),
86
+ ),
87
+ },
88
+ }));
89
+
90
+ try {
91
+ await adapter.save(payload);
92
+ if (syncSeqRef.current === seq) {
93
+ hasPendingLocalChangeRef.current = false;
94
+ setState((prev) => {
95
+ const persistence = { ...prev.persistence };
96
+ for (const id of dirtyIds) delete persistence[id];
97
+ return { ...prev, persistence };
98
+ });
99
+ }
100
+ } catch (e) {
101
+ if (syncSeqRef.current === seq) {
102
+ hasPendingLocalChangeRef.current = false;
103
+ setState((prev) => ({
104
+ ...prev,
105
+ persistence: {
106
+ ...prev.persistence,
107
+ ...Object.fromEntries(
108
+ [...dirtyIds].map((id) => [id, { isPending: false, error: e }]),
109
+ ),
110
+ },
111
+ }));
112
+ }
113
+ } finally {
114
+ if (dirtyIdsRef.current.size > 0 && adapterRef.current) {
115
+ runPersistence();
116
+ } else {
117
+ for (const resolve of flushResolversRef.current) resolve();
118
+ flushResolversRef.current = [];
119
+ }
120
+ }
121
+ }, []);
122
+
123
+ const schedulePersistence = tapCallback(
124
+ (id: string) => {
125
+ if (!adapterRef.current) return;
126
+ dirtyIdsRef.current.add(id);
127
+ if (debounceTimerRef.current !== undefined) {
128
+ clearTimeout(debounceTimerRef.current);
129
+ }
130
+ debounceTimerRef.current = setTimeout(() => {
131
+ debounceTimerRef.current = undefined;
132
+ if (!hasPendingLocalChangeRef.current) {
133
+ runPersistence();
134
+ } else {
135
+ debounceTimerRef.current = setTimeout(() => {
136
+ debounceTimerRef.current = undefined;
137
+ runPersistence();
138
+ }, PERSISTENCE_DEBOUNCE_MS);
139
+ }
140
+ }, PERSISTENCE_DEBOUNCE_MS);
141
+ },
142
+ [runPersistence],
143
+ );
144
+
145
+ const exportState = tapCallback((): InteractablePersistedState => {
146
+ const result: InteractablePersistedState = {};
147
+ for (const [id, def] of Object.entries(stateRef.current.definitions)) {
148
+ result[id] = { name: def.name, state: def.state };
149
+ }
150
+ return result;
151
+ }, []);
152
+
153
+ const importState = tapCallback((saved: InteractablePersistedState) => {
154
+ for (const [id, entry] of Object.entries(saved)) {
155
+ detachedStateRef.current.set(id, entry.state);
156
+ }
157
+ setState((prev) => {
158
+ let changed = false;
159
+ const definitions = { ...prev.definitions };
160
+ for (const [id, entry] of Object.entries(saved)) {
161
+ if (definitions[id]) {
162
+ definitions[id] = { ...definitions[id], state: entry.state };
163
+ changed = true;
164
+ }
165
+ }
166
+ return changed ? { ...prev, definitions } : prev;
167
+ });
168
+ }, []);
169
+
170
+ const setPersistenceAdapter = tapCallback(
171
+ (adapter: InteractablePersistenceAdapter | undefined) => {
172
+ adapterRef.current = adapter;
173
+ },
174
+ [],
175
+ );
176
+
177
+ const flush = tapCallback(async () => {
178
+ if (debounceTimerRef.current !== undefined) {
179
+ clearTimeout(debounceTimerRef.current);
180
+ debounceTimerRef.current = undefined;
181
+ }
182
+ if (!adapterRef.current) return;
183
+ if (!hasPendingLocalChangeRef.current && dirtyIdsRef.current.size === 0)
184
+ return;
185
+ const p = new Promise<void>((resolve) => {
186
+ flushResolversRef.current.push(resolve);
187
+ });
188
+ if (!hasPendingLocalChangeRef.current) {
189
+ runPersistence();
190
+ }
191
+ return p;
192
+ }, [runPersistence]);
193
+
194
+ const flushIfPending = tapCallback(() => {
195
+ if (adapterRef.current && debounceTimerRef.current !== undefined) {
196
+ clearTimeout(debounceTimerRef.current);
197
+ debounceTimerRef.current = undefined;
198
+ runPersistence();
199
+ }
200
+ }, [runPersistence]);
201
+
60
202
  const setDefState = tapCallback(
61
203
  (id: string, updater: (prev: unknown) => unknown) => {
62
204
  setState((prev) => {
@@ -70,8 +212,9 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
70
212
  },
71
213
  };
72
214
  });
215
+ if (stateRef.current.definitions[id]) schedulePersistence(id);
73
216
  },
74
- [],
217
+ [schedulePersistence],
75
218
  );
76
219
 
77
220
  const setDefSelected = tapCallback((id: string, selected: boolean) => {
@@ -92,63 +235,13 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
92
235
  () => ({
93
236
  getModelContext: () => {
94
237
  const defs = stateRef.current.definitions;
95
- const entries = Object.values(defs);
96
- if (entries.length === 0) return {};
97
-
98
- const byName = new Map<string, InteractableDefinition[]>();
99
- for (const def of entries) {
100
- const list = byName.get(def.name) ?? [];
101
- list.push(def);
102
- byName.set(def.name, list);
103
- }
104
-
105
- const systemParts: string[] = [];
106
- const tools: Record<string, Tool<any, any>> = {};
107
-
108
- for (const [name, instances] of byName) {
109
- const isMulti = instances.length > 1;
110
-
111
- for (const def of instances) {
112
- const selectedTag = def.selected ? " (SELECTED)" : "";
113
- const idTag = isMulti ? ` [id="${def.id}"]` : "";
114
-
115
- systemParts.push(
116
- `Interactable component "${name}"${idTag}${selectedTag} (${def.description}). Current state: ${JSON.stringify(def.state)}`,
117
- );
118
-
119
- const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
120
- const safeId = def.id.replace(/[^a-zA-Z0-9_-]/g, "_");
121
- const toolName = isMulti
122
- ? `update_${safeName}_${safeId}`
123
- : `update_${safeName}`;
124
-
125
- const partialSchema =
126
- partialSchemaCacheRef.current.get(def.id) ?? def.stateSchema;
127
-
128
- tools[toolName] = {
129
- type: "frontend" as const,
130
- description: `Update the state of interactable component "${name}"${isMulti ? ` (id: ${def.id})` : ""}. Only include the fields you want to change; omitted fields keep their current values. ${def.description}`,
131
- parameters: partialSchema,
132
- streamCall: async (reader) => {
133
- try {
134
- for await (const partialArgs of reader.args.streamValues()) {
135
- setDefState(def.id, (prev) =>
136
- shallowMerge(prev, partialArgs),
137
- );
138
- }
139
- } catch {
140
- // Non-fatal: execute handles the final state
141
- }
142
- },
143
- execute: async (partialState: unknown) => {
144
- setDefState(def.id, (prev) => shallowMerge(prev, partialState));
145
- return { success: true };
146
- },
147
- };
148
- }
149
- }
150
-
151
- return { system: systemParts.join("\n"), tools };
238
+ return (
239
+ buildInteractableModelContext(
240
+ defs,
241
+ partialSchemaCacheRef.current,
242
+ setDefState,
243
+ ) ?? {}
244
+ );
152
245
  },
153
246
  subscribe: (callback: () => void) => {
154
247
  subscribersRef.current.add(callback);
@@ -162,67 +255,73 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
162
255
 
163
256
  // biome-ignore lint/correctness/useExhaustiveDependencies: state dep triggers notification
164
257
  tapEffect(() => {
165
- for (const cb of subscribersRef.current) {
166
- cb();
167
- }
258
+ for (const cb of subscribersRef.current) cb();
168
259
  }, [state]);
169
260
 
170
261
  tapEffect(() => {
171
262
  return clientRef.current!.modelContext().register(provider);
172
263
  }, [clientRef, provider]);
173
264
 
174
- const register = tapCallback((def: InteractableRegistration) => {
175
- try {
176
- const jsonSchema = toJSONSchema(def.stateSchema);
177
- partialSchemaCacheRef.current.set(
178
- def.id,
179
- toPartialJSONSchema(jsonSchema),
180
- );
181
- } catch (e) {
182
- // Fall back to the raw schema; partial updates may not work correctly
183
- console.warn(
184
- `[Interactables] Failed to create partial schema for "${def.name}". The update tool will require all fields.`,
185
- e,
186
- );
187
- }
265
+ const register = tapCallback(
266
+ (def: InteractableRegistration) => {
267
+ try {
268
+ const jsonSchema = toJSONSchema(def.stateSchema);
269
+ partialSchemaCacheRef.current.set(
270
+ def.id,
271
+ toPartialJSONSchema(jsonSchema),
272
+ );
273
+ } catch (e) {
274
+ console.warn(
275
+ `[Interactables] Failed to create partial schema for "${def.name}". The update tool will require all fields.`,
276
+ e,
277
+ );
278
+ }
188
279
 
189
- const detached = detachedStateRef.current.get(def.id);
190
- detachedStateRef.current.delete(def.id);
280
+ const detached = detachedStateRef.current.get(def.id);
281
+ detachedStateRef.current.delete(def.id);
191
282
 
192
- setState((prev) => ({
193
- ...prev,
194
- definitions: {
195
- ...prev.definitions,
196
- [def.id]: {
197
- id: def.id,
198
- name: def.name,
199
- description: def.description,
200
- stateSchema: def.stateSchema,
201
- state:
202
- prev.definitions[def.id]?.state ?? detached ?? def.initialState,
203
- selected: def.selected,
283
+ setState((prev) => ({
284
+ ...prev,
285
+ definitions: {
286
+ ...prev.definitions,
287
+ [def.id]: {
288
+ id: def.id,
289
+ name: def.name,
290
+ description: def.description,
291
+ stateSchema: def.stateSchema,
292
+ state:
293
+ prev.definitions[def.id]?.state ?? detached ?? def.initialState,
294
+ selected: def.selected,
295
+ },
204
296
  },
205
- },
206
- }));
297
+ }));
207
298
 
208
- return () => {
209
- setState((prev) => {
210
- const existing = prev.definitions[def.id];
211
- if (existing) {
212
- detachedStateRef.current.set(def.id, existing.state);
213
- }
214
- partialSchemaCacheRef.current.delete(def.id);
215
- const { [def.id]: _, ...rest } = prev.definitions;
216
- return { ...prev, definitions: rest };
217
- });
218
- };
219
- }, []);
299
+ return () => {
300
+ flushIfPending();
301
+ setState((prev) => {
302
+ const existing = prev.definitions[def.id];
303
+ if (existing) {
304
+ detachedStateRef.current.set(def.id, existing.state);
305
+ }
306
+ partialSchemaCacheRef.current.delete(def.id);
307
+ const { [def.id]: _, ...rest } = prev.definitions;
308
+ const { [def.id]: __, ...restPersistence } = prev.persistence;
309
+ return { ...prev, definitions: rest, persistence: restPersistence };
310
+ });
311
+ };
312
+ },
313
+ [flushIfPending],
314
+ );
220
315
 
221
316
  return {
222
317
  getState: () => state,
223
318
  register,
224
319
  setState: setDefState,
225
320
  setSelected: setDefSelected,
321
+ exportState,
322
+ importState,
323
+ setPersistenceAdapter,
324
+ flush,
226
325
  };
227
326
  });
228
327
 
@@ -0,0 +1,83 @@
1
+ import type { Tool } from "assistant-stream";
2
+ import type {
3
+ InteractableDefinition,
4
+ InteractableStateSchema,
5
+ } from "../types/scopes/interactables";
6
+
7
+ export function shallowMerge(prev: unknown, partial: unknown): unknown {
8
+ if (
9
+ typeof prev !== "object" ||
10
+ prev === null ||
11
+ typeof partial !== "object" ||
12
+ partial === null ||
13
+ Array.isArray(prev) ||
14
+ Array.isArray(partial)
15
+ ) {
16
+ return partial;
17
+ }
18
+ return {
19
+ ...(prev as Record<string, unknown>),
20
+ ...(partial as Record<string, unknown>),
21
+ };
22
+ }
23
+
24
+ export function buildInteractableModelContext(
25
+ definitions: Record<string, InteractableDefinition>,
26
+ partialSchemaCache: Map<string, InteractableStateSchema>,
27
+ setDefState: (id: string, updater: (prev: unknown) => unknown) => void,
28
+ ): { system: string; tools: Record<string, Tool<any, any>> } | undefined {
29
+ const entries = Object.values(definitions);
30
+ if (entries.length === 0) return undefined;
31
+
32
+ const byName = new Map<string, InteractableDefinition[]>();
33
+ for (const def of entries) {
34
+ const list = byName.get(def.name) ?? [];
35
+ list.push(def);
36
+ byName.set(def.name, list);
37
+ }
38
+
39
+ const systemParts: string[] = [];
40
+ const tools: Record<string, Tool<any, any>> = {};
41
+
42
+ for (const [name, instances] of byName) {
43
+ const isMulti = instances.length > 1;
44
+
45
+ for (const def of instances) {
46
+ const selectedTag = def.selected ? " (SELECTED)" : "";
47
+ const idTag = isMulti ? ` [id="${def.id}"]` : "";
48
+
49
+ systemParts.push(
50
+ `Interactable component "${name}"${idTag}${selectedTag} (${def.description}). Current state: ${JSON.stringify(def.state)}`,
51
+ );
52
+
53
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
54
+ const safeId = def.id.replace(/[^a-zA-Z0-9_-]/g, "_");
55
+ const toolName = isMulti
56
+ ? `update_${safeName}_${safeId}`
57
+ : `update_${safeName}`;
58
+
59
+ const partialSchema = partialSchemaCache.get(def.id) ?? def.stateSchema;
60
+
61
+ tools[toolName] = {
62
+ type: "frontend" as const,
63
+ description: `Update the state of interactable component "${name}"${isMulti ? ` (id: ${def.id})` : ""}. Only include the fields you want to change; omitted fields keep their current values. ${def.description}`,
64
+ parameters: partialSchema,
65
+ streamCall: async (reader) => {
66
+ try {
67
+ for await (const partialArgs of reader.args.streamValues()) {
68
+ setDefState(def.id, (prev) => shallowMerge(prev, partialArgs));
69
+ }
70
+ } catch {
71
+ // Non-fatal: execute handles the final state
72
+ }
73
+ },
74
+ execute: async (partialState: unknown) => {
75
+ setDefState(def.id, (prev) => shallowMerge(prev, partialState));
76
+ return { success: true };
77
+ },
78
+ };
79
+ }
80
+ }
81
+
82
+ return { system: systemParts.join("\n"), tools };
83
+ }
@@ -14,6 +14,10 @@ export {
14
14
  makeAssistantDataUI,
15
15
  } from "./model-context/makeAssistantDataUI";
16
16
  export { useAssistantInstructions } from "./model-context/useAssistantInstructions";
17
+ export {
18
+ useAssistantContext,
19
+ type AssistantContextConfig,
20
+ } from "./model-context/useAssistantContext";
17
21
  export {
18
22
  useAssistantTool,
19
23
  type AssistantToolProps,
@@ -29,15 +33,14 @@ export {
29
33
  export { useInlineRender } from "./model-context/useInlineRender";
30
34
  export type { Toolkit, ToolDefinition } from "./model-context/toolbox";
31
35
  export {
32
- useInteractable,
33
- type UseInteractableConfig,
34
- type UseInteractableMetadata,
35
- } from "./model-context/useInteractable";
36
+ useAssistantInteractable,
37
+ type AssistantInteractableProps,
38
+ } from "./model-context/useAssistantInteractable";
39
+ export { useInteractableState } from "./model-context/useInteractableState";
36
40
  export {
37
- makeInteractable,
38
- type InteractableConfig,
39
- type AssistantInteractable,
40
- } from "./model-context/makeInteractable";
41
+ useToolArgsStatus,
42
+ type ToolArgsStatus,
43
+ } from "./model-context/useToolArgsStatus";
41
44
 
42
45
  // client
43
46
  export { Tools } from "./client/Tools";
@@ -85,6 +88,9 @@ export type {
85
88
  InteractableDefinition,
86
89
  InteractableRegistration,
87
90
  InteractablesMethods,
91
+ InteractablePersistedState,
92
+ InteractablePersistenceAdapter,
93
+ InteractablePersistenceStatus,
88
94
  InteractablesClientSchema,
89
95
  } from "./types/scopes/interactables";
90
96
 
@@ -211,6 +217,11 @@ export {
211
217
  } from "./primitive-hooks/useActionBarFeedback";
212
218
  export { useActionBarSpeak } from "./primitive-hooks/useActionBarSpeak";
213
219
  export { useActionBarStopSpeaking } from "./primitive-hooks/useActionBarStopSpeaking";
220
+ export {
221
+ useVoiceState,
222
+ useVoiceVolume,
223
+ useVoiceControls,
224
+ } from "./primitive-hooks/useVoice";
214
225
  export { useBranchPickerNext } from "./primitive-hooks/useBranchPickerNext";
215
226
  export { useBranchPickerPrevious } from "./primitive-hooks/useBranchPickerPrevious";
216
227
  export {
@@ -0,0 +1,22 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useAui } from "@assistant-ui/store";
3
+ import type { AssistantContextConfig } from "../..";
4
+
5
+ export type { AssistantContextConfig };
6
+
7
+ export const useAssistantContext = (config: AssistantContextConfig) => {
8
+ const { getContext, disabled = false } = config;
9
+ const aui = useAui();
10
+ const getContextRef = useRef(getContext);
11
+ getContextRef.current = getContext;
12
+
13
+ useEffect(() => {
14
+ if (disabled) return;
15
+
16
+ return aui.modelContext().register({
17
+ getModelContext: () => ({
18
+ system: getContextRef.current(),
19
+ }),
20
+ });
21
+ }, [aui, disabled]);
22
+ };
@@ -0,0 +1,47 @@
1
+ import { useEffect, useId, useRef } from "react";
2
+ import { useAui } from "@assistant-ui/store";
3
+ import type { InteractableStateSchema } from "../types/scopes/interactables";
4
+
5
+ export type AssistantInteractableProps = {
6
+ description: string;
7
+ stateSchema: InteractableStateSchema;
8
+ initialState: unknown;
9
+ id?: string;
10
+ selected?: boolean;
11
+ };
12
+
13
+ /**
14
+ * Registers an interactable with the AI assistant.
15
+ *
16
+ * This hook handles registration only. To read and write the interactable's
17
+ * state, use {@link useInteractableState} with the returned id.
18
+ *
19
+ * @returns The interactable instance id.
20
+ */
21
+ export const useAssistantInteractable = (
22
+ name: string,
23
+ config: AssistantInteractableProps,
24
+ ): string => {
25
+ const aui = useAui();
26
+
27
+ const autoId = useId().replace(/[^a-zA-Z0-9]/g, "");
28
+ const id = config.id ?? autoId;
29
+
30
+ const stateSchemaRef = useRef(config.stateSchema);
31
+ stateSchemaRef.current = config.stateSchema;
32
+ const initialStateRef = useRef(config.initialState);
33
+ initialStateRef.current = config.initialState;
34
+
35
+ useEffect(() => {
36
+ return aui.interactables().register({
37
+ id,
38
+ name,
39
+ description: config.description,
40
+ stateSchema: stateSchemaRef.current,
41
+ initialState: initialStateRef.current,
42
+ selected: config.selected,
43
+ });
44
+ }, [aui, id, name, config.description, config.selected]);
45
+
46
+ return id;
47
+ };
@@ -0,0 +1,63 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ type StateUpdater<TState> = TState | ((prev: TState) => TState);
5
+
6
+ /**
7
+ * Reads and writes the state of a registered interactable.
8
+ *
9
+ * Pair with {@link useAssistantInteractable} which handles registration.
10
+ */
11
+ export const useInteractableState = <TState>(
12
+ id: string,
13
+ fallback: TState,
14
+ ): [
15
+ TState,
16
+ {
17
+ setState: (updater: StateUpdater<TState>) => void;
18
+ setSelected: (selected: boolean) => void;
19
+ isPending: boolean;
20
+ error: unknown;
21
+ flush: () => Promise<void>;
22
+ },
23
+ ] => {
24
+ const aui = useAui();
25
+
26
+ const state =
27
+ (useAuiState((s) => s.interactables.definitions[id]?.state) as TState) ??
28
+ (fallback as TState);
29
+
30
+ const persistenceStatus = useAuiState((s) => s.interactables.persistence[id]);
31
+
32
+ const setState = useCallback(
33
+ (updater: StateUpdater<TState>) => {
34
+ aui.interactables().setState(id, (prev) => {
35
+ if (typeof updater === "function") {
36
+ return (updater as (prev: TState) => TState)(prev as TState);
37
+ }
38
+ return updater;
39
+ });
40
+ },
41
+ [aui, id],
42
+ );
43
+
44
+ const setSelected = useCallback(
45
+ (selected: boolean) => {
46
+ aui.interactables().setSelected(id, selected);
47
+ },
48
+ [aui, id],
49
+ );
50
+
51
+ const flush = useCallback(() => aui.interactables().flush(), [aui]);
52
+
53
+ return [
54
+ state,
55
+ {
56
+ setState,
57
+ setSelected,
58
+ isPending: persistenceStatus?.isPending ?? false,
59
+ error: persistenceStatus?.error,
60
+ flush,
61
+ },
62
+ ];
63
+ };