@assistant-ui/core 0.1.8 → 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 (146) hide show
  1. package/dist/adapters/attachment.d.ts +4 -0
  2. package/dist/adapters/attachment.d.ts.map +1 -1
  3. package/dist/adapters/attachment.js +1 -1
  4. package/dist/adapters/attachment.js.map +1 -1
  5. package/dist/adapters/index.d.ts +3 -0
  6. package/dist/adapters/index.d.ts.map +1 -1
  7. package/dist/adapters/index.js +1 -0
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/adapters/voice.d.ts +49 -0
  10. package/dist/adapters/voice.d.ts.map +1 -0
  11. package/dist/adapters/voice.js +109 -0
  12. package/dist/adapters/voice.js.map +1 -0
  13. package/dist/index.d.ts +5 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/model-context/types.d.ts +4 -0
  18. package/dist/model-context/types.d.ts.map +1 -1
  19. package/dist/model-context/types.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts +3 -0
  21. package/dist/react/client/Interactables.d.ts.map +1 -0
  22. package/dist/react/client/Interactables.js +263 -0
  23. package/dist/react/client/Interactables.js.map +1 -0
  24. package/dist/react/client/interactable-model-context.d.ts +8 -0
  25. package/dist/react/client/interactable-model-context.d.ts.map +1 -0
  26. package/dist/react/client/interactable-model-context.js +62 -0
  27. package/dist/react/client/interactable-model-context.js.map +1 -0
  28. package/dist/react/index.d.ts +7 -0
  29. package/dist/react/index.d.ts.map +1 -1
  30. package/dist/react/index.js +6 -0
  31. package/dist/react/index.js.map +1 -1
  32. package/dist/react/model-context/useAssistantContext.d.ts +4 -0
  33. package/dist/react/model-context/useAssistantContext.d.ts.map +1 -0
  34. package/dist/react/model-context/useAssistantContext.js +18 -0
  35. package/dist/react/model-context/useAssistantContext.js.map +1 -0
  36. package/dist/react/model-context/useAssistantInteractable.d.ts +18 -0
  37. package/dist/react/model-context/useAssistantInteractable.d.ts.map +1 -0
  38. package/dist/react/model-context/useAssistantInteractable.js +31 -0
  39. package/dist/react/model-context/useAssistantInteractable.js.map +1 -0
  40. package/dist/react/model-context/useInteractableState.d.ts +15 -0
  41. package/dist/react/model-context/useInteractableState.d.ts.map +1 -0
  42. package/dist/react/model-context/useInteractableState.js +36 -0
  43. package/dist/react/model-context/useInteractableState.js.map +1 -0
  44. package/dist/react/model-context/useToolArgsStatus.d.ts +8 -0
  45. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -0
  46. package/dist/react/model-context/useToolArgsStatus.js +31 -0
  47. package/dist/react/model-context/useToolArgsStatus.js.map +1 -0
  48. package/dist/react/primitive-hooks/useVoice.d.ts +10 -0
  49. package/dist/react/primitive-hooks/useVoice.d.ts.map +1 -0
  50. package/dist/react/primitive-hooks/useVoice.js +28 -0
  51. package/dist/react/primitive-hooks/useVoice.js.map +1 -0
  52. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  53. package/dist/react/primitives/message/MessageParts.js +2 -0
  54. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  55. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +14 -0
  56. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  57. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +14 -0
  58. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  59. package/dist/react/runtimes/useLocalRuntime.d.ts +1 -0
  60. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  61. package/dist/react/types/scopes/interactables.d.ts +56 -0
  62. package/dist/react/types/scopes/interactables.d.ts.map +1 -0
  63. package/dist/react/types/scopes/interactables.js +2 -0
  64. package/dist/react/types/scopes/interactables.js.map +1 -0
  65. package/dist/react/types/store-augmentation.d.ts +2 -0
  66. package/dist/react/types/store-augmentation.d.ts.map +1 -1
  67. package/dist/runtime/api/thread-runtime.d.ts +21 -1
  68. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  69. package/dist/runtime/api/thread-runtime.js +25 -0
  70. package/dist/runtime/api/thread-runtime.js.map +1 -1
  71. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  72. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  73. package/dist/runtime/base/base-composer-runtime-core.js +33 -8
  74. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  75. package/dist/runtime/base/base-thread-runtime-core.d.ts +24 -1
  76. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  77. package/dist/runtime/base/base-thread-runtime-core.js +205 -1
  78. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  79. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  80. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  81. package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
  82. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  83. package/dist/runtimes/external-store/external-store-adapter.d.ts +2 -0
  84. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  85. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -1
  86. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  87. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +3 -1
  88. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  89. package/dist/runtimes/local/local-runtime-options.d.ts +2 -0
  90. package/dist/runtimes/local/local-runtime-options.d.ts.map +1 -1
  91. package/dist/runtimes/local/local-thread-runtime-core.d.ts +2 -0
  92. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  93. package/dist/runtimes/local/local-thread-runtime-core.js +6 -0
  94. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  95. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +8 -0
  96. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  97. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +15 -6
  98. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  99. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  100. package/dist/runtimes/remote-thread-list/empty-thread-core.js +17 -1
  101. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  102. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  103. package/dist/store/runtime-clients/composer-runtime-client.js +12 -5
  104. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  105. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  106. package/dist/store/runtime-clients/thread-runtime-client.js +7 -8
  107. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  108. package/dist/store/scopes/composer.d.ts +5 -0
  109. package/dist/store/scopes/composer.d.ts.map +1 -1
  110. package/dist/store/scopes/thread.d.ts +9 -9
  111. package/dist/store/scopes/thread.d.ts.map +1 -1
  112. package/dist/types/message.d.ts +1 -0
  113. package/dist/types/message.d.ts.map +1 -1
  114. package/package.json +9 -15
  115. package/src/adapters/attachment.ts +1 -1
  116. package/src/adapters/index.ts +5 -0
  117. package/src/adapters/voice.ts +166 -0
  118. package/src/index.ts +10 -0
  119. package/src/model-context/types.ts +5 -0
  120. package/src/react/client/Interactables.ts +332 -0
  121. package/src/react/client/interactable-model-context.ts +83 -0
  122. package/src/react/index.ts +30 -0
  123. package/src/react/model-context/useAssistantContext.ts +22 -0
  124. package/src/react/model-context/useAssistantInteractable.ts +47 -0
  125. package/src/react/model-context/useInteractableState.ts +63 -0
  126. package/src/react/model-context/useToolArgsStatus.ts +51 -0
  127. package/src/react/primitive-hooks/useVoice.ts +41 -0
  128. package/src/react/primitives/message/MessageParts.tsx +2 -0
  129. package/src/react/types/scopes/interactables.ts +66 -0
  130. package/src/react/types/store-augmentation.ts +2 -0
  131. package/src/runtime/api/thread-runtime.ts +41 -0
  132. package/src/runtime/base/base-composer-runtime-core.ts +45 -9
  133. package/src/runtime/base/base-thread-runtime-core.ts +243 -2
  134. package/src/runtime/interfaces/composer-runtime-core.ts +4 -1
  135. package/src/runtime/interfaces/thread-runtime-core.ts +17 -0
  136. package/src/runtimes/external-store/external-store-adapter.ts +2 -0
  137. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +3 -1
  138. package/src/runtimes/local/local-runtime-options.ts +2 -0
  139. package/src/runtimes/local/local-thread-runtime-core.ts +7 -0
  140. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +20 -6
  141. package/src/runtimes/remote-thread-list/empty-thread-core.ts +22 -1
  142. package/src/store/runtime-clients/composer-runtime-client.ts +18 -7
  143. package/src/store/runtime-clients/thread-runtime-client.ts +7 -8
  144. package/src/store/scopes/composer.ts +5 -0
  145. package/src/store/scopes/thread.ts +9 -8
  146. package/src/types/message.ts +1 -0
