@alexkroman1/aai-ui 3.2.0 → 5.0.0

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 (38) hide show
  1. package/dist/audio.d.ts +0 -33
  2. package/dist/audio.js +2 -20
  3. package/dist/{chat-view-C1oJxsWz.js → chat-view-CW-tZCMm.js} +1 -4
  4. package/dist/components/chat-view.js +1 -1
  5. package/dist/components/console-shell.d.ts +0 -9
  6. package/dist/components/markdown.d.ts +21 -0
  7. package/dist/components/message-list.d.ts +0 -8
  8. package/dist/components/message-list.js +3 -230
  9. package/dist/components/url-chips.d.ts +0 -15
  10. package/dist/context.d.ts +1 -1
  11. package/dist/default-client/assets/audio-Bu1LSuiR.js +1 -0
  12. package/dist/default-client/assets/{capture-processor-DYl6YIu5.js → capture-processor-ssRD81Mo.js} +1 -1
  13. package/dist/default-client/assets/index-0-DXv-S4.js +192 -0
  14. package/dist/default-client/assets/index-CRfdZPH1.css +2 -0
  15. package/dist/default-client/assets/{playback-processor-CeEGzgVT.js → playback-processor-CRwbxC1v.js} +1 -1
  16. package/dist/default-client/index.html +2 -2
  17. package/dist/define-client.d.ts +13 -2
  18. package/dist/define-client.js +120 -4
  19. package/dist/hooks.d.ts +40 -2
  20. package/dist/hooks.js +24 -1
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +7 -8
  23. package/dist/message-list-q2lTiFhF.js +381 -0
  24. package/dist/{session-core-DgioOYyx.js → session-core-Bjkr4PAP.js} +112 -57
  25. package/dist/session-core-audio-setup.d.ts +3 -6
  26. package/dist/session-core-messages.d.ts +2 -1
  27. package/dist/session-core-reconnect.d.ts +4 -3
  28. package/dist/session-core-types.d.ts +11 -12
  29. package/dist/session-core-url.d.ts +5 -0
  30. package/dist/session-core.d.ts +0 -1
  31. package/dist/session-core.js +1 -1
  32. package/dist/types.d.ts +18 -2
  33. package/dist/types.js +2 -2
  34. package/package.json +4 -2
  35. package/dist/default-client/assets/audio-CIZMTfDQ.js +0 -1
  36. package/dist/default-client/assets/index-BeVXjGwG.js +0 -344
  37. package/dist/default-client/assets/index-Dv-Q5VRL.css +0 -2
  38. package/dist/define-client-fjNCp9be.js +0 -150
