@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
@@ -1,7 +1,12 @@
1
- import type { AppendMessage, ThreadMessage } from "../../types/message";
1
+ import type {
2
+ AppendMessage,
3
+ ThreadAssistantMessage,
4
+ ThreadMessage,
5
+ } from "../../types/message";
2
6
  import type { Unsubscribe } from "../../types/unsubscribe";
3
7
  import type { ModelContextProvider } from "../../model-context/types";
4
8
  import { getThreadMessageText } from "../../utils/text";
9
+ import { generateId } from "../../utils/id";
5
10
  import {
6
11
  ExportedMessageRepository,
7
12
  MessageRepository,
@@ -14,6 +19,7 @@ import type {
14
19
  SubmitFeedbackOptions,
15
20
  ThreadRuntimeCore,
16
21
  SpeechState,
22
+ VoiceSessionState,
17
23
  RuntimeCapabilities,
18
24
  ThreadRuntimeEventType,
19
25
  StartRunConfig,
@@ -23,12 +29,14 @@ import { DefaultEditComposerRuntimeCore } from "./default-edit-composer-runtime-
23
29
  import type { SpeechSynthesisAdapter } from "../../adapters/speech";
24
30
  import type { FeedbackAdapter } from "../../adapters/feedback";
25
31
  import type { AttachmentAdapter } from "../../adapters/attachment";
32
+ import type { RealtimeVoiceAdapter } from "../../adapters/voice";
26
33
  import type { ThreadMessageLike } from "../utils/thread-message-like";
27
34
 
28
35
  type BaseThreadAdapters = {
29
36
  speech?: SpeechSynthesisAdapter | undefined;
30
37
  feedback?: FeedbackAdapter | undefined;
31
38
  attachments?: AttachmentAdapter | undefined;
39
+ voice?: RealtimeVoiceAdapter | undefined;
32
40
  };
33
41
 
34
42
  export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
@@ -53,10 +61,37 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
53
61
  public abstract importExternalState(state: any): void;
54
62
  public abstract unstable_loadExternalState(state: any): void;
55
63
 
56
- public get messages() {
64
+ protected _voiceMessages: ThreadMessage[] = [];
65
+ protected _voiceGeneration = 0;
66
+ private _cachedMergedMessages: readonly ThreadMessage[] | null = null;
67
+ private _cachedVoiceGeneration = -1;
68
+ private _cachedMergedBase: readonly ThreadMessage[] | null = null;
69
+
70
+ protected _markVoiceMessagesDirty() {
71
+ this._voiceGeneration++;
72
+ this._cachedMergedMessages = null;
73
+ }
74
+
75
+ protected _getBaseMessages(): readonly ThreadMessage[] {
57
76
  return this.repository.getMessages();
58
77
  }
59
78
 
79
+ public get messages(): readonly ThreadMessage[] {
80
+ if (this._voiceMessages.length === 0) {
81
+ return this._getBaseMessages();
82
+ }
83
+ const base = this._getBaseMessages();
84
+ if (
85
+ this._cachedVoiceGeneration !== this._voiceGeneration ||
86
+ this._cachedMergedBase !== base
87
+ ) {
88
+ this._cachedMergedMessages = [...base, ...this._voiceMessages];
89
+ this._cachedVoiceGeneration = this._voiceGeneration;
90
+ this._cachedMergedBase = base;
91
+ }
92
+ return this._cachedMergedMessages!;
93
+ }
94
+
60
95
  public get state() {
61
96
  let mostRecentAssistantMessage;
62
97
  for (const message of this.messages) {
@@ -99,11 +134,28 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
99
134
  try {
100
135
  return this.repository.getMessage(messageId);
101
136
  } catch {
137
+ // Check voice messages
138
+ const baseMessages = this.repository.getMessages();
139
+ const voiceIdx = this._voiceMessages.findIndex((m) => m.id === messageId);
140
+ if (voiceIdx !== -1) {
141
+ const parentId =
142
+ voiceIdx > 0
143
+ ? this._voiceMessages[voiceIdx - 1]!.id
144
+ : (baseMessages.at(-1)?.id ?? null);
145
+ return {
146
+ parentId,
147
+ message: this._voiceMessages[voiceIdx]!,
148
+ index: baseMessages.length + voiceIdx,
149
+ };
150
+ }
102
151
  return undefined;
103
152
  }
104
153
  }
105
154
 
106
155
  public getBranches(messageId: string): string[] {
156
+ if (this._voiceMessages.some((m) => m.id === messageId)) {
157
+ return [];
158
+ }
107
159
  return this.repository.getBranches(messageId);
108
160
  }
109
161
 
@@ -188,6 +240,195 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
188
240
  this._notifySubscribers();
189
241
  }
190
242
 
243
+ private _voiceSession: RealtimeVoiceAdapter.Session | undefined;
244
+ private _voiceUnsubs: Array<() => void> = [];
245
+ public voice: VoiceSessionState | undefined;
246
+
247
+ private _voiceVolume = 0;
248
+ private _voiceVolumeSubscribers = new Set<() => void>();
249
+
250
+ public getVoiceVolume = () => this._voiceVolume;
251
+
252
+ public subscribeVoiceVolume = (callback: () => void): Unsubscribe => {
253
+ this._voiceVolumeSubscribers.add(callback);
254
+ return () => this._voiceVolumeSubscribers.delete(callback);
255
+ };
256
+
257
+ public connectVoice() {
258
+ const adapter = this.adapters?.voice;
259
+ if (!adapter) throw new Error("Voice adapter not configured");
260
+
261
+ this.disconnectVoice();
262
+
263
+ const session = adapter.connect({});
264
+ this._voiceSession = session;
265
+ const unsubs: Array<() => void> = [];
266
+
267
+ let currentMode: RealtimeVoiceAdapter.Mode = "listening";
268
+
269
+ this.voice = {
270
+ status: session.status,
271
+ isMuted: session.isMuted,
272
+ mode: currentMode,
273
+ };
274
+ this._voiceVolume = 0;
275
+ this._notifySubscribers();
276
+
277
+ unsubs.push(
278
+ session.onStatusChange((status) => {
279
+ if (status.type === "ended") {
280
+ this._finishVoiceAssistantMessage();
281
+ this._voiceSession = undefined;
282
+ this.voice = undefined;
283
+ } else {
284
+ this.voice = {
285
+ status,
286
+ isMuted: session.isMuted,
287
+ mode: currentMode,
288
+ };
289
+ }
290
+ this._notifySubscribers();
291
+ }),
292
+ );
293
+
294
+ unsubs.push(
295
+ session.onModeChange((mode) => {
296
+ currentMode = mode;
297
+ if (this.voice) {
298
+ this.voice = { ...this.voice, mode };
299
+ this._notifySubscribers();
300
+ }
301
+ }),
302
+ );
303
+
304
+ unsubs.push(
305
+ session.onVolumeChange((volume) => {
306
+ this._voiceVolume = volume;
307
+ for (const cb of this._voiceVolumeSubscribers) cb();
308
+ }),
309
+ );
310
+
311
+ unsubs.push(
312
+ session.onTranscript((transcript) => {
313
+ this._handleVoiceTranscript(transcript);
314
+ }),
315
+ );
316
+
317
+ this._voiceUnsubs = unsubs;
318
+ }
319
+
320
+ private _currentAssistantMsg: ThreadAssistantMessage | null = null;
321
+
322
+ private _handleVoiceTranscript(
323
+ transcript: RealtimeVoiceAdapter.TranscriptItem,
324
+ ) {
325
+ this.ensureInitialized();
326
+
327
+ if (transcript.role === "user") {
328
+ this._finishVoiceAssistantMessage();
329
+ this._currentAssistantMsg = null;
330
+
331
+ if (transcript.isFinal) {
332
+ this._voiceMessages.push({
333
+ id: generateId(),
334
+ role: "user",
335
+ content: [{ type: "text", text: transcript.text }],
336
+ metadata: { custom: {} },
337
+ createdAt: new Date(),
338
+ status: { type: "complete", reason: "unknown" },
339
+ attachments: [],
340
+ });
341
+ this._markVoiceMessagesDirty();
342
+ this._notifySubscribers();
343
+ }
344
+ } else {
345
+ if (!this._currentAssistantMsg) {
346
+ this._currentAssistantMsg = {
347
+ id: generateId(),
348
+ role: "assistant",
349
+ content: [{ type: "text", text: transcript.text }],
350
+ metadata: {
351
+ unstable_state: this.state,
352
+ unstable_annotations: [],
353
+ unstable_data: [],
354
+ steps: [],
355
+ custom: {},
356
+ },
357
+ status: { type: "running" },
358
+ createdAt: new Date(),
359
+ };
360
+ this._voiceMessages.push(this._currentAssistantMsg);
361
+ } else {
362
+ const idx = this._voiceMessages.indexOf(this._currentAssistantMsg);
363
+ if (idx === -1) return;
364
+ const updated: ThreadAssistantMessage = {
365
+ ...this._currentAssistantMsg,
366
+ content: [{ type: "text", text: transcript.text }],
367
+ ...(transcript.isFinal
368
+ ? { status: { type: "complete", reason: "stop" } }
369
+ : {}),
370
+ };
371
+ this._voiceMessages[idx] = updated;
372
+ this._currentAssistantMsg = updated;
373
+ }
374
+
375
+ if (transcript.isFinal) {
376
+ this._currentAssistantMsg = null;
377
+ }
378
+
379
+ this._markVoiceMessagesDirty();
380
+ this._notifySubscribers();
381
+ }
382
+ }
383
+
384
+ private _finishVoiceAssistantMessage() {
385
+ const last = this._voiceMessages.at(-1);
386
+ if (last?.role === "assistant" && last.status.type === "running") {
387
+ const idx = this._voiceMessages.length - 1;
388
+ this._voiceMessages[idx] = {
389
+ ...(last as ThreadAssistantMessage),
390
+ status: { type: "complete", reason: "stop" },
391
+ };
392
+ this._markVoiceMessagesDirty();
393
+ this._notifySubscribers();
394
+ }
395
+ }
396
+
397
+ public disconnectVoice() {
398
+ this._finishVoiceAssistantMessage();
399
+ this._currentAssistantMsg = null;
400
+ for (const unsub of this._voiceUnsubs) unsub();
401
+ this._voiceUnsubs = [];
402
+ this._voiceSession?.disconnect();
403
+ this._voiceSession = undefined;
404
+ this.voice = undefined;
405
+ this._voiceVolume = 0;
406
+ for (const cb of this._voiceVolumeSubscribers) cb();
407
+ this._voiceMessages = [];
408
+ this._markVoiceMessagesDirty();
409
+ this._notifySubscribers();
410
+ }
411
+
412
+ public muteVoice() {
413
+ if (!this._voiceSession) throw new Error("No active voice session");
414
+ this._voiceSession.mute();
415
+ this.voice = {
416
+ ...this.voice!,
417
+ isMuted: true,
418
+ };
419
+ this._notifySubscribers();
420
+ }
421
+
422
+ public unmuteVoice() {
423
+ if (!this._voiceSession) throw new Error("No active voice session");
424
+ this._voiceSession.unmute();
425
+ this.voice = {
426
+ ...this.voice!,
427
+ isMuted: false,
428
+ };
429
+ this._notifySubscribers();
430
+ }
431
+
191
432
  protected ensureInitialized() {
192
433
  if (!this._isInitialized) {
193
434
  this._isInitialized = true;
@@ -5,7 +5,10 @@ import type { Unsubscribe } from "../../types/unsubscribe";
5
5
  import type { RunConfig } from "../../types/message";
6
6
  import type { DictationAdapter } from "../../adapters/speech";
7
7
 
8
- export type ComposerRuntimeEventType = "send" | "attachmentAdd";
8
+ export type ComposerRuntimeEventType =
9
+ | "send"
10
+ | "attachmentAdd"
11
+ | "attachmentAddError";
9
12
 
10
13
  export type DictationState = {
11
14
  readonly status: DictationAdapter.Status;
@@ -4,6 +4,7 @@ import type { Unsubscribe } from "../../types/unsubscribe";
4
4
  import type { AppendMessage, ThreadMessage } from "../../types/message";
5
5
  import type { RunConfig } from "../../types/message";
6
6
  import type { SpeechSynthesisAdapter } from "../../adapters/speech";
7
+ import type { RealtimeVoiceAdapter } from "../../adapters/voice";
7
8
  import type {
8
9
  ChatModelRunOptions,
9
10
  ChatModelRunResult,
@@ -24,6 +25,7 @@ export type RuntimeCapabilities = {
24
25
  readonly unstable_copy: boolean;
25
26
  readonly speech: boolean;
26
27
  readonly dictation: boolean;
28
+ readonly voice: boolean;
27
29
  readonly attachments: boolean;
28
30
  readonly feedback: boolean;
29
31
  readonly queue: boolean;
@@ -57,6 +59,12 @@ export type SpeechState = {
57
59
  readonly status: SpeechSynthesisAdapter.Status;
58
60
  };
59
61
 
62
+ export type VoiceSessionState = {
63
+ readonly status: RealtimeVoiceAdapter.Status;
64
+ readonly isMuted: boolean;
65
+ readonly mode: RealtimeVoiceAdapter.Mode;
66
+ };
67
+
60
68
  export type SubmittedFeedback = {
61
69
  readonly type: "negative" | "positive";
62
70
  };
@@ -102,6 +110,11 @@ export type ThreadRuntimeCore = Readonly<{
102
110
  speak: (messageId: string) => void;
103
111
  stopSpeaking: () => void;
104
112
 
113
+ connectVoice: () => void;
114
+ disconnectVoice: () => void;
115
+ muteVoice: () => void;
116
+ unmuteVoice: () => void;
117
+
105
118
  submitFeedback: (feedback: SubmitFeedbackOptions) => void;
106
119
 
107
120
  getModelContext: () => ModelContext;
@@ -111,6 +124,7 @@ export type ThreadRuntimeCore = Readonly<{
111
124
  beginEdit: (messageId: string) => void;
112
125
 
113
126
  speech: SpeechState | undefined;
127
+ voice: VoiceSessionState | undefined;
114
128
 
115
129
  capabilities: Readonly<RuntimeCapabilities>;
116
130
  isDisabled: boolean;
@@ -123,6 +137,9 @@ export type ThreadRuntimeCore = Readonly<{
123
137
 
124
138
  subscribe: (callback: () => void) => Unsubscribe;
125
139
 
140
+ getVoiceVolume: () => number;
141
+ subscribeVoiceVolume: (callback: () => void) => Unsubscribe;
142
+
126
143
  import(repository: ExportedMessageRepository): void;
127
144
  export(): ExportedMessageRepository;
128
145
 
@@ -5,6 +5,7 @@ import type {
5
5
  SpeechSynthesisAdapter,
6
6
  DictationAdapter,
7
7
  } from "../../adapters/speech";
8
+ import type { RealtimeVoiceAdapter } from "../../adapters/voice";
8
9
  import type { FeedbackAdapter } from "../../adapters/feedback";
9
10
  import type {
10
11
  AddToolResultOptions,
@@ -90,6 +91,7 @@ type ExternalStoreAdapterBase<T> = {
90
91
  attachments?: AttachmentAdapter | undefined;
91
92
  speech?: SpeechSynthesisAdapter | undefined;
92
93
  dictation?: DictationAdapter | undefined;
94
+ voice?: RealtimeVoiceAdapter | undefined;
93
95
  feedback?: FeedbackAdapter | undefined;
94
96
  /**
95
97
  * @deprecated This API is still under active development and might change without notice.
@@ -63,6 +63,7 @@ export class ExternalStoreThreadRuntimeCore
63
63
  unstable_copy: false,
64
64
  speech: false,
65
65
  dictation: false,
66
+ voice: false,
66
67
  attachments: false,
67
68
  feedback: false,
68
69
  queue: false,
@@ -78,7 +79,7 @@ export class ExternalStoreThreadRuntimeCore
78
79
  return this._store.isLoading ?? false;
79
80
  }
80
81
 
81
- public override get messages() {
82
+ protected override _getBaseMessages(): readonly ThreadMessage[] {
82
83
  return this._messages;
83
84
  }
84
85
 
@@ -137,6 +138,7 @@ export class ExternalStoreThreadRuntimeCore
137
138
  cancel: this._store.onCancel !== undefined,
138
139
  speech: this._store.adapters?.speech !== undefined,
139
140
  dictation: this._store.adapters?.dictation !== undefined,
141
+ voice: this._store.adapters?.voice !== undefined,
140
142
  unstable_copy: this._store.unstable_capabilities?.copy !== false,
141
143
  attachments: !!this._store.adapters?.attachments,
142
144
  feedback: !!this._store.adapters?.feedback,
@@ -5,6 +5,7 @@ import type {
5
5
  SpeechSynthesisAdapter,
6
6
  DictationAdapter,
7
7
  } from "../../adapters/speech";
8
+ import type { RealtimeVoiceAdapter } from "../../adapters/voice";
8
9
  import type { SuggestionAdapter } from "../../adapters/suggestion";
9
10
  import type { ChatModelAdapter } from "../../runtime/utils/chat-model-adapter";
10
11
 
@@ -16,6 +17,7 @@ export type LocalRuntimeOptionsBase = {
16
17
  attachments?: AttachmentAdapter | undefined;
17
18
  speech?: SpeechSynthesisAdapter | undefined;
18
19
  dictation?: DictationAdapter | undefined;
20
+ voice?: RealtimeVoiceAdapter | undefined;
19
21
  feedback?: FeedbackAdapter | undefined;
20
22
  suggestion?: SuggestionAdapter | undefined;
21
23
  };
@@ -45,6 +45,7 @@ export class LocalThreadRuntimeCore
45
45
  unstable_copy: true,
46
46
  speech: false,
47
47
  dictation: false,
48
+ voice: false,
48
49
  attachments: false,
49
50
  feedback: false,
50
51
  queue: false,
@@ -118,6 +119,12 @@ export class LocalThreadRuntimeCore
118
119
  hasUpdates = true;
119
120
  }
120
121
 
122
+ const canVoice = options.adapters?.voice !== undefined;
123
+ if (this.capabilities.voice !== canVoice) {
124
+ this.capabilities.voice = canVoice;
125
+ hasUpdates = true;
126
+ }
127
+
121
128
  const canAttach = options.adapters?.attachments !== undefined;
122
129
  if (this.capabilities.attachments !== canAttach) {
123
130
  this.capabilities.attachments = canAttach;
@@ -34,7 +34,6 @@ export class ReadonlyThreadRuntimeCore
34
34
  }
35
35
 
36
36
  getBranches(messageId: string) {
37
- // no branching in readonly threads
38
37
  const idx = this._messages.findIndex((m) => m.id === messageId);
39
38
  if (idx === -1) return [];
40
39
  return [messageId];
@@ -56,9 +55,7 @@ export class ReadonlyThreadRuntimeCore
56
55
  throw READONLY_THREAD_ERROR;
57
56
  }
58
57
 
59
- cancelRun(): void {
60
- // noop - nothing to cancel
61
- }
58
+ cancelRun(): void {}
62
59
 
63
60
  addToolResult(): void {
64
61
  throw READONLY_THREAD_ERROR;
@@ -72,8 +69,23 @@ export class ReadonlyThreadRuntimeCore
72
69
  throw READONLY_THREAD_ERROR;
73
70
  }
74
71
 
75
- stopSpeaking(): void {
76
- // noop
72
+ stopSpeaking(): void {}
73
+
74
+ connectVoice(): void {
75
+ throw READONLY_THREAD_ERROR;
76
+ }
77
+
78
+ disconnectVoice(): void {}
79
+
80
+ getVoiceVolume = () => 0;
81
+ subscribeVoiceVolume = (): Unsubscribe => () => {};
82
+
83
+ muteVoice(): void {
84
+ throw READONLY_THREAD_ERROR;
85
+ }
86
+
87
+ unmuteVoice(): void {
88
+ throw READONLY_THREAD_ERROR;
77
89
  }
78
90
 
79
91
  submitFeedback(): void {
@@ -179,6 +191,7 @@ export class ReadonlyThreadRuntimeCore
179
191
  }
180
192
 
181
193
  speech = undefined;
194
+ voice = undefined;
182
195
 
183
196
  capabilities = {
184
197
  switchToBranch: false,
@@ -189,6 +202,7 @@ export class ReadonlyThreadRuntimeCore
189
202
  unstable_copy: false,
190
203
  speech: false,
191
204
  dictation: false,
205
+ voice: false,
192
206
  attachments: false,
193
207
  feedback: false,
194
208
  queue: false,
@@ -48,6 +48,25 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
48
48
  throw EMPTY_THREAD_ERROR;
49
49
  },
50
50
 
51
+ connectVoice() {
52
+ throw EMPTY_THREAD_ERROR;
53
+ },
54
+
55
+ disconnectVoice() {
56
+ throw EMPTY_THREAD_ERROR;
57
+ },
58
+
59
+ getVoiceVolume: () => 0,
60
+ subscribeVoiceVolume: () => () => {},
61
+
62
+ muteVoice() {
63
+ throw EMPTY_THREAD_ERROR;
64
+ },
65
+
66
+ unmuteVoice() {
67
+ throw EMPTY_THREAD_ERROR;
68
+ },
69
+
51
70
  submitFeedback() {
52
71
  throw EMPTY_THREAD_ERROR;
53
72
  },
@@ -72,7 +91,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
72
91
  attachments: [],
73
92
  attachmentAccept: "*",
74
93
 
75
- async addAttachment(_fileOrAttachment) {
94
+ async addAttachment() {
76
95
  throw EMPTY_THREAD_ERROR;
77
96
  },
78
97
 
@@ -153,6 +172,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
153
172
  },
154
173
 
155
174
  speech: undefined,
175
+ voice: undefined,
156
176
 
157
177
  capabilities: {
158
178
  switchToBranch: false,
@@ -163,6 +183,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
163
183
  unstable_copy: false,
164
184
  speech: false,
165
185
  dictation: false,
186
+ voice: false,
166
187
  attachments: false,
167
188
  feedback: false,
168
189
  queue: false,
@@ -16,7 +16,6 @@ import {
16
16
  ComposerRuntime,
17
17
  EditComposerRuntime,
18
18
  } from "../../runtime/api/composer-runtime";
19
- import { ComposerRuntimeEventType } from "../../runtime/interfaces/composer-runtime-core";
20
19
  import { ComposerState } from "../scopes/composer";
21
20
  import { AttachmentRuntimeClient } from "./attachment-runtime-client";
22
21
  import { tapSubscribable } from "./tap-subscribable";
@@ -54,12 +53,7 @@ export const ComposerClient = resource(
54
53
  const unsubscribers: Unsubscribe[] = [];
55
54
 
56
55
  // Subscribe to composer events
57
- const composerEvents: ComposerRuntimeEventType[] = [
58
- "send",
59
- "attachmentAdd",
60
- ];
61
-
62
- for (const event of composerEvents) {
56
+ for (const event of ["send", "attachmentAdd"] as const) {
63
57
  const unsubscribe = runtime.unstable_on(event, () => {
64
58
  emit(`composer.${event}`, {
65
59
  threadId: threadIdRef.current,
@@ -69,6 +63,23 @@ export const ComposerClient = resource(
69
63
  unsubscribers.push(unsubscribe);
70
64
  }
71
65
 
66
+ // attachmentAddError carries the failed attachment ID
67
+ unsubscribers.push(
68
+ runtime.unstable_on("attachmentAddError", () => {
69
+ const errorAttachment = runtime
70
+ .getState()
71
+ .attachments.findLast(
72
+ (a) =>
73
+ a.status.type === "incomplete" && a.status.reason === "error",
74
+ );
75
+ emit("composer.attachmentAddError", {
76
+ threadId: threadIdRef.current,
77
+ ...(messageIdRef && { messageId: messageIdRef.current }),
78
+ ...(errorAttachment && { attachmentId: errorAttachment.id }),
79
+ });
80
+ }),
81
+ );
82
+
72
83
  return () => {
73
84
  for (const unsub of unsubscribers) unsub();
74
85
  };
@@ -44,11 +44,9 @@ export const ThreadClient = resource(
44
44
  const runtimeState = tapSubscribable(runtime);
45
45
  const emit = tapAssistantEmit();
46
46
 
47
- // Bind thread events to event manager
48
47
  tapEffect(() => {
49
48
  const unsubscribers: Unsubscribe[] = [];
50
49
 
51
- // Subscribe to thread events
52
50
  const threadEvents: ThreadRuntimeEventType[] = [
53
51
  "runStart",
54
52
  "runEnd",
@@ -105,6 +103,7 @@ export const ThreadClient = resource(
105
103
  suggestions: runtimeState.suggestions,
106
104
  extras: runtimeState.extras,
107
105
  speech: runtimeState.speech,
106
+ voice: runtimeState.voice,
108
107
 
109
108
  composer: composer.state,
110
109
  messages: messages.state,
@@ -124,12 +123,12 @@ export const ThreadClient = resource(
124
123
  import: runtime.import,
125
124
  reset: runtime.reset,
126
125
  stopSpeaking: runtime.stopSpeaking,
127
- startVoice: async () => {
128
- throw new Error("startVoice is not supported in this runtime");
129
- },
130
- stopVoice: async () => {
131
- throw new Error("stopVoice is not supported in this runtime");
132
- },
126
+ connectVoice: runtime.connectVoice,
127
+ disconnectVoice: runtime.disconnectVoice,
128
+ getVoiceVolume: runtime.getVoiceVolume,
129
+ subscribeVoiceVolume: runtime.subscribeVoiceVolume,
130
+ muteVoice: runtime.muteVoice,
131
+ unmuteVoice: runtime.unmuteVoice,
133
132
  message: (selector) => {
134
133
  if ("id" in selector) {
135
134
  return messages.get({ key: selector.id });
@@ -90,6 +90,11 @@ export type ComposerMeta = {
90
90
  export type ComposerEvents = {
91
91
  "composer.send": { threadId: string; messageId?: string };
92
92
  "composer.attachmentAdd": { threadId: string; messageId?: string };
93
+ "composer.attachmentAddError": {
94
+ threadId: string;
95
+ messageId?: string;
96
+ attachmentId?: string;
97
+ };
93
98
  };
94
99
 
95
100
  export type ComposerClientSchema = {