@alexkroman1/aai-ui 13.3.0 → 14.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/README.md +159 -69
- package/dist/{_colors-CZ6OlPbL.js → _colors-CpZO-88A.js} +24 -1
- package/dist/_recover-run.d.ts +2 -2
- package/dist/_submission-state.d.ts +92 -0
- package/dist/_upload-files.d.ts +2 -2
- package/dist/_upload-report.d.ts +26 -0
- package/dist/{_utils-CyzjK0gW.js → _utils-DnQDM9Uy.js} +3 -3
- package/dist/_utils.d.ts +3 -3
- package/dist/_web-storage.d.ts +43 -0
- package/dist/_workflow-files.d.ts +1 -1
- package/dist/agent-state-labels.d.ts +60 -0
- package/dist/audio.js +20 -14
- package/dist/{chat-view-Bv5VFJIE.js → chat-view-C_3T7Ln8.js} +96 -29
- package/dist/{client-config-DD820zHn.js → client-config-DJQHnYjm.js} +4 -4
- package/dist/client-config.d.ts +4 -4
- package/dist/client-dir.d.ts +1 -1
- package/dist/client-dir.js +1 -1
- package/dist/components/_colors.d.ts +23 -0
- package/dist/components/_form-readiness.d.ts +1 -1
- package/dist/components/bullet-list.d.ts +74 -0
- package/dist/components/button.js +3 -3
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +16 -20
- package/dist/components/controls.js +3 -3
- package/dist/components/facts.d.ts +81 -0
- package/dist/components/form-fields.d.ts +6 -6
- package/dist/components/form-types.d.ts +1 -1
- package/dist/components/form.d.ts +1 -1
- package/dist/components/message-list.js +1 -1
- package/dist/components/session-error-banner.d.ts +69 -0
- package/dist/components/start-screen.js +1 -1
- package/dist/components/tool-call-block.js +1 -1
- package/dist/components/tool-config-context.d.ts +1 -1
- package/dist/components/workflow-progress.d.ts +11 -4
- package/dist/context.d.ts +142 -19
- package/dist/context.js +155 -17
- package/dist/default-client/assets/{audio-BuDICbPf.js → audio-9zQsNc1w.js} +1 -1
- package/dist/default-client/assets/index-BTv30Z4F.css +2 -0
- package/dist/default-client/assets/index-RAZ-29Sz.js +284 -0
- package/dist/default-client/index.html +2 -2
- package/dist/define-client.d.ts +19 -19
- package/dist/define-client.js +19 -19
- package/dist/hooks.d.ts +44 -8
- package/dist/hooks.js +19 -13
- package/dist/index.d.ts +11 -7
- package/dist/index.js +418 -185
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +5 -5
- package/dist/{message-list-C0pL7x41.js → message-list-CdOnSh5m.js} +19 -12
- package/dist/page.d.ts +11 -11
- package/dist/session-core-audio-setup.d.ts +1 -1
- package/dist/session-core-dial.d.ts +0 -2
- package/dist/{session-core-C9elBIdu.js → session-core-gwePM95B.js} +125 -52
- package/dist/session-core-messages.d.ts +2 -2
- package/dist/session-core-types.d.ts +58 -1
- package/dist/session-core.d.ts +6 -6
- package/dist/session-core.js +2 -2
- package/dist/session-resume-store.d.ts +3 -3
- package/dist/{tool-call-block-Bunc6rCw.js → tool-call-block-C2t_5fpp.js} +27 -9
- package/dist/{tool-config-context-Bh8p3DtG.js → tool-config-context-Es4YUzV2.js} +1 -1
- package/dist/types.d.ts +19 -4
- package/dist/types.js +2 -2
- package/dist/{url-chips-C2u7QPv8.js → url-chips-BxhzZgk2.js} +4 -4
- package/dist/use-conversation.d.ts +1 -1
- package/dist/{use-user-transcript-DFTSEuZN.js → use-user-transcript-uyHhzy4d.js} +3 -2
- package/dist/use-workflow-form.d.ts +34 -2
- package/dist/{use-workflow-run-CXGEcM0l.js → use-workflow-run-CP2ekKPV.js} +3 -6
- package/dist/use-workflow-stream.d.ts +1 -1
- package/dist/workflow-client.d.ts +1 -1
- package/package.json +2 -2
- package/styles.css +78 -0
- package/dist/default-client/assets/index-B1_ROnTJ.js +0 -284
- package/dist/default-client/assets/index-S5fkKi6B.css +0 -2
package/dist/internal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@alexkroman1/aai-ui/internal` — the plumbing `
|
|
2
|
+
* `@alexkroman1/aai-ui/internal` — the plumbing `mountClient()` and the default
|
|
3
3
|
* client install for themselves, NOT part of the public client API and not
|
|
4
4
|
* covered by semver. A `client.tsx` should never import from here; everything
|
|
5
5
|
* an author writes a page or a custom chrome against lives on the root export
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* These names used to ride on the root barrel, tagged `@internal` and nothing
|
|
9
9
|
* else, which meant they were in a client author's autocomplete beside
|
|
10
|
-
* `
|
|
10
|
+
* `mountClient()`, `<Form>`, `useAgentState` and `useWorkflowRun` — eight symbols an
|
|
11
11
|
* author is invited to reach for and no capability contract covers. Keeping
|
|
12
12
|
* them on their own subpath keeps the root importable surface the same shape as
|
|
13
13
|
* the promise: what is on it is contracted.
|
package/dist/internal.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-
|
|
1
|
+
import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-DJQHnYjm.js";
|
|
2
2
|
import { SessionProvider, ThemeProvider } from "./context.js";
|
|
3
|
-
import { n as SessionUrlChips, r as UiUrlChip, t as ApiUrlChip } from "./url-chips-
|
|
4
|
-
import { t as TRANSCRIBING_PLACEHOLDER } from "./use-user-transcript-
|
|
5
|
-
import { t as ToolConfigContext } from "./tool-config-context-
|
|
6
|
-
import { i as DEFAULT_PROGRESS_POLL_MS, n as MAX_MISSING_READS, t as DEFAULT_WORKFLOW_POLL_MS } from "./use-workflow-run-
|
|
3
|
+
import { n as SessionUrlChips, r as UiUrlChip, t as ApiUrlChip } from "./url-chips-BxhzZgk2.js";
|
|
4
|
+
import { t as TRANSCRIBING_PLACEHOLDER } from "./use-user-transcript-uyHhzy4d.js";
|
|
5
|
+
import { t as ToolConfigContext } from "./tool-config-context-Es4YUzV2.js";
|
|
6
|
+
import { i as DEFAULT_PROGRESS_POLL_MS, n as MAX_MISSING_READS, t as DEFAULT_WORKFLOW_POLL_MS } from "./use-workflow-run-CP2ekKPV.js";
|
|
7
7
|
import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
|
|
8
8
|
export { ApiUrlChip, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, MAX_MISSING_READS, SessionProvider, SessionUrlChips, TRANSCRIBING_PLACEHOLDER, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, loadClientConfig };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useSessionSelector, useTheme } from "./context.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as useUserTranscript } from "./use-user-transcript-
|
|
4
|
-
import { t as ToolCallBlock } from "./tool-call-block-
|
|
2
|
+
import { a as inkTint, o as primaryTint } from "./_colors-CpZO-88A.js";
|
|
3
|
+
import { n as useUserTranscript } from "./use-user-transcript-uyHhzy4d.js";
|
|
4
|
+
import { t as ToolCallBlock } from "./tool-call-block-C2t_5fpp.js";
|
|
5
5
|
import clsx from "clsx";
|
|
6
6
|
import { StickToBottom } from "use-stick-to-bottom";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -142,7 +142,7 @@ function isThinking(state, messages, toolCalls) {
|
|
|
142
142
|
* Subscribe to the conversation: the interleaved exchange, the streaming
|
|
143
143
|
* utterance, the live transcript and the thinking rule — with no markup.
|
|
144
144
|
*
|
|
145
|
-
* Must be used inside the provider `
|
|
145
|
+
* Must be used inside the provider `mountClient()` installs.
|
|
146
146
|
*
|
|
147
147
|
* @example A custom bubble, keeping every rule `<MessageList>` knows
|
|
148
148
|
* ```tsx
|
|
@@ -179,16 +179,23 @@ function useConversation() {
|
|
|
179
179
|
const toolCalls = useSessionSelector((s) => s.toolCalls);
|
|
180
180
|
const streaming = useSessionSelector((s) => s.agentTranscript);
|
|
181
181
|
const transcript = useUserTranscript();
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
const items = useMemo(() => interleave(messages, toolCalls), [messages, toolCalls]);
|
|
183
|
+
const thinking = useMemo(() => isThinking(state, messages, toolCalls), [
|
|
184
|
+
state,
|
|
185
|
+
messages,
|
|
186
|
+
toolCalls
|
|
187
|
+
]);
|
|
188
|
+
return useMemo(() => ({
|
|
189
|
+
items,
|
|
184
190
|
streaming,
|
|
185
191
|
transcript,
|
|
186
|
-
thinking
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
thinking
|
|
193
|
+
}), [
|
|
194
|
+
items,
|
|
195
|
+
streaming,
|
|
196
|
+
transcript,
|
|
197
|
+
thinking
|
|
198
|
+
]);
|
|
192
199
|
}
|
|
193
200
|
//#endregion
|
|
194
201
|
//#region src/components/markdown.tsx
|
package/dist/page.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
2
|
/**
|
|
3
|
-
* `
|
|
3
|
+
* `mountPage()` — mount a WORKFLOW APP's UI: React, theme, no session.
|
|
4
4
|
*
|
|
5
|
-
* The twin of `
|
|
5
|
+
* The twin of `mountClient()` for an agent whose front door is a form rather than a
|
|
6
6
|
* microphone (`workflowApp()`). It is a separate entry rather than
|
|
7
|
-
* an option on `
|
|
8
|
-
* constructs a `
|
|
7
|
+
* an option on `mountClient()` because of what `mountClient()` unavoidably does: it
|
|
8
|
+
* constructs a `BrowserSession`, which owns a WebSocket URL provider, an audio
|
|
9
9
|
* graph, and a microphone request. A flag would have to make all of that
|
|
10
10
|
* conditional, and every session hook would then have to answer "what does this
|
|
11
11
|
* mean with no session?" — so the honest split is two mounts. A page that wants
|
|
12
|
-
* voice uses `
|
|
12
|
+
* voice uses `mountClient()`; a page that wants neither audio nor a socket uses this.
|
|
13
13
|
*
|
|
14
14
|
* Authoring is otherwise identical — the file is still `client.tsx`, still
|
|
15
15
|
* React, still Tailwind, still the same theme tokens — so a workflow app reads
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { type ComponentType } from "react";
|
|
20
20
|
import type { ClientTheme } from "./types.ts";
|
|
21
21
|
/**
|
|
22
|
-
* Configuration for {@link
|
|
22
|
+
* Configuration for {@link mountPage}.
|
|
23
23
|
*
|
|
24
24
|
* @public
|
|
25
25
|
*/
|
|
@@ -33,14 +33,14 @@ export type PageConfig = {
|
|
|
33
33
|
target?: string | HTMLElement;
|
|
34
34
|
/**
|
|
35
35
|
* Page title. Set only when given, so a title the HTML shell declared is never
|
|
36
|
-
* clobbered — the same rule `
|
|
36
|
+
* clobbered — the same rule `mountClient()`'s custom-component tier follows.
|
|
37
37
|
*/
|
|
38
38
|
name?: string;
|
|
39
39
|
/** Theme color overrides, read by the same tokens the voice components use. */
|
|
40
40
|
theme?: ClientTheme;
|
|
41
41
|
};
|
|
42
42
|
/**
|
|
43
|
-
* Handle returned by {@link
|
|
43
|
+
* Handle returned by {@link mountPage}. `Disposable`, so `using` works.
|
|
44
44
|
*
|
|
45
45
|
* @public
|
|
46
46
|
*/
|
|
@@ -59,7 +59,7 @@ export type PageHandle = {
|
|
|
59
59
|
*
|
|
60
60
|
* @example
|
|
61
61
|
* ```tsx
|
|
62
|
-
* import { createWorkflowApi,
|
|
62
|
+
* import { createWorkflowApi, mountPage, useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
63
63
|
* import { useState } from "react";
|
|
64
64
|
*
|
|
65
65
|
* // Hoisted: a client built in render is a new object every render.
|
|
@@ -78,11 +78,11 @@ export type PageHandle = {
|
|
|
78
78
|
* );
|
|
79
79
|
* }
|
|
80
80
|
*
|
|
81
|
-
*
|
|
81
|
+
* mountPage({ name: "Digest", component: App });
|
|
82
82
|
* ```
|
|
83
83
|
*
|
|
84
84
|
* @throws If the target element is not found in the DOM.
|
|
85
85
|
*
|
|
86
86
|
* @public
|
|
87
87
|
*/
|
|
88
|
-
export declare function
|
|
88
|
+
export declare function mountPage(config: PageConfig): PageHandle;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SessionCommand } from "@alexkroman1/aai/protocol";
|
|
2
2
|
import type { VoiceIO } from "./audio.ts";
|
|
3
3
|
import type { SessionStateMachine } from "./session-core-state.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { type ConnState, type SessionSnapshot } from "./session-core-types.ts";
|
|
5
5
|
/** Dependencies `initAudioCapture` needs from the owning session core. */
|
|
6
6
|
export type AudioSetupDeps = {
|
|
7
7
|
sendJson: (msg: SessionCommand) => void;
|
|
@@ -22,8 +22,6 @@ export type DialOptions = {
|
|
|
22
22
|
resumeSessionId?: string | undefined;
|
|
23
23
|
};
|
|
24
24
|
export type Dialer = {
|
|
25
|
-
/** The URL for the next attempt — partysocket's async provider. */
|
|
26
|
-
url(): Promise<string>;
|
|
27
25
|
/** A socket for this attempt. */
|
|
28
26
|
open(): InstanceType<WebSocketConstructor>;
|
|
29
27
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-
|
|
1
|
+
import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-DJQHnYjm.js";
|
|
2
2
|
import { MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
|
|
3
3
|
import { SessionEventSchema, lenientParse } from "@alexkroman1/aai/protocol";
|
|
4
4
|
import { errorMessage, safeJsonParse } from "@alexkroman1/aai";
|
|
@@ -6,6 +6,44 @@ import { omitUndefined } from "@alexkroman1/aai/utils";
|
|
|
6
6
|
import { DEFAULT_MAX_HISTORY, WS_OPEN, createEpoch, toArgsRecord } from "@alexkroman1/aai/internal";
|
|
7
7
|
import ReconnectingWebSocket from "partysocket/ws";
|
|
8
8
|
import { and, assign, createActor, not, setup, stateIn } from "xstate";
|
|
9
|
+
//#region src/session-core-types.ts
|
|
10
|
+
/**
|
|
11
|
+
* The two liveness fields at rest.
|
|
12
|
+
*
|
|
13
|
+
* `running` and `recording` ride with almost every state transition and were
|
|
14
|
+
* spread as a pair of literals at seven sites across three modules — the same
|
|
15
|
+
* shape `session-core-state.ts` folded `state` and `error` out of, one field
|
|
16
|
+
* short. Naming it relates them: a transition that ends the call says so once,
|
|
17
|
+
* and a reader looking for "what stops the mic" finds one symbol rather than a
|
|
18
|
+
* grep.
|
|
19
|
+
*
|
|
20
|
+
* It is deliberately NOT the whole snapshot patch — a transition still supplies
|
|
21
|
+
* its own `agentState.apply(...)` projection beside this.
|
|
22
|
+
*/
|
|
23
|
+
const STOPPED = {
|
|
24
|
+
running: false,
|
|
25
|
+
recording: false
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A turn boundary: end the current turn and settle whatever it was playing.
|
|
29
|
+
*
|
|
30
|
+
* The two calls are one fact and were written out at four sites — `cancel()`
|
|
31
|
+
* and `reset()` here, `reply.cancelled` and `session.reset` on the server side
|
|
32
|
+
* — where the pair is load-bearing in both halves. The bump stops a stale drain
|
|
33
|
+
* continuation from stamping `"listening"` over a state the session has since
|
|
34
|
+
* moved to; the flush settles the interrupted turn's `done()` so it cannot
|
|
35
|
+
* strand.
|
|
36
|
+
*
|
|
37
|
+
* Two further sites bump WITHOUT flushing (`cleanupAudio`, a committed user
|
|
38
|
+
* transcript) and stay spelled out, which is the point of naming this one: a
|
|
39
|
+
* bump on its own now reads as a deliberate choice rather than a forgotten
|
|
40
|
+
* flush.
|
|
41
|
+
*/
|
|
42
|
+
function bargeIn(conn) {
|
|
43
|
+
conn.turn.bump();
|
|
44
|
+
conn.voiceIO?.flush();
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
9
47
|
//#region src/session-core-audio-setup.ts
|
|
10
48
|
/**
|
|
11
49
|
* Audio-path initialization for the voice session core.
|
|
@@ -60,11 +98,11 @@ async function initAudioCapture(conn, msg, deps) {
|
|
|
60
98
|
type: "FAILED",
|
|
61
99
|
error: {
|
|
62
100
|
code: "audio",
|
|
63
|
-
message
|
|
101
|
+
message,
|
|
102
|
+
fatal: false
|
|
64
103
|
}
|
|
65
104
|
}),
|
|
66
|
-
|
|
67
|
-
recording: false
|
|
105
|
+
...STOPPED
|
|
68
106
|
});
|
|
69
107
|
};
|
|
70
108
|
try {
|
|
@@ -232,6 +270,55 @@ function applyResumeParams(wsUrl, resume, sessionId) {
|
|
|
232
270
|
return wsUrl;
|
|
233
271
|
}
|
|
234
272
|
//#endregion
|
|
273
|
+
//#region src/_web-storage.ts
|
|
274
|
+
/**
|
|
275
|
+
* The store, or nothing.
|
|
276
|
+
*
|
|
277
|
+
* Not exported: a caller that reaches for the store itself has stepped outside
|
|
278
|
+
* the guard, which is the whole point of this module.
|
|
279
|
+
*/
|
|
280
|
+
function storeFor(kind) {
|
|
281
|
+
return kind === "local" ? globalThis.localStorage : globalThis.sessionStorage;
|
|
282
|
+
}
|
|
283
|
+
/** The stored value, or undefined — for a missing entry and an absent store alike. */
|
|
284
|
+
function storageGet(kind, key) {
|
|
285
|
+
try {
|
|
286
|
+
return storeFor(kind)?.getItem(key) ?? void 0;
|
|
287
|
+
} catch {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Remember a value. A store that refuses is a no-op, never a throw. */
|
|
292
|
+
function storageSet(kind, key, value) {
|
|
293
|
+
try {
|
|
294
|
+
storeFor(kind)?.setItem(key, value);
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
/** Forget a value. Nothing stored and no store are the same outcome. */
|
|
298
|
+
function storageRemove(kind, key) {
|
|
299
|
+
try {
|
|
300
|
+
storeFor(kind)?.removeItem(key);
|
|
301
|
+
} catch {}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* A storage key namespaced by a URL.
|
|
305
|
+
*
|
|
306
|
+
* `target` is resolved against the document, so a relative path ("./", the
|
|
307
|
+
* default-client case) and the absolute form of the same agent agree on one
|
|
308
|
+
* key.
|
|
309
|
+
*
|
|
310
|
+
* @param prefix - The owning module's namespace, e.g. `"aai:session:"`.
|
|
311
|
+
* @param target - What to resolve — an agent's `platformUrl`, or `"./"` for the
|
|
312
|
+
* page's own directory.
|
|
313
|
+
*/
|
|
314
|
+
function urlSlot(prefix, target) {
|
|
315
|
+
try {
|
|
316
|
+
return `${prefix}${new URL(target, globalThis.location?.href).href}`;
|
|
317
|
+
} catch {
|
|
318
|
+
return `${prefix}${target}`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
235
322
|
//#region src/session-resume-store.ts
|
|
236
323
|
/**
|
|
237
324
|
* Where a session id survives a page RELOAD.
|
|
@@ -258,32 +345,22 @@ function applyResumeParams(wsUrl, resume, sessionId) {
|
|
|
258
345
|
* Keyed by the agent's own URL, so two agents served from one origin — which is
|
|
259
346
|
* every deployed agent, at `/:slug/` — cannot inherit each other's session.
|
|
260
347
|
*
|
|
261
|
-
* Every access is guarded
|
|
262
|
-
*
|
|
263
|
-
*
|
|
348
|
+
* Every access is guarded, and the guard lives in `_web-storage.ts`: a session
|
|
349
|
+
* that cannot be remembered must degrade to today's behaviour rather than
|
|
350
|
+
* failing to start.
|
|
264
351
|
*/
|
|
265
352
|
const PREFIX = "aai:session:";
|
|
266
353
|
/** One agent's slot in storage. */
|
|
267
354
|
function keyFor(platformUrl) {
|
|
268
|
-
|
|
269
|
-
return `${PREFIX}${new URL(platformUrl, globalThis.location?.href).href}`;
|
|
270
|
-
} catch {
|
|
271
|
-
return `${PREFIX}${platformUrl}`;
|
|
272
|
-
}
|
|
355
|
+
return urlSlot(PREFIX, platformUrl);
|
|
273
356
|
}
|
|
274
357
|
/** The stored session id for this agent, or undefined. @internal */
|
|
275
358
|
function readStoredSessionId(platformUrl) {
|
|
276
|
-
|
|
277
|
-
return globalThis.sessionStorage?.getItem(keyFor(platformUrl)) ?? void 0;
|
|
278
|
-
} catch {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
359
|
+
return storageGet("session", keyFor(platformUrl));
|
|
281
360
|
}
|
|
282
361
|
/** Remember this agent's session id for the next load. @internal */
|
|
283
362
|
function writeStoredSessionId(platformUrl, sessionId) {
|
|
284
|
-
|
|
285
|
-
globalThis.sessionStorage?.setItem(keyFor(platformUrl), sessionId);
|
|
286
|
-
} catch {}
|
|
363
|
+
storageSet("session", keyFor(platformUrl), sessionId);
|
|
287
364
|
}
|
|
288
365
|
/**
|
|
289
366
|
* Forget it, so the next load is a NEW session.
|
|
@@ -295,9 +372,7 @@ function writeStoredSessionId(platformUrl, sessionId) {
|
|
|
295
372
|
* @internal
|
|
296
373
|
*/
|
|
297
374
|
function clearStoredSessionId(platformUrl) {
|
|
298
|
-
|
|
299
|
-
globalThis.sessionStorage?.removeItem(keyFor(platformUrl));
|
|
300
|
-
} catch {}
|
|
375
|
+
storageRemove("session", keyFor(platformUrl));
|
|
301
376
|
}
|
|
302
377
|
//#endregion
|
|
303
378
|
//#region src/session-core-dial.ts
|
|
@@ -363,7 +438,6 @@ function createDialer(options) {
|
|
|
363
438
|
return (cfg?.sessionUrl ? buildBrokeredWsUrl(cfg.sessionUrl, hasConnected, sessionId) : buildWsUrl(options.platformUrl, hasConnected, sessionId)).toString();
|
|
364
439
|
}
|
|
365
440
|
return {
|
|
366
|
-
url,
|
|
367
441
|
open: () => {
|
|
368
442
|
if (options.WebSocket) return new options.WebSocket(buildWsUrl(options.platformUrl, hasConnected, sessionId).toString());
|
|
369
443
|
return openReconnectingSocket(url);
|
|
@@ -407,7 +481,8 @@ function createDialer(options) {
|
|
|
407
481
|
/** What the session reports once the budget below is spent. */
|
|
408
482
|
const HANDSHAKE_ERROR = {
|
|
409
483
|
code: "connection",
|
|
410
|
-
message: "Agent did not complete the session handshake"
|
|
484
|
+
message: "Agent did not complete the session handshake",
|
|
485
|
+
fatal: false
|
|
411
486
|
};
|
|
412
487
|
/** How long an OPEN socket may go without a `config` frame. */
|
|
413
488
|
const HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
@@ -507,7 +582,7 @@ function appendCapped(list, item, cap) {
|
|
|
507
582
|
*
|
|
508
583
|
* Encapsulates the per-session dedup counters (`customEventSeq`,
|
|
509
584
|
* `messageSeq`, `toolCallSeq`) that previously lived as closure locals in
|
|
510
|
-
* `
|
|
585
|
+
* `createBrowserSession`. The turn-boundary epoch is NOT one of them: it lives
|
|
511
586
|
* on `conn` because the session core bumps it on teardown too (see
|
|
512
587
|
* `ConnState.turn`).
|
|
513
588
|
*/
|
|
@@ -611,7 +686,8 @@ function createMessageHandlers(deps) {
|
|
|
611
686
|
console.error("Agent error:", e.message);
|
|
612
687
|
const error = {
|
|
613
688
|
code: e.code,
|
|
614
|
-
message: e.message
|
|
689
|
+
message: e.message,
|
|
690
|
+
fatal: e.fatal !== false
|
|
615
691
|
};
|
|
616
692
|
if (e.fatal === false) updateState(agentState.apply({
|
|
617
693
|
type: "TURN_ERROR",
|
|
@@ -625,8 +701,7 @@ function createMessageHandlers(deps) {
|
|
|
625
701
|
type: "FATAL",
|
|
626
702
|
error
|
|
627
703
|
}),
|
|
628
|
-
|
|
629
|
-
recording: false
|
|
704
|
+
...STOPPED
|
|
630
705
|
});
|
|
631
706
|
}
|
|
632
707
|
}
|
|
@@ -677,14 +752,12 @@ function createMessageHandlers(deps) {
|
|
|
677
752
|
toListening();
|
|
678
753
|
break;
|
|
679
754
|
case "reply.cancelled":
|
|
680
|
-
conn
|
|
681
|
-
conn.voiceIO?.flush();
|
|
755
|
+
bargeIn(conn);
|
|
682
756
|
commitAgentTranscript();
|
|
683
757
|
toListening({ userTranscript: null });
|
|
684
758
|
break;
|
|
685
759
|
case "session.reset": {
|
|
686
|
-
conn
|
|
687
|
-
conn.voiceIO?.flush();
|
|
760
|
+
bargeIn(conn);
|
|
688
761
|
const next = agentState.apply({ type: "RESET" });
|
|
689
762
|
updateState(agentState.fatal() ? next : {
|
|
690
763
|
...CLEARED_SESSION_STATE,
|
|
@@ -1000,27 +1073,27 @@ function createSessionStateMachine() {
|
|
|
1000
1073
|
* Uses a subscribe/getSnapshot pattern for state management, compatible with
|
|
1001
1074
|
* React's `useSyncExternalStore` and other external store integrations.
|
|
1002
1075
|
*
|
|
1003
|
-
* Most clients never call this: `
|
|
1076
|
+
* Most clients never call this: `mountClient()` creates a core and installs it in
|
|
1004
1077
|
* React context for the hooks. Reach for it directly when building a
|
|
1005
1078
|
* non-React UI (or wiring the session into another framework's store).
|
|
1006
1079
|
*
|
|
1007
1080
|
* @example
|
|
1008
1081
|
* ```ts
|
|
1009
|
-
* import {
|
|
1082
|
+
* import { createBrowserSession, type SessionSnapshot } from "@alexkroman1/aai-ui";
|
|
1010
1083
|
*
|
|
1011
1084
|
* declare function render(snapshot: SessionSnapshot): void;
|
|
1012
1085
|
*
|
|
1013
|
-
* const session =
|
|
1086
|
+
* const session = createBrowserSession({ platformUrl: "https://host/my-agent/" });
|
|
1014
1087
|
* session.subscribe(() => render(session.getSnapshot()));
|
|
1015
1088
|
* session.start();
|
|
1016
1089
|
* ```
|
|
1017
1090
|
*
|
|
1018
1091
|
* @param options - Session configuration including the platform server URL.
|
|
1019
|
-
* @returns A {@link
|
|
1092
|
+
* @returns A {@link BrowserSession} handle for controlling the session.
|
|
1020
1093
|
*
|
|
1021
1094
|
* @public
|
|
1022
1095
|
*/
|
|
1023
|
-
function
|
|
1096
|
+
function createBrowserSession(options) {
|
|
1024
1097
|
let currentSnapshot = {
|
|
1025
1098
|
...CLEARED_SESSION_STATE,
|
|
1026
1099
|
state: "disconnected",
|
|
@@ -1192,8 +1265,7 @@ function createSessionCore(options) {
|
|
|
1192
1265
|
type: "FAILED",
|
|
1193
1266
|
error: HANDSHAKE_ERROR
|
|
1194
1267
|
}),
|
|
1195
|
-
|
|
1196
|
-
recording: false
|
|
1268
|
+
...STOPPED
|
|
1197
1269
|
});
|
|
1198
1270
|
}
|
|
1199
1271
|
});
|
|
@@ -1232,24 +1304,22 @@ function createSessionCore(options) {
|
|
|
1232
1304
|
type: "FAILED",
|
|
1233
1305
|
error: {
|
|
1234
1306
|
code: "connection",
|
|
1235
|
-
message: failure
|
|
1307
|
+
message: failure,
|
|
1308
|
+
fatal: false
|
|
1236
1309
|
}
|
|
1237
1310
|
}),
|
|
1238
|
-
|
|
1239
|
-
recording: false
|
|
1311
|
+
...STOPPED
|
|
1240
1312
|
});
|
|
1241
1313
|
}, { signal: sig });
|
|
1242
1314
|
}
|
|
1243
1315
|
function cancel() {
|
|
1244
1316
|
if (!openSocket()) return;
|
|
1245
|
-
conn
|
|
1246
|
-
conn.voiceIO?.flush();
|
|
1317
|
+
bargeIn(conn);
|
|
1247
1318
|
updateState(agentState.apply({ type: "LISTEN" }));
|
|
1248
1319
|
sendJson({ type: "cancel" });
|
|
1249
1320
|
}
|
|
1250
1321
|
function reset() {
|
|
1251
|
-
conn
|
|
1252
|
-
conn.voiceIO?.flush();
|
|
1322
|
+
bargeIn(conn);
|
|
1253
1323
|
if (openSocket()) {
|
|
1254
1324
|
sendJson({ type: "reset" });
|
|
1255
1325
|
return;
|
|
@@ -1261,8 +1331,7 @@ function createSessionCore(options) {
|
|
|
1261
1331
|
teardownConnection();
|
|
1262
1332
|
updateState({
|
|
1263
1333
|
...agentState.apply({ type: "DISCONNECT" }),
|
|
1264
|
-
|
|
1265
|
-
recording: false
|
|
1334
|
+
...STOPPED
|
|
1266
1335
|
});
|
|
1267
1336
|
}
|
|
1268
1337
|
function start() {
|
|
@@ -1286,10 +1355,13 @@ function createSessionCore(options) {
|
|
|
1286
1355
|
...CLEARED_SESSION_STATE,
|
|
1287
1356
|
...agentState.apply({ type: "END" }),
|
|
1288
1357
|
started: false,
|
|
1289
|
-
|
|
1290
|
-
recording: false
|
|
1358
|
+
...STOPPED
|
|
1291
1359
|
});
|
|
1292
1360
|
}
|
|
1361
|
+
function restart() {
|
|
1362
|
+
end();
|
|
1363
|
+
start();
|
|
1364
|
+
}
|
|
1293
1365
|
return {
|
|
1294
1366
|
getSnapshot,
|
|
1295
1367
|
subscribe,
|
|
@@ -1301,10 +1373,11 @@ function createSessionCore(options) {
|
|
|
1301
1373
|
start,
|
|
1302
1374
|
toggle,
|
|
1303
1375
|
end,
|
|
1376
|
+
restart,
|
|
1304
1377
|
[Symbol.dispose]() {
|
|
1305
1378
|
disconnect();
|
|
1306
1379
|
}
|
|
1307
1380
|
};
|
|
1308
1381
|
}
|
|
1309
1382
|
//#endregion
|
|
1310
|
-
export {
|
|
1383
|
+
export { urlSlot as i, storageGet as n, storageSet as r, createBrowserSession as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionStateMachine } from "./session-core-state.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type ConnState, type SessionSnapshot } from "./session-core-types.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Snapshot fields cleared when a session's conversation state is wiped —
|
|
5
5
|
* shared by the initial snapshot, `resetState()`, and `session.reset`.
|
|
@@ -56,7 +56,7 @@ type MessageHandlers = {
|
|
|
56
56
|
*
|
|
57
57
|
* Encapsulates the per-session dedup counters (`customEventSeq`,
|
|
58
58
|
* `messageSeq`, `toolCallSeq`) that previously lived as closure locals in
|
|
59
|
-
* `
|
|
59
|
+
* `createBrowserSession`. The turn-boundary epoch is NOT one of them: it lives
|
|
60
60
|
* on `conn` because the session core bumps it on teardown too (see
|
|
61
61
|
* `ConnState.turn`).
|
|
62
62
|
*/
|
|
@@ -149,7 +149,7 @@ export type SessionSnapshot = {
|
|
|
149
149
|
*
|
|
150
150
|
* @public
|
|
151
151
|
*/
|
|
152
|
-
export type
|
|
152
|
+
export type BrowserSession = {
|
|
153
153
|
/** Return the current immutable state snapshot. */
|
|
154
154
|
getSnapshot(): SessionSnapshot;
|
|
155
155
|
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
@@ -195,6 +195,30 @@ export type SessionCore = {
|
|
|
195
195
|
* per-session tool state, greeting included.
|
|
196
196
|
*/
|
|
197
197
|
end(): void;
|
|
198
|
+
/**
|
|
199
|
+
* End the current call and immediately begin a new one — `end()` then
|
|
200
|
+
* `start()`, which is what "New Conversation" means for an agent that keeps
|
|
201
|
+
* SESSION-SCOPED STATE.
|
|
202
|
+
*
|
|
203
|
+
* `reset()` is the one whose name suggests this and it is not the same
|
|
204
|
+
* thing: it clears the transcript and reconnects, but the reconnect carries
|
|
205
|
+
* the same `?sessionId=`, so every `sessionSlot` on the server survives —
|
|
206
|
+
* the game world, the incident board, the cart. A caller who asked to start
|
|
207
|
+
* over gets a blank transcript in front of the old state. This drops the
|
|
208
|
+
* session id, so the next connect mints a fresh one and the greeting plays
|
|
209
|
+
* again.
|
|
210
|
+
*
|
|
211
|
+
* Three templates had each written `session.end(); session.start();` with
|
|
212
|
+
* the same paragraph explaining why `reset()` was wrong; the six on the
|
|
213
|
+
* stock shell could not, because {@link Controls} called `reset()` for them.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* declare const session: import("@alexkroman1/aai-ui").Session;
|
|
218
|
+
* session.restart();
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
restart(): void;
|
|
198
222
|
/** Alias for `disconnect` for use with `using`. */
|
|
199
223
|
[Symbol.dispose](): void;
|
|
200
224
|
};
|
|
@@ -247,3 +271,36 @@ export type ConnState = {
|
|
|
247
271
|
* buffered during mic-permission never finishes playing. */
|
|
248
272
|
preInitDone: boolean;
|
|
249
273
|
};
|
|
274
|
+
/**
|
|
275
|
+
* The two liveness fields at rest.
|
|
276
|
+
*
|
|
277
|
+
* `running` and `recording` ride with almost every state transition and were
|
|
278
|
+
* spread as a pair of literals at seven sites across three modules — the same
|
|
279
|
+
* shape `session-core-state.ts` folded `state` and `error` out of, one field
|
|
280
|
+
* short. Naming it relates them: a transition that ends the call says so once,
|
|
281
|
+
* and a reader looking for "what stops the mic" finds one symbol rather than a
|
|
282
|
+
* grep.
|
|
283
|
+
*
|
|
284
|
+
* It is deliberately NOT the whole snapshot patch — a transition still supplies
|
|
285
|
+
* its own `agentState.apply(...)` projection beside this.
|
|
286
|
+
*/
|
|
287
|
+
export declare const STOPPED: {
|
|
288
|
+
readonly running: false;
|
|
289
|
+
readonly recording: false;
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* A turn boundary: end the current turn and settle whatever it was playing.
|
|
293
|
+
*
|
|
294
|
+
* The two calls are one fact and were written out at four sites — `cancel()`
|
|
295
|
+
* and `reset()` here, `reply.cancelled` and `session.reset` on the server side
|
|
296
|
+
* — where the pair is load-bearing in both halves. The bump stops a stale drain
|
|
297
|
+
* continuation from stamping `"listening"` over a state the session has since
|
|
298
|
+
* moved to; the flush settles the interrupted turn's `done()` so it cannot
|
|
299
|
+
* strand.
|
|
300
|
+
*
|
|
301
|
+
* Two further sites bump WITHOUT flushing (`cleanupAudio`, a committed user
|
|
302
|
+
* transcript) and stay spelled out, which is the point of naming this one: a
|
|
303
|
+
* bump on its own now reads as a deliberate choice rather than a forgotten
|
|
304
|
+
* flush.
|
|
305
|
+
*/
|
|
306
|
+
export declare function bargeIn(conn: ConnState): void;
|
package/dist/session-core.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BrowserSession } from "./session-core-types.ts";
|
|
2
2
|
import { type VoiceSessionOptions } from "./types.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Create a framework-agnostic voice session core that connects to an AAI
|
|
@@ -7,24 +7,24 @@ import { type VoiceSessionOptions } from "./types.ts";
|
|
|
7
7
|
* Uses a subscribe/getSnapshot pattern for state management, compatible with
|
|
8
8
|
* React's `useSyncExternalStore` and other external store integrations.
|
|
9
9
|
*
|
|
10
|
-
* Most clients never call this: `
|
|
10
|
+
* Most clients never call this: `mountClient()` creates a core and installs it in
|
|
11
11
|
* React context for the hooks. Reach for it directly when building a
|
|
12
12
|
* non-React UI (or wiring the session into another framework's store).
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```ts
|
|
16
|
-
* import {
|
|
16
|
+
* import { createBrowserSession, type SessionSnapshot } from "@alexkroman1/aai-ui";
|
|
17
17
|
*
|
|
18
18
|
* declare function render(snapshot: SessionSnapshot): void;
|
|
19
19
|
*
|
|
20
|
-
* const session =
|
|
20
|
+
* const session = createBrowserSession({ platformUrl: "https://host/my-agent/" });
|
|
21
21
|
* session.subscribe(() => render(session.getSnapshot()));
|
|
22
22
|
* session.start();
|
|
23
23
|
* ```
|
|
24
24
|
*
|
|
25
25
|
* @param options - Session configuration including the platform server URL.
|
|
26
|
-
* @returns A {@link
|
|
26
|
+
* @returns A {@link BrowserSession} handle for controlling the session.
|
|
27
27
|
*
|
|
28
28
|
* @public
|
|
29
29
|
*/
|
|
30
|
-
export declare function
|
|
30
|
+
export declare function createBrowserSession(options: VoiceSessionOptions): BrowserSession;
|
package/dist/session-core.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
export {
|
|
1
|
+
import { t as createBrowserSession } from "./session-core-gwePM95B.js";
|
|
2
|
+
export { createBrowserSession };
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
* Keyed by the agent's own URL, so two agents served from one origin — which is
|
|
24
24
|
* every deployed agent, at `/:slug/` — cannot inherit each other's session.
|
|
25
25
|
*
|
|
26
|
-
* Every access is guarded
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Every access is guarded, and the guard lives in `_web-storage.ts`: a session
|
|
27
|
+
* that cannot be remembered must degrade to today's behaviour rather than
|
|
28
|
+
* failing to start.
|
|
29
29
|
*/
|
|
30
30
|
/** The stored session id for this agent, or undefined. @internal */
|
|
31
31
|
export declare function readStoredSessionId(platformUrl: string): string | undefined;
|