@@ -1,7 +1,36 @@
1
1
  import { MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
2
- import { DEFAULT_MAX_HISTORY, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
3
- import { ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
2
+ import { CLIENT_CONFIG_PATH, ClientConfigResponseSchema, ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
3
+ import { DEFAULT_MAX_HISTORY, WS_OPEN, createEpoch, errorMessage, safeJsonParse } from "@alexkroman1/aai";
4
4
  import ReconnectingWebSocket from "partysocket/ws";
5
+ //#region client-config.ts
6
+ /**
7
+ * Pre-connection client-config lookup.
8
+ *
9
+ * `GET client-config` (relative to the agent's base URL — see
10
+ * `sdk/client-config.ts` in `@alexkroman1/aai`) gives the default client the
11
+ * agent's display name and greeting before any connection exists. Every
12
+ * failure path — network error, 404 from an older server, malformed body —
13
+ * degrades to the empty default, so this lookup can never break an existing
14
+ * agent.
15
+ */
16
+ /** Resolve a relative endpoint path against the agent's base URL. */
17
+ function buildAgentUrl(platformUrl, endpointPath) {
18
+ return new URL(endpointPath, platformUrl.endsWith("/") ? platformUrl : `${platformUrl}/`);
19
+ }
20
+ const AGENT_DEFAULT = {};
21
+ /** Fetch the agent's client config; any failure yields the agent default. */
22
+ async function fetchClientConfig(platformUrl, fetchFn) {
23
+ const doFetch = fetchFn ?? ((input, init) => globalThis.fetch(input, init));
24
+ try {
25
+ const resp = await doFetch(buildAgentUrl(platformUrl, CLIENT_CONFIG_PATH).href);
26
+ if (!resp.ok) return AGENT_DEFAULT;
27
+ const parsed = ClientConfigResponseSchema.safeParse(await resp.json());
28
+ return parsed.success ? parsed.data : AGENT_DEFAULT;
29
+ } catch {
30
+ return AGENT_DEFAULT;
31
+ }
32
+ }
33
+ //#endregion
5
34
  //#region session-core-audio-setup.ts
6
35
  /**
7
36
  * Audio-path initialization for the voice session core.
@@ -23,20 +52,17 @@ import ReconnectingWebSocket from "partysocket/ws";
23
52
  * VoiceIO is closed immediately to prevent it from being assigned to a newer
24
53
  * connection.
25
54
  *
26
- * `fatal` selects the failure mode: voice sessions (`config` handshake) can't
27
- * function without the mic, so failure there sets the error state and ends
28
- * the session; a text-only session's opt-in record button must not brick an
29
- * otherwise healthy session, so failure there is a banner (`error` set,
30
- * state kept) and the session stays interactive.
55
+ * A failure is fatal: a voice session can't function without the mic, so it
56
+ * sets the error state and ends the session.
31
57
  */
32
- async function initAudioCapture(conn, msg, deps, fatal) {
58
+ async function initAudioCapture(conn, msg, deps) {
33
59
  if (conn.audioSetupInFlight) return;
34
60
  conn.audioSetupInFlight = true;
35
- const gen = conn.generation;
36
- const stale = () => conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN;
61
+ const gen = conn.generation.current();
62
+ const stale = () => !(conn.generation.isCurrent(gen) && conn.ws) || conn.ws.readyState !== WS_OPEN;
37
63
  const reportAudioFailure = (message) => {
38
64
  deps.cleanupAudio();
39
- if (fatal) deps.updateState({
65
+ deps.updateState({
40
66
  state: "error",
41
67
  error: {
42
68
  code: "audio",
@@ -45,13 +71,6 @@ async function initAudioCapture(conn, msg, deps, fatal) {
45
71
  running: false,
46
72
  recording: false
47
73
  });
48
- else deps.updateState({
49
- error: {
50
- code: "audio",
51
- message
52
- },
53
- recording: false
54
- });
55
74
  };
56
75
  try {
57
76
  const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
@@ -78,7 +97,7 @@ async function initAudioCapture(conn, msg, deps, fatal) {
78
97
  console.warn("[aai-ui] microphone is delivering only silence — check the selected input device");
79
98
  },
80
99
  onError: (err) => {
81
- if (conn.generation !== gen) return;
100
+ if (!conn.generation.isCurrent(gen)) return;
82
101
  reportAudioFailure(err.message);
83
102
  }
84
103
  });
@@ -102,7 +121,7 @@ async function initAudioCapture(conn, msg, deps, fatal) {
102
121
  if (stale()) return;
103
122
  reportAudioFailure(`Microphone access failed: ${errorMessage(err)}`);
104
123
  } finally {
105
- if (conn.generation === gen) conn.audioSetupInFlight = false;
124
+ if (conn.generation.isCurrent(gen)) conn.audioSetupInFlight = false;
106
125
  }
107
126
  }
108
127
  //#endregion
@@ -134,6 +153,7 @@ const CLEARED_SESSION_STATE = {
134
153
  messages: [],
135
154
  toolCalls: [],
136
155
  customEvents: [],
156
+ agentState: null,
137
157
  userTranscript: null,
138
158
  agentTranscript: null,
139
159
  error: null
@@ -147,14 +167,14 @@ function appendCapped(list, item, cap) {
147
167
  /**
148
168
  * Create the server→client message handlers for one session core.
149
169
  *
150
- * Encapsulates the two turn-boundary counters (`handlerGeneration` for
170
+ * Encapsulates the two turn-boundary counters (`turnEpoch` for
151
171
  * discarding stale async audio completions, `customEventSeq` for event
152
172
  * dedup) that previously lived as closure locals in `createSessionCore`.
153
173
  */
154
174
  function createMessageHandlers(deps) {
155
175
  const { getSnapshot, updateState, conn, cleanupAudio } = deps;
156
- /** Incremented on each turn boundary -- stale async callbacks compare against this. */
157
- let handlerGeneration = 0;
176
+ /** Bumped on each turn boundary -- stale async callbacks check against this. */
177
+ const turnEpoch = createEpoch();
158
178
  /** Monotonically increasing counter for custom events -- used by useEvent to deduplicate. */
159
179
  let customEventSeq = 0;
160
180
  /** Monotonically increasing counter for chat messages -- stable render keys
@@ -171,7 +191,7 @@ function createMessageHandlers(deps) {
171
191
  }, MAX_CUSTOM_EVENTS) });
172
192
  }
173
193
  function handleUserTranscriptEvent(text) {
174
- handlerGeneration++;
194
+ turnEpoch.bump();
175
195
  updateState({
176
196
  userTranscript: null,
177
197
  messages: appendCapped(getSnapshot().messages, {
@@ -231,7 +251,7 @@ function createMessageHandlers(deps) {
231
251
  } });
232
252
  else {
233
253
  cleanupAudio();
234
- conn.generation++;
254
+ conn.generation.bump();
235
255
  updateState({
236
256
  state: "error",
237
257
  error: {
@@ -290,7 +310,7 @@ function createMessageHandlers(deps) {
290
310
  updateState({ state: "listening" });
291
311
  break;
292
312
  case "cancelled":
293
- handlerGeneration++;
313
+ turnEpoch.bump();
294
314
  conn.voiceIO?.flush();
295
315
  commitAgentTranscript();
296
316
  updateState({
@@ -299,7 +319,7 @@ function createMessageHandlers(deps) {
299
319
  });
300
320
  break;
301
321
  case "reset":
302
- handlerGeneration++;
322
+ turnEpoch.bump();
303
323
  conn.voiceIO?.flush();
304
324
  updateState({
305
325
  ...CLEARED_SESSION_STATE,
@@ -309,10 +329,12 @@ function createMessageHandlers(deps) {
309
329
  case "custom_event":
310
330
  appendCustomEvent(e.event, e.data);
311
331
  break;
332
+ case "agent_state":
333
+ updateState({ agentState: e.state });
334
+ break;
312
335
  case "error":
313
336
  handleErrorEvent(e);
314
337
  break;
315
- case "idle_timeout": break;
316
338
  default: break;
317
339
  }
318
340
  }
@@ -325,12 +347,12 @@ function createMessageHandlers(deps) {
325
347
  else if (conn.preInitAudio.length < MAX_PREINIT_AUDIO_CHUNKS) conn.preInitAudio.push(chunk);
326
348
  }
327
349
  /** See {@link MessageHandlers.settleWhenAudioDrained}. Captures
328
- * `handlerGeneration` so a completion (or failure) that lands after a turn
350
+ * `turnEpoch` so a completion (or failure) that lands after a turn
329
351
  * boundary is discarded instead of overwriting the newer turn's state. */
330
352
  function settleWhenAudioDrained(io) {
331
- const gen = handlerGeneration;
353
+ const gen = turnEpoch.current();
332
354
  io.done().then(() => {
333
- if (handlerGeneration !== gen) return;
355
+ if (!turnEpoch.isCurrent(gen)) return;
334
356
  updateState({ state: "listening" });
335
357
  }).catch((err) => {
336
358
  console.warn("Audio playback done failed:", err);
@@ -339,7 +361,7 @@ function createMessageHandlers(deps) {
339
361
  /**
340
362
  * Signal that the server has finished sending audio for this turn.
341
363
  * Waits for the audio queue to drain, then transitions state to `"listening"`.
342
- * Uses the `handlerGeneration` counter to discard stale completions from interrupted turns.
364
+ * Uses the `turnEpoch` epoch to discard stale completions from interrupted turns.
343
365
  */
344
366
  function playAudioDone() {
345
367
  const io = conn.voiceIO;
@@ -406,8 +428,9 @@ const RECONNECT_OPTIONS = {
406
428
  };
407
429
  /**
408
430
  * Open partysocket's reconnecting WebSocket. The URL is a *provider*,
409
- * re-evaluated on every attempt, so each retry picks up the current resume
410
- * URL rather than the one the session started with.
431
+ * re-evaluated on every attempt (async supported), so each retry picks up
432
+ * the current broker-named endpoint and resume URL rather than the ones the
433
+ * session started with.
411
434
  */
412
435
  function openReconnectingSocket(urlProvider) {
413
436
  return new ReconnectingWebSocket(urlProvider, void 0, RECONNECT_OPTIONS);
@@ -425,8 +448,21 @@ function reconnectPending(socket) {
425
448
  //#region session-core-url.ts
426
449
  /** Build the session WebSocket URL from the platform URL and resume state. */
427
450
  function buildWsUrl(platformUrl, resume, sessionId) {
428
- const wsUrl = new URL("websocket", platformUrl.endsWith("/") ? platformUrl : `${platformUrl}/`);
429
- wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
451
+ return applyResumeParams(new URL("websocket", platformUrl.endsWith("/") ? platformUrl : `${platformUrl}/`), resume, sessionId);
452
+ }
453
+ /**
454
+ * Turn a broker-provided session URL (`sessionUrl` from `GET client-config`
455
+ * — the agent's live sandbox endpoint) into this attempt's connect URL.
456
+ */
457
+ function buildBrokeredWsUrl(sessionUrl, resume, sessionId) {
458
+ return applyResumeParams(new URL(sessionUrl), resume, sessionId);
459
+ }
460
+ const WS_PROTOCOLS = {
461
+ "https:": "wss:",
462
+ "http:": "ws:"
463
+ };
464
+ function applyResumeParams(wsUrl, resume, sessionId) {
465
+ wsUrl.protocol = WS_PROTOCOLS[wsUrl.protocol] ?? wsUrl.protocol;
430
466
  if (sessionId) wsUrl.searchParams.set("sessionId", sessionId);
431
467
  else if (resume) wsUrl.searchParams.set("resume", "1");
432
468
  return wsUrl;
@@ -502,10 +538,9 @@ function createSessionCore(options) {
502
538
  ws: null,
503
539
  voiceIO: null,
504
540
  audioSetupInFlight: false,
505
- generation: 0,
541
+ generation: createEpoch(),
506
542
  preInitAudio: [],
507
- preInitDone: false,
508
- readyConfig: null
543
+ preInitDone: false
509
544
  };
510
545
  let connectionController = null;
511
546
  let hasConnected = false;
@@ -518,6 +553,15 @@ function createSessionCore(options) {
518
553
  * agent's context, greeting suppression aside.
519
554
  */
520
555
  let sessionId = options.resumeSessionId;
556
+ /**
557
+ * Whether `platformUrl` is a broker (its `client-config` names a
558
+ * `sessionUrl`). A server is one or it isn't — it never flips mid-session
559
+ * — so once a non-broker is observed, later reconnects skip the
560
+ * `client-config` re-fetch that would only fall through to `buildWsUrl`
561
+ * (every reconnect on `aai dev` / self-hosted otherwise pays a wasted GET).
562
+ * `undefined` until the first fetch settles.
563
+ */
564
+ let serverIsBroker;
521
565
  function cleanupAudio() {
522
566
  conn.audioSetupInFlight = false;
523
567
  conn.voiceIO?.close().catch(() => {});
@@ -566,11 +610,7 @@ function createSessionCore(options) {
566
610
  }
567
611
  const isReconnect = hasConnected;
568
612
  hasConnected = true;
569
- conn.readyConfig = {
570
- sampleRate: config.sampleRate,
571
- ttsSampleRate: config.ttsSampleRate
572
- };
573
- initAudioCapture(conn, config, audioDeps, true);
613
+ initAudioCapture(conn, config, audioDeps);
574
614
  if (isReconnect && currentSnapshot.messages.length > 0) sendJson({
575
615
  type: "history",
576
616
  messages: currentSnapshot.messages.map((m) => ({
@@ -581,19 +621,34 @@ function createSessionCore(options) {
581
621
  }
582
622
  /**
583
623
  * The WebSocket URL for the *next* connection attempt. Evaluated per
584
- * attempt (partysocket takes it as a URL provider), so once the first
585
- * `config` arrives, every reconnect — automatic or explicit — carries
586
- * `?sessionId=<id>` and the server resumes the SAME session (id, tool
587
- * state) instead of minting a new one. `resume=1` remains only as the
588
- * greeting-suppression fallback for a server whose config carried no id.
624
+ * attempt (partysocket takes it as an async URL provider):
625
+ *
626
+ * - `GET client-config` is re-fetched every attempt. When it names a
627
+ * `sessionUrl` the platform's broker pointing at the agent's live
628
+ * sandbox the session connects DIRECTLY there. The URL changes when
629
+ * the sandbox is replaced (idle eviction, redeploy), which is exactly
630
+ * when a reconnect happens, so per-attempt brokering is what makes
631
+ * reconnects land on the replacement. Without one (`aai dev`, older
632
+ * servers), the same-origin `websocket` path is used.
633
+ * - Once the first `config` arrives, every reconnect carries
634
+ * `?sessionId=<id>` and the server resumes the SAME session (id, tool
635
+ * state) instead of minting a new one. `resume=1` remains only as the
636
+ * greeting-suppression fallback for a server whose config carried no id.
589
637
  */
590
- function currentWsUrl() {
591
- return buildWsUrl(options.platformUrl, hasConnected, sessionId).toString();
638
+ async function currentWsUrl() {
639
+ const cfg = serverIsBroker === false ? {} : await fetchClientConfig(options.platformUrl);
640
+ serverIsBroker = cfg.sessionUrl !== void 0;
641
+ const url = cfg.sessionUrl ? buildBrokeredWsUrl(cfg.sessionUrl, hasConnected, sessionId) : buildWsUrl(options.platformUrl, hasConnected, sessionId);
642
+ const display = new URL(url);
643
+ display.search = "";
644
+ if (display.toString() !== currentSnapshot.apiUrl) updateState({ apiUrl: display.toString() });
645
+ return url.toString();
592
646
  }
593
- /** Open a socket: an injected constructor as-is (tests), or partysocket's
594
- * reconnecting WebSocket same interface, plus reconnect-on-close. */
647
+ /** Open a socket: an injected constructor as-is (tests connects to the
648
+ * same-origin path, no brokering), or partysocket's reconnecting
649
+ * WebSocket — same interface, plus reconnect-on-close. */
595
650
  function openSocket() {
596
- if (options.WebSocket) return new options.WebSocket(currentWsUrl());
651
+ if (options.WebSocket) return new options.WebSocket(buildWsUrl(options.platformUrl, hasConnected, sessionId).toString());
597
652
  return openReconnectingSocket(currentWsUrl);
598
653
  }
599
654
  function connect(opts) {
@@ -606,7 +661,7 @@ function createSessionCore(options) {
606
661
  error: null
607
662
  });
608
663
  teardownConnection();
609
- conn.generation++;
664
+ conn.generation.bump();
610
665
  const controller = new AbortController();
611
666
  connectionController = controller;
612
667
  const { signal: sig } = controller;
@@ -629,7 +684,7 @@ function createSessionCore(options) {
629
684
  if (sig.aborted) return;
630
685
  cleanupAudio();
631
686
  if (reconnectPending(socket)) {
632
- conn.generation++;
687
+ conn.generation.bump();
633
688
  socketErrored = false;
634
689
  updateState({
635
690
  state: "connecting",
@@ -716,4 +771,4 @@ function createSessionCore(options) {
716
771
  };
717
772
  }
718
773
  //#endregion
719
- export { createSessionCore as t };
774
+ export { buildAgentUrl as n, fetchClientConfig as r, createSessionCore as t };
@@ -24,13 +24,10 @@ export type AudioSetupDeps = {
24
24
  * VoiceIO is closed immediately to prevent it from being assigned to a newer
25
25
  * connection.
26
26
  *
27
- * `fatal` selects the failure mode: voice sessions (`config` handshake) can't
28
- * function without the mic, so failure there sets the error state and ends
29
- * the session; a text-only session's opt-in record button must not brick an
30
- * otherwise healthy session, so failure there is a banner (`error` set,
31
- * state kept) and the session stays interactive.
27
+ * A failure is fatal: a voice session can't function without the mic, so it
28
+ * sets the error state and ends the session.
32
29
  */
33
30
  export declare function initAudioCapture(conn: ConnState, msg: {
34
31
  sampleRate: number;
35
32
  ttsSampleRate: number;
36
- }, deps: AudioSetupDeps, fatal: boolean): Promise<void>;
33
+ }, deps: AudioSetupDeps): Promise<void>;
@@ -9,6 +9,7 @@ export declare const CLEARED_SESSION_STATE: {
9
9
  messages: never[];
10
10
  toolCalls: never[];
11
11
  customEvents: never[];
12
+ agentState: null;
12
13
  userTranscript: null;
13
14
  agentTranscript: null;
14
15
  error: null;
@@ -50,7 +51,7 @@ type MessageHandlers = {
50
51
  /**
51
52
  * Create the server→client message handlers for one session core.
52
53
  *
53
- * Encapsulates the two turn-boundary counters (`handlerGeneration` for
54
+ * Encapsulates the two turn-boundary counters (`turnEpoch` for
54
55
  * discarding stale async audio completions, `customEventSeq` for event
55
56
  * dedup) that previously lived as closure locals in `createSessionCore`.
56
57
  */
@@ -6,10 +6,11 @@
6
6
  import ReconnectingWebSocket from "partysocket/ws";
7
7
  /**
8
8
  * Open partysocket's reconnecting WebSocket. The URL is a *provider*,
9
- * re-evaluated on every attempt, so each retry picks up the current resume
10
- * URL rather than the one the session started with.
9
+ * re-evaluated on every attempt (async supported), so each retry picks up
10
+ * the current broker-named endpoint and resume URL rather than the ones the
11
+ * session started with.
11
12
  */
12
- export declare function openReconnectingSocket(urlProvider: () => string): ReconnectingWebSocket;
13
+ export declare function openReconnectingSocket(urlProvider: () => Promise<string>): ReconnectingWebSocket;
13
14
  /**
14
15
  * True while `socket` is a reconnecting socket that will retry after the
15
16
  * close event currently being handled. partysocket schedules the retry
@@ -2,9 +2,8 @@
2
2
  * Type declarations for the framework-agnostic voice session core.
3
3
  *
4
4
  * Split out of `session-core.ts` to keep that module focused on behaviour.
5
- * The public types here are re-exported from `session-core.ts` for
6
- * backwards compatibility.
7
5
  */
6
+ import type { Epoch } from "@alexkroman1/aai";
8
7
  import type { VoiceIO } from "./audio.ts";
9
8
  import type { AgentState, ChatMessage, SessionError, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor } from "./types.ts";
10
9
  /**
@@ -45,6 +44,12 @@ export type SessionSnapshot = {
45
44
  readonly messages: ChatMessage[];
46
45
  readonly toolCalls: ToolCallInfo[];
47
46
  readonly customEvents: CustomEvent[];
47
+ /**
48
+ * Latest state the agent projected via `syncState`, or `null` before the
49
+ * first push. A value, not a log — a component that mounts mid-session
50
+ * reads current state rather than replaying events it missed.
51
+ */
52
+ readonly agentState: unknown;
48
53
  readonly userTranscript: string | null;
49
54
  readonly agentTranscript: string | null;
50
55
  readonly error: SessionError | null;
@@ -99,9 +104,10 @@ export type ConnState = {
99
104
  ws: InstanceType<WebSocketConstructor> | null;
100
105
  voiceIO: VoiceIO | null;
101
106
  audioSetupInFlight: boolean;
102
- /** Monotonically increasing counter bumped on each connect(). Prevents a stale
103
- * initAudioCapture from assigning its voiceIO to a newer connection. */
104
- generation: number;
107
+ /** Connection epoch, bumped on each connect()/retry (see `createEpoch`).
108
+ * Prevents a stale initAudioCapture from assigning its voiceIO to a newer
109
+ * connection. */
110
+ generation: Epoch;
105
111
  /** Audio chunks that arrived before `voiceIO` was initialized — drained into
106
112
  * the playback worklet once init completes. Closes the race between the
107
113
  * server starting greeting audio (immediately on S2S connect) and the
@@ -111,11 +117,4 @@ export type ConnState = {
111
117
  * signal must be replayed after draining preInitAudio, or a short greeting
112
118
  * buffered during mic-permission never finishes playing. */
113
119
  preInitDone: boolean;
114
- /** The server's `config` payload for the current connection — kept so
115
- * text-only sessions can init the mic lazily (record button) and file
116
- * uploads know the STT sample rate to resample to. */
117
- readyConfig: {
118
- sampleRate: number;
119
- ttsSampleRate: number;
120
- } | null;
121
120
  };
@@ -1,2 +1,7 @@
1
1
  /** Build the session WebSocket URL from the platform URL and resume state. */
2
2
  export declare function buildWsUrl(platformUrl: string, resume: boolean, sessionId?: string): URL;
3
+ /**
4
+ * Turn a broker-provided session URL (`sessionUrl` from `GET client-config`
5
+ * — the agent's live sandbox endpoint) into this attempt's connect URL.
6
+ */
7
+ export declare function buildBrokeredWsUrl(sessionUrl: string, resume: boolean, sessionId?: string): URL;
@@ -1,5 +1,4 @@
1
1
  import type { SessionCore, SessionCoreOptions } from "./session-core-types.ts";
2
- export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core-types.ts";
3
2
  /**
4
3
  * Create a framework-agnostic voice session core that connects to an AAI
5
4
  * server via WebSocket.
@@ -1,2 +1,2 @@
1
- import { t as createSessionCore } from "./session-core-DgioOYyx.js";
1
+ import { t as createSessionCore } from "./session-core-Bjkr4PAP.js";
2
2
  export { createSessionCore };
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import type { DefaultToolResult } from "@alexkroman1/aai";
1
2
  import type { SessionErrorCode } from "@alexkroman1/aai/protocol";
2
- export { CAPTURE_STOP_ACK_TIMEOUT_MS, DEFAULT_STT_SAMPLE_RATE, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, } from "@alexkroman1/aai";
3
+ export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, } from "@alexkroman1/aai";
3
4
  /**
4
5
  * `getUserMedia` audio constraints for every capture path in this package.
5
6
  *
@@ -54,7 +55,22 @@ export type ChatMessage = {
54
55
  export type ToolCallInfo = {
55
56
  callId: string;
56
57
  name: string;
57
- args: Record<string, unknown>;
58
+ /**
59
+ * The tool's arguments, as the model sent them.
60
+ *
61
+ * Values are {@link DefaultToolResult} — `any` — for the same reason a tool
62
+ * *result* is: the shape is the author's own Zod schema, which the framework
63
+ * cannot see from here. As `Record<string, unknown>` the ordinary
64
+ * `toolCall.args.url` was a compile error in a client that runs correctly,
65
+ * and the escape hatch agents reached for next (`args as FetchJsonArgs`) is
66
+ * itself an error — TypeScript rejects the cast as insufficiently
67
+ * overlapping. That pair cost two build rounds in one run.
68
+ *
69
+ * Annotate at the read site for real checking:
70
+ * `const { url } = toolCall.args as { url: string }` is still available, and
71
+ * now actually compiles.
72
+ */
73
+ args: Record<string, DefaultToolResult>;
58
74
  status: "pending" | "done";
59
75
  result?: string | undefined;
60
76
  /**
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CAPTURE_STOP_ACK_TIMEOUT_MS, DEFAULT_STT_SAMPLE_RATE, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS } from "@alexkroman1/aai";
1
+ import { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS } from "@alexkroman1/aai";
2
2
  //#region types.ts
3
3
  /**
4
4
  * `getUserMedia` audio constraints for every capture path in this package.
@@ -29,4 +29,4 @@ const VOICE_CAPTURE_CONSTRAINTS = {
29
29
  voiceIsolation: false
30
30
  };
31
31
  //#endregion
32
- export { CAPTURE_STOP_ACK_TIMEOUT_MS, DEFAULT_STT_SAMPLE_RATE, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, VOICE_CAPTURE_CONSTRAINTS };
32
+ export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, VOICE_CAPTURE_CONSTRAINTS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "3.2.0",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -19,8 +19,10 @@
19
19
  "dependencies": {
20
20
  "clsx": "^2.1.1",
21
21
  "partysocket": "^1.3.0",
22
+ "react-markdown": "^10.1.0",
23
+ "remark-gfm": "^4.0.1",
22
24
  "use-sync-external-store": "^1.6.0",
23
- "@alexkroman1/aai": "3.2.0"
25
+ "@alexkroman1/aai": "5.0.0"
24
26
  },
25
27
  "peerDependencies": {
26
28
  "react": "^19.0.0",
@@ -1 +0,0 @@
1
- import{a as e,o as t,t as n}from"./index-BeVXjGwG.js";function r(e){let t=new Int16Array(e.length),n=0;for(let r of e){let e=Math.max(-1,Math.min(1,r));t[n++]=e<0?e*32768:e*32767}return t}function i(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function a(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function o(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){return new Promise(e=>{let t=setTimeout(e,250);i=()=>{clearTimeout(t),e()},r.port.postMessage({event:`stop`})})}}}async function s(r){let{sttSampleRate:s,ttsSampleRate:c,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:d,onError:f,onPlaybackStats:p,onMicSilent:m}=r,h=new AudioContext({sampleRate:c,latencyHint:`playback`}),g=s===c,_=g?h:new AudioContext({sampleRate:s,latencyHint:`interactive`});async function v(){let e=g?[h]:[h,_];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let y=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),b;try{[b]=await Promise.all([y,h.resume(),_.resume(),_.audioWorklet.addModule(l),h.audioWorklet.addModule(u)]),i(_.sampleRate,s,`capture`),i(h.sampleRate,c,`playback`)}catch(e){throw a(y),await v(),e}let x=_.createMediaStreamSource(b),S=o(_,d,m);x.connect(S.node),S.node.onprocessorerror=()=>{let e=Error(`Audio capture worklet crashed`);console.error(`[aai-ui]`,e.message),f?.(e)},S.start();let C=null,w=null,T=new AbortController;function E(){if(C)return C;let e=new AudioWorkletNode(h,`playback-processor`);return e.connect(h.destination),e.port.onmessage=e=>{if(e.data.event===`stop`){let t=e.data.stats;if(t&&t.concealedSamples>0&&p?.(t),e.data.reason===`interrupt`)return;w?.(),w=null}},e.onprocessorerror=()=>{let e=Error(`Audio playback worklet crashed`);console.error(`[aai-ui]`,e.message),w?.(),w=null,f?.(e)},C=e,e}let D={enqueue(e){T.signal.aborted||e.byteLength!==0&&E().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){return!C||(C.port.postMessage({event:`done`}),h.state!==`running`)?Promise.resolve():new Promise(n=>{w?.();let r=()=>{clearInterval(i),clearTimeout(a),w===r&&(w=null),n()},i=setInterval(()=>{h.state!==`running`&&r()},t),a=setTimeout(r,e);w=r})},flush(){C&&(w?.(),w=null,C.port.postMessage({event:`interrupt`}))},async close(){if(!T.signal.aborted){T.abort(),await S.stop(),x.disconnect(),S.node.disconnect(),C&&C.disconnect();for(let e of b.getTracks())e.stop();await v()}},async[Symbol.asyncDispose](){await D.close()}};return D}export{i as assertGranted,o as createCaptureNode,s as createVoiceIO,r as floatToPcm16,a as releaseStreamOnFailure};