@@ -0,0 +1,166 @@
1
+ import type { Unsubscribe } from "../types/unsubscribe";
2
+
3
+ export namespace RealtimeVoiceAdapter {
4
+ export type Status =
5
+ | {
6
+ type: "starting" | "running";
7
+ }
8
+ | {
9
+ type: "ended";
10
+ reason: "finished" | "cancelled" | "error";
11
+ error?: unknown;
12
+ };
13
+
14
+ export type Mode = "listening" | "speaking";
15
+
16
+ export type TranscriptItem = {
17
+ role: "user" | "assistant";
18
+ text: string;
19
+ isFinal?: boolean;
20
+ };
21
+
22
+ export type Session = {
23
+ status: Status;
24
+ isMuted: boolean;
25
+
26
+ disconnect: () => void;
27
+ mute: () => void;
28
+ unmute: () => void;
29
+
30
+ onStatusChange: (callback: (status: Status) => void) => Unsubscribe;
31
+ onTranscript: (
32
+ callback: (transcript: TranscriptItem) => void,
33
+ ) => Unsubscribe;
34
+ onModeChange: (callback: (mode: Mode) => void) => Unsubscribe;
35
+ onVolumeChange: (callback: (volume: number) => void) => Unsubscribe;
36
+ };
37
+ }
38
+
39
+ export type RealtimeVoiceAdapter = {
40
+ connect: (options: {
41
+ abortSignal?: AbortSignal;
42
+ }) => RealtimeVoiceAdapter.Session;
43
+ };
44
+
45
+ export type VoiceSessionControls = {
46
+ disconnect: () => void;
47
+ mute: () => void;
48
+ unmute: () => void;
49
+ };
50
+
51
+ export type VoiceSessionHelpers = {
52
+ setStatus: (status: RealtimeVoiceAdapter.Status) => void;
53
+ end: (reason: "finished" | "cancelled" | "error", error?: unknown) => void;
54
+ emitTranscript: (item: RealtimeVoiceAdapter.TranscriptItem) => void;
55
+ emitMode: (mode: RealtimeVoiceAdapter.Mode) => void;
56
+ emitVolume: (volume: number) => void;
57
+ isDisposed: () => boolean;
58
+ };
59
+
60
+ export function createVoiceSession(
61
+ options: { abortSignal?: AbortSignal },
62
+ setup: (helpers: VoiceSessionHelpers) => Promise<VoiceSessionControls>,
63
+ ): RealtimeVoiceAdapter.Session {
64
+ const statusCbs = new Set<(s: RealtimeVoiceAdapter.Status) => void>();
65
+ const transcriptCbs = new Set<
66
+ (t: RealtimeVoiceAdapter.TranscriptItem) => void
67
+ >();
68
+ const modeCbs = new Set<(m: RealtimeVoiceAdapter.Mode) => void>();
69
+ const volumeCbs = new Set<(v: number) => void>();
70
+
71
+ let currentStatus: RealtimeVoiceAdapter.Status = { type: "starting" };
72
+ let isMuted = false;
73
+ let disposed = false;
74
+ let controls: VoiceSessionControls | null = null;
75
+
76
+ const cleanup = () => {
77
+ disposed = true;
78
+ statusCbs.clear();
79
+ transcriptCbs.clear();
80
+ modeCbs.clear();
81
+ volumeCbs.clear();
82
+ };
83
+
84
+ const helpers: VoiceSessionHelpers = {
85
+ setStatus: (status) => {
86
+ if (disposed) return;
87
+ currentStatus = status;
88
+ for (const cb of statusCbs) cb(status);
89
+ },
90
+ end: (reason, error?) => {
91
+ if (disposed) return;
92
+ currentStatus = { type: "ended", reason, error };
93
+ for (const cb of statusCbs) cb(currentStatus);
94
+ cleanup();
95
+ },
96
+ emitTranscript: (item) => {
97
+ if (disposed) return;
98
+ for (const cb of transcriptCbs) cb(item);
99
+ },
100
+ emitMode: (mode) => {
101
+ if (disposed) return;
102
+ for (const cb of modeCbs) cb(mode);
103
+ },
104
+ emitVolume: (volume) => {
105
+ if (disposed) return;
106
+ for (const cb of volumeCbs) cb(volume);
107
+ },
108
+ isDisposed: () => disposed,
109
+ };
110
+
111
+ const session: RealtimeVoiceAdapter.Session = {
112
+ get status() {
113
+ return currentStatus;
114
+ },
115
+ get isMuted() {
116
+ return isMuted;
117
+ },
118
+ disconnect: () => {
119
+ controls?.disconnect();
120
+ cleanup();
121
+ },
122
+ mute: () => {
123
+ controls?.mute();
124
+ isMuted = true;
125
+ },
126
+ unmute: () => {
127
+ controls?.unmute();
128
+ isMuted = false;
129
+ },
130
+ onStatusChange: (cb) => {
131
+ statusCbs.add(cb);
132
+ return () => statusCbs.delete(cb);
133
+ },
134
+ onTranscript: (cb) => {
135
+ transcriptCbs.add(cb);
136
+ return () => transcriptCbs.delete(cb);
137
+ },
138
+ onModeChange: (cb) => {
139
+ modeCbs.add(cb);
140
+ return () => modeCbs.delete(cb);
141
+ },
142
+ onVolumeChange: (cb) => {
143
+ volumeCbs.add(cb);
144
+ return () => volumeCbs.delete(cb);
145
+ },
146
+ };
147
+
148
+ if (options.abortSignal) {
149
+ options.abortSignal.addEventListener("abort", () => session.disconnect(), {
150
+ once: true,
151
+ });
152
+ }
153
+
154
+ const doSetup = async () => {
155
+ try {
156
+ if (disposed) return;
157
+ controls = await setup(helpers);
158
+ if (disposed) controls.disconnect();
159
+ } catch (error) {
160
+ helpers.end("error", error);
161
+ }
162
+ };
163
+
164
+ doSetup();
165
+ return session;
166
+ }
package/src/index.ts CHANGED
@@ -62,6 +62,7 @@ export type {
62
62
  // Tool & instruction config
63
63
  AssistantToolProps,
64
64
  AssistantInstructionsConfig,
65
+ AssistantContextConfig,
65
66
  } from "./model-context/types";
