@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.
- package/dist/audio.d.ts +0 -33
- package/dist/audio.js +2 -20
- package/dist/{chat-view-C1oJxsWz.js → chat-view-CW-tZCMm.js} +1 -4
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +0 -9
- package/dist/components/markdown.d.ts +21 -0
- package/dist/components/message-list.d.ts +0 -8
- package/dist/components/message-list.js +3 -230
- package/dist/components/url-chips.d.ts +0 -15
- package/dist/context.d.ts +1 -1
- package/dist/default-client/assets/audio-Bu1LSuiR.js +1 -0
- package/dist/default-client/assets/{capture-processor-DYl6YIu5.js → capture-processor-ssRD81Mo.js} +1 -1
- package/dist/default-client/assets/index-0-DXv-S4.js +192 -0
- package/dist/default-client/assets/index-CRfdZPH1.css +2 -0
- package/dist/default-client/assets/{playback-processor-CeEGzgVT.js → playback-processor-CRwbxC1v.js} +1 -1
- package/dist/default-client/index.html +2 -2
- package/dist/define-client.d.ts +13 -2
- package/dist/define-client.js +120 -4
- package/dist/hooks.d.ts +40 -2
- package/dist/hooks.js +24 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +7 -8
- package/dist/message-list-q2lTiFhF.js +381 -0
- package/dist/{session-core-DgioOYyx.js → session-core-Bjkr4PAP.js} +112 -57
- package/dist/session-core-audio-setup.d.ts +3 -6
- package/dist/session-core-messages.d.ts +2 -1
- package/dist/session-core-reconnect.d.ts +4 -3
- package/dist/session-core-types.d.ts +11 -12
- package/dist/session-core-url.d.ts +5 -0
- package/dist/session-core.d.ts +0 -1
- package/dist/session-core.js +1 -1
- package/dist/types.d.ts +18 -2
- package/dist/types.js +2 -2
- package/package.json +4 -2
- package/dist/default-client/assets/audio-CIZMTfDQ.js +0 -1
- package/dist/default-client/assets/index-BeVXjGwG.js +0 -344
- package/dist/default-client/assets/index-Dv-Q5VRL.css +0 -2
- 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 {
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (`
|
|
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
|
-
/**
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* `
|
|
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 =
|
|
353
|
+
const gen = turnEpoch.current();
|
|
332
354
|
io.done().then(() => {
|
|
333
|
-
if (
|
|
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 `
|
|
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
|
|
410
|
-
* URL rather than the
|
|
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
|
-
|
|
429
|
-
|
|
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:
|
|
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
|
|
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
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
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
|
-
|
|
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
|
|
594
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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 (`
|
|
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
|
|
10
|
-
* URL rather than the
|
|
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
|
-
/**
|
|
103
|
-
* initAudioCapture from assigning its voiceIO to a newer
|
|
104
|
-
|
|
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;
|
package/dist/session-core.d.ts
CHANGED
|
@@ -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.
|
package/dist/session-core.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createSessionCore } from "./session-core-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
+
"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": "
|
|
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};
|