66
67
  export { mergeModelContexts } from "./model-context/types";
67
68
 
@@ -104,6 +105,14 @@ export {
104
105
  WebSpeechDictationAdapter,
105
106
  } from "./adapters/speech";
106
107
 
108
+ // Voice adapter
109
+ export type { RealtimeVoiceAdapter } from "./adapters/voice";
110
+ export { createVoiceSession } from "./adapters/voice";
111
+ export type {
112
+ VoiceSessionControls,
113
+ VoiceSessionHelpers,
114
+ } from "./adapters/voice";
115
+
107
116
  // Feedback adapter
108
117
  export type { FeedbackAdapter } from "./adapters/feedback";
109
118
 
@@ -151,6 +160,7 @@ export type {
151
160
  SubmitFeedbackOptions,
152
161
  ThreadSuggestion,
153
162
  SpeechState,
163
+ VoiceSessionState,
154
164
  SubmittedFeedback,
155
165
  ThreadRuntimeEventType,
156
166
  StartRunConfig,
@@ -55,6 +55,11 @@ export type AssistantInstructionsConfig = {
55
55
  instruction: string;
56
56
  };
57
57
 
58
+ export type AssistantContextConfig = {
59
+ getContext: () => string;
60
+ disabled?: boolean | undefined;
61
+ };
62
+
58
63
  // =============================================================================
59
64
  // Merging
60
65
  // =============================================================================
@@ -0,0 +1,332 @@
1
+ import {
2
+ resource,
3
+ tapState,
4
+ tapEffect,
5
+ tapCallback,
6
+ tapRef,
7
+ tapMemo,
8
+ } from "@assistant-ui/tap";
9
+ import {
10
+ tapAssistantClientRef,
11
+ type ClientOutput,
12
+ attachTransformScopes,
13
+ } from "@assistant-ui/store";
14
+ import type {
15
+ InteractablesState,
16
+ InteractableRegistration,
17
+ InteractableStateSchema,
18
+ InteractablePersistedState,
19
+ InteractablePersistenceAdapter,
20
+ } from "../types/scopes/interactables";
21
+ import { toJSONSchema, toPartialJSONSchema } from "assistant-stream";
22
+ import { ModelContext } from "../../store";
23
+ import { buildInteractableModelContext } from "./interactable-model-context";
24
+
25
+ const PERSISTENCE_DEBOUNCE_MS = 500;
26
+
27
+ export const Interactables = resource((): ClientOutput<"interactables"> => {
28
+ const [state, setState] = tapState<InteractablesState>(() => ({
29
+ definitions: {},
30
+ persistence: {},
31
+ }));
32
+
33
+ const clientRef = tapAssistantClientRef();
34
+
35
+ const stateRef = tapRef(state);
36
+ tapEffect(() => {
37
+ stateRef.current = state;
38
+ }, [state]);
39
+
40
+ const subscribersRef = tapRef(new Set<() => void>());
41
+ const partialSchemaCacheRef = tapRef(
42
+ new Map<string, InteractableStateSchema>(),
43
+ );
44
+ const detachedStateRef = tapRef(new Map<string, unknown>());
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
+
202
+ const setDefState = tapCallback(
203
+ (id: string, updater: (prev: unknown) => unknown) => {
204
+ setState((prev) => {
205
+ const existing = prev.definitions[id];
206
+ if (!existing) return prev;
207
+ return {
208
+ ...prev,
209
+ definitions: {
210
+ ...prev.definitions,
211
+ [id]: { ...existing, state: updater(existing.state) },
212
+ },
213
+ };
214
+ });
215
+ if (stateRef.current.definitions[id]) schedulePersistence(id);
216
+ },
217
+ [schedulePersistence],
218
+ );
219
+
220
+ const setDefSelected = tapCallback((id: string, selected: boolean) => {
221
+ setState((prev) => {
222
+ const existing = prev.definitions[id];
223
+ if (!existing) return prev;
224
+ return {
225
+ ...prev,
226
+ definitions: {
227
+ ...prev.definitions,
228
+ [id]: { ...existing, selected },
229
+ },
230
+ };
231
+ });
232
+ }, []);
233
+
234
+ const provider = tapMemo(
235
+ () => ({
236
+ getModelContext: () => {
237
+ const defs = stateRef.current.definitions;
238
+ return (
239
+ buildInteractableModelContext(
240
+ defs,
241
+ partialSchemaCacheRef.current,
242
+ setDefState,
243
+ ) ?? {}
244
+ );
245
+ },
246
+ subscribe: (callback: () => void) => {
247
+ subscribersRef.current.add(callback);
248
+ return () => {
249
+ subscribersRef.current.delete(callback);
250
+ };
251
+ },
252
+ }),
253
+ [setDefState],
254
+ );
255
+
256
+ // biome-ignore lint/correctness/useExhaustiveDependencies: state dep triggers notification
257
+ tapEffect(() => {
258
+ for (const cb of subscribersRef.current) cb();
259
+ }, [state]);
260
+
261
+ tapEffect(() => {
262
+ return clientRef.current!.modelContext().register(provider);
263
+ }, [clientRef, provider]);
264
+
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
+ }
279
+
280
+ const detached = detachedStateRef.current.get(def.id);
281
+ detachedStateRef.current.delete(def.id);
282
+
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
+ },
296
+ },
297
+ }));
298
+
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
+ );
315
+
316
+ return {
317
+ getState: () => state,
318
+ register,
319
+ setState: setDefState,
320
+ setSelected: setDefSelected,
321
+ exportState,
322
+ importState,
323
+ setPersistenceAdapter,
324
+ flush,
325
+ };
326
+ });
327
+
328
+ attachTransformScopes(Interactables, (scopes, parent) => {
329
+ if (!scopes.modelContext && parent.modelContext.source === null) {
330
+ scopes.modelContext = ModelContext();
331
+ }
332
+ });
@@ -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,
@@ -28,10 +32,20 @@ export {
28
32
  } from "./model-context/useAssistantDataUI";
29
33
  export { useInlineRender } from "./model-context/useInlineRender";
30
34
  export type { Toolkit, ToolDefinition } from "./model-context/toolbox";
35
+ export {
36
+ useAssistantInteractable,
37
+ type AssistantInteractableProps,
38
+ } from "./model-context/useAssistantInteractable";
39
+ export { useInteractableState } from "./model-context/useInteractableState";
40
+ export {
41
+ useToolArgsStatus,
42
+ type ToolArgsStatus,
43
+ } from "./model-context/useToolArgsStatus";
31
44
 
32
45
  // client
33
46
  export { Tools } from "./client/Tools";
34
47
  export { DataRenderers } from "./client/DataRenderers";
48
+ export { Interactables } from "./client/Interactables";
35
49
 
36
50
  // types
37
51
  export type {
@@ -68,6 +82,17 @@ export type {
68
82
  DataRenderersMethods,
69
83
  DataRenderersClientSchema,
70
84
  } from "./types/scopes/dataRenderers";
85
+ export type {
86
+ InteractableStateSchema,
87
+ InteractablesState,
88
+ InteractableDefinition,
89
+ InteractableRegistration,
90
+ InteractablesMethods,
91
+ InteractablePersistedState,
92
+ InteractablePersistenceAdapter,
93
+ InteractablePersistenceStatus,
94
+ InteractablesClientSchema,
95
+ } from "./types/scopes/interactables";
71
96
 
72
97
  // providers
73
98
  export {
@@ -192,6 +217,11 @@ export {
192
217
  } from "./primitive-hooks/useActionBarFeedback";
193
218
  export { useActionBarSpeak } from "./primitive-hooks/useActionBarSpeak";
194
219
  export { useActionBarStopSpeaking } from "./primitive-hooks/useActionBarStopSpeaking";
220
+ export {
221
+ useVoiceState,
222
+ useVoiceVolume,
223
+ useVoiceControls,
224
+ } from "./primitive-hooks/useVoice";
195
225
  export { useBranchPickerNext } from "./primitive-hooks/useBranchPickerNext";
196
226
  export { useBranchPickerPrevious } from "./primitive-hooks/useBranchPickerPrevious";
197
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
+ };