@alexkroman1/aai-ui 0.16.0 → 0.17.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 (86) hide show
  1. package/dist/agent-state-labels.d.ts +8 -3
  2. package/dist/{chat-view-DIwPoAEG.js → chat-view-W6KcdjJY.js} +40 -30
  3. package/dist/{client-config-C7fQrioM.js → client-config-CzvWmw4X.js} +7 -12
  4. package/dist/client-config.d.ts +29 -10
  5. package/dist/components/_form-readiness.d.ts +2 -1
  6. package/dist/components/audio-result.d.ts +2 -0
  7. package/dist/components/bullet-list.d.ts +1 -0
  8. package/dist/components/button.d.ts +2 -2
  9. package/dist/components/chat-view.js +1 -1
  10. package/dist/components/console-shell.d.ts +8 -5
  11. package/dist/components/controls.d.ts +1 -0
  12. package/dist/components/conversation-view.d.ts +1 -0
  13. package/dist/components/facts.d.ts +1 -0
  14. package/dist/components/form-fields.d.ts +8 -8
  15. package/dist/components/form-types.d.ts +16 -2
  16. package/dist/components/form.d.ts +9 -4
  17. package/dist/components/markdown.d.ts +1 -0
  18. package/dist/components/message-list.d.ts +1 -0
  19. package/dist/components/message-list.js +1 -1
  20. package/dist/components/session-controls.d.ts +7 -2
  21. package/dist/components/session-error-banner.d.ts +1 -0
  22. package/dist/components/session-state-dot.d.ts +20 -7
  23. package/dist/components/sidebar-layout.d.ts +2 -2
  24. package/dist/components/tool-call-block.d.ts +3 -3
  25. package/dist/components/tool-call-row.d.ts +1 -0
  26. package/dist/components/tool-config-context.d.ts +2 -1
  27. package/dist/components/url-chips.d.ts +4 -3
  28. package/dist/components/workflow-fields.d.ts +2 -1
  29. package/dist/components/workflow-pending-note.d.ts +1 -0
  30. package/dist/components/workflow-run-error.d.ts +1 -0
  31. package/dist/components/workflow-run-panel.d.ts +1 -0
  32. package/dist/context.d.ts +8 -7
  33. package/dist/context.js +2 -2
  34. package/dist/default-client/assets/{audio-DzwGxdv4.js → audio-Ct7JpSNT.js} +1 -1
  35. package/dist/default-client/assets/{capture-processor-CXCWxCO8.js → capture-processor-Cx1AdMHL.js} +1 -1
  36. package/dist/default-client/assets/index-BlUYCTB5.js +126 -0
  37. package/dist/default-client/assets/{playback-processor-DXHfyQD9.js → playback-processor-CCwTknSu.js} +1 -1
  38. package/dist/default-client/index.html +1 -1
  39. package/dist/define-client.d.ts +2 -0
  40. package/dist/define-client.js +4 -4
  41. package/dist/inbox-protocol.d.ts +14 -4
  42. package/dist/inbox.d.ts +1 -0
  43. package/dist/index.d.ts +3 -3
  44. package/dist/index.js +61 -19
  45. package/dist/internal.js +1 -1
  46. package/dist/linked-client.d.ts +2 -0
  47. package/dist/{message-list-DHZcLhRA.js → message-list-BsaBkBgo.js} +21 -10
  48. package/dist/page.d.ts +2 -0
  49. package/dist/phone.d.ts +1 -0
  50. package/dist/route-fetch.d.ts +1 -0
  51. package/dist/session/audio-setup.d.ts +1 -1
  52. package/dist/session/audio-state.d.ts +6 -6
  53. package/dist/session/index.js +1 -1
  54. package/dist/session/messages.d.ts +1 -9
  55. package/dist/session/preconnect.d.ts +1 -1
  56. package/dist/session/types.d.ts +4 -0
  57. package/dist/{session-CYdjRvbl.js → session-3lgPYNNO.js} +25 -14
  58. package/dist/stored-value.d.ts +2 -0
  59. package/dist/types.d.ts +48 -4
  60. package/dist/use-client-runs.d.ts +2 -0
  61. package/dist/use-conversation-log.d.ts +2 -0
  62. package/dist/use-conversation.d.ts +24 -9
  63. package/dist/use-copy.d.ts +1 -0
  64. package/dist/use-download-url.d.ts +6 -1
  65. package/dist/use-flash.d.ts +1 -0
  66. package/dist/use-inbox.d.ts +2 -0
  67. package/dist/use-push-to-talk.d.ts +6 -1
  68. package/dist/use-route-mutation.d.ts +3 -0
  69. package/dist/use-route.d.ts +2 -0
  70. package/dist/use-session-controls.d.ts +1 -0
  71. package/dist/use-tap-to-talk.d.ts +5 -2
  72. package/dist/use-user-transcript.d.ts +5 -1
  73. package/dist/use-workflow-form.d.ts +8 -1
  74. package/dist/use-workflow-progress.d.ts +5 -0
  75. package/dist/use-workflow-run.d.ts +5 -0
  76. package/dist/use-workflow-runs.d.ts +10 -2
  77. package/dist/use-workflow-stream.d.ts +25 -3
  78. package/dist/use-workflows.d.ts +10 -2
  79. package/dist/workflow-client.d.ts +5 -0
  80. package/dist/workflow-field-kind.d.ts +8 -4
  81. package/dist/worklets/capture-processor.d.ts +3 -3
  82. package/dist/worklets/capture-processor.js +2 -2
  83. package/dist/worklets/playback-processor.d.ts +3 -3
  84. package/dist/worklets/playback-processor.js +2 -2
  85. package/package.json +2 -2
  86. package/dist/default-client/assets/index-BOTLwsNO.js +0 -126
package/dist/types.d.ts CHANGED
@@ -56,9 +56,13 @@ export declare const VOICE_CAPTURE_CONSTRAINTS: MediaTrackConstraints;
56
56
  * until the next completed handshake, so a later frame cannot quietly paint
57
57
  * over the banner explaining a dead call.
58
58
  *
59
+ * The set is OPEN (`| (string & {})`): a later release may add a state, so a
60
+ * `switch` over it needs a `default`, and a lookup keyed by it needs a
61
+ * fallback for a state it has no entry for.
62
+ *
59
63
  * @public
60
64
  */
61
- export type AgentState = "disconnected" | "connecting" | "ready" | "listening" | "thinking" | "speaking" | "error";
65
+ export type AgentState = "disconnected" | "connecting" | "ready" | "listening" | "thinking" | "speaking" | "error" | (string & {});
62
66
  /**
63
67
  * A chat message exchanged between user and assistant.
64
68
  *
@@ -67,6 +71,7 @@ export type AgentState = "disconnected" | "connecting" | "ready" | "listening" |
67
71
  * surfaced via `SessionSnapshot.toolCalls` (or `useEvent` for `ctx.send`
68
72
  * events).
69
73
  *
74
+ * @sealed
70
75
  * @public
71
76
  */
72
77
  export type ChatMessage = {
@@ -76,14 +81,19 @@ export type ChatMessage = {
76
81
  * render key.
77
82
  */
78
83
  id: number;
79
- /** The sender of the message. */
80
- role: "user" | "assistant";
84
+ /**
85
+ * The sender of the message. Open (`| (string & {})`) so a later sender is
86
+ * not a break: render anything that is not `"user"` the way you render
87
+ * `"assistant"`, or skip it.
88
+ */
89
+ role: "user" | "assistant" | (string & {});
81
90
  /** The text content of the message. */
82
91
  content: string;
83
92
  };
84
93
  /**
85
94
  * Info about a tool call for display in the UI.
86
95
  *
96
+ * @sealed
87
97
  * @public
88
98
  */
89
99
  export type ToolCallInfo = {
@@ -105,7 +115,13 @@ export type ToolCallInfo = {
105
115
  * now actually compiles.
106
116
  */
107
117
  args: Record<string, DefaultToolResult>;
108
- status: "pending" | "done";
118
+ /**
119
+ * `"pending"` until the tool returns, then `"done"`. A tool that FAILED is
120
+ * `"done"` too: the wire reports a failure as a completion whose `result` is
121
+ * the error text the model read. Open (`| (string & {})`) so a later status
122
+ * is not a break — treat anything but `"pending"` as settled.
123
+ */
124
+ status: "pending" | "done" | (string & {});
109
125
  result?: string | undefined;
110
126
  /**
111
127
  * Monotonically increasing, session-unique insertion sequence number.
@@ -128,6 +144,7 @@ export type { SessionErrorCode } from "@alexkroman1/aai/protocol";
128
144
  /**
129
145
  * Error reported by the voice session.
130
146
  *
147
+ * @sealed
131
148
  * @public
132
149
  */
133
150
  export type SessionError = {
@@ -157,6 +174,7 @@ export type SessionError = {
157
174
  * defaults `platformUrl` from `location.href`, while `createBrowserSession`
158
175
  * requires it.
159
176
  *
177
+ * @input
160
178
  * @public
161
179
  */
162
180
  export type VoiceSessionOptions = {
@@ -314,6 +332,7 @@ export type VoiceSessionOptions = {
314
332
  /**
315
333
  * Minimal WebSocket constructor type accepted by {@link VoiceSessionOptions}.
316
334
  *
335
+ * @input
317
336
  * @public
318
337
  */
319
338
  export type WebSocketConstructor = {
@@ -323,6 +342,7 @@ export type WebSocketConstructor = {
323
342
  /**
324
343
  * Theme color overrides for the AAI UI components.
325
344
  *
345
+ * @input
326
346
  * @public
327
347
  */
328
348
  export type ClientTheme = {
@@ -337,3 +357,27 @@ export type ClientTheme = {
337
357
  /** Border color. Default: `#E4E4E7`. */
338
358
  border?: string;
339
359
  };
360
+ /**
361
+ * The theme a component reads back from `useTheme()`: every
362
+ * {@link ClientTheme} field, filled with its default where the client set
363
+ * none.
364
+ *
365
+ * Its own type rather than `Required<ClientTheme>`, because the two travel in
366
+ * opposite directions — an author BUILDS a `ClientTheme` and only READS this —
367
+ * so a token added later can be optional on one and present on the other.
368
+ *
369
+ * @sealed
370
+ * @public
371
+ */
372
+ export type ResolvedClientTheme = {
373
+ /** Background color, also painted on `html`/`body`. */
374
+ readonly bg: string;
375
+ /** Primary accent color. */
376
+ readonly primary: string;
377
+ /** Main text color. */
378
+ readonly text: string;
379
+ /** Surface/card color. */
380
+ readonly surface: string;
381
+ /** Border color. */
382
+ readonly border: string;
383
+ };
@@ -15,6 +15,7 @@ import type { ClientRun } from "@alexkroman1/aai";
15
15
  /**
16
16
  * Options for {@link useClientRuns}.
17
17
  *
18
+ * @input
18
19
  * @public
19
20
  */
20
21
  export type UseClientRunsOptions = {
@@ -29,6 +30,7 @@ export type UseClientRunsOptions = {
29
30
  /**
30
31
  * What {@link useClientRuns} returns.
31
32
  *
33
+ * @sealed
32
34
  * @public
33
35
  */
34
36
  export type UseClientRunsResult = {
@@ -23,6 +23,7 @@ import type { InboxEvent } from "./inbox-protocol.ts";
23
23
  /**
24
24
  * Options for {@link useConversationLog}.
25
25
  *
26
+ * @input
26
27
  * @public
27
28
  */
28
29
  export type UseConversationLogOptions = {
@@ -37,6 +38,7 @@ export type UseConversationLogOptions = {
37
38
  /**
38
39
  * What {@link useConversationLog} returns.
39
40
  *
41
+ * @sealed
40
42
  * @public
41
43
  */
42
44
  export type UseConversationLogResult = {
@@ -33,6 +33,12 @@ import { type UseUserTranscriptResult } from "./use-user-transcript.ts";
33
33
  * is the thing this hook computes — handing back two lists would hand back the
34
34
  * problem. `kind` is what a `switch` in a custom renderer narrows on.
35
35
  *
36
+ * `"other"` is the row this version cannot describe — today, an item a newer
37
+ * page wrote into a stored conversation log (`type` is the `kind` it was
38
+ * stored under). It is a member from the start so that a row kind added
39
+ * later is a value an existing `switch` already has a branch for, not a
40
+ * compile error: render nothing for it, or a placeholder.
41
+ *
36
42
  * @public
37
43
  */
38
44
  export type ConversationItem = {
@@ -41,10 +47,14 @@ export type ConversationItem = {
41
47
  } | {
42
48
  kind: "tool";
43
49
  toolCall: ToolCallInfo;
50
+ } | {
51
+ kind: "other";
52
+ type: string;
44
53
  };
45
54
  /**
46
55
  * What {@link useConversation} returns.
47
56
  *
57
+ * @sealed
48
58
  * @public
49
59
  */
50
60
  export type UseConversationResult = {
@@ -107,15 +117,20 @@ export declare function interleave(messages: readonly ChatMessage[], toolCalls:
107
117
  * const { items, streaming, transcript, thinking } = useConversation();
108
118
  * return (
109
119
  * <div>
110
- * {items.map((item) =>
111
- * item.kind === "message" ? (
112
- * <p key={item.message.id} data-role={item.message.role}>
113
- * {item.message.content}
114
- * </p>
115
- * ) : (
116
- * <code key={item.toolCall.callId}>{item.toolCall.name}</code>
117
- * ),
118
- * )}
120
+ * {items.map((item) => {
121
+ * switch (item.kind) {
122
+ * case "message":
123
+ * return (
124
+ * <p key={item.message.id} data-role={item.message.role}>
125
+ * {item.message.content}
126
+ * </p>
127
+ * );
128
+ * case "tool":
129
+ * return <code key={item.toolCall.callId}>{item.toolCall.name}</code>;
130
+ * default:
131
+ * return null;
132
+ * }
133
+ * })}
119
134
  * {streaming !== null && <p data-role="assistant">{streaming}</p>}
120
135
  * {transcript.speaking && <p data-role="user">{transcript.text}</p>}
121
136
  * {thinking && <p>…</p>}
@@ -17,6 +17,7 @@
17
17
  * What {@link useCopy} hands back — the click handler and the two readings a
18
18
  * button needs off one shared flash.
19
19
  *
20
+ * @sealed
20
21
  * @public
21
22
  */
22
23
  export type UseCopyResult = {
@@ -23,7 +23,11 @@
23
23
  * "downloading" as the same thing — so this reports it.
24
24
  */
25
25
  import type { WorkflowApi } from "./workflow-client.ts";
26
- /** Options for {@link useDownloadUrl}. */
26
+ /**
27
+ * Options for {@link useDownloadUrl}.
28
+ *
29
+ * @input
30
+ */
27
31
  export type UseDownloadUrlOptions = {
28
32
  /** The client to read the bytes with. Defaults to one for the page's own agent. */
29
33
  api?: WorkflowApi;
@@ -31,6 +35,7 @@ export type UseDownloadUrlOptions = {
31
35
  /**
32
36
  * What {@link useDownloadUrl} reports.
33
37
  *
38
+ * @sealed
34
39
  * @public
35
40
  */
36
41
  export type UseDownloadUrlResult = {
@@ -15,6 +15,7 @@
15
15
  * @typeParam T - What is being flashed. A `string` for a label; a record for a
16
16
  * flash that has to say WHICH thing it belongs to, as `useCopy` does.
17
17
  *
18
+ * @sealed
18
19
  * @public
19
20
  */
20
21
  export type UseFlashResult<T> = {
@@ -33,6 +33,7 @@ import type { InboxEvent, InboxNotice } from "./inbox-protocol.ts";
33
33
  /**
34
34
  * Options for {@link useInbox}.
35
35
  *
36
+ * @input
36
37
  * @public
37
38
  */
38
39
  export type UseInboxOptions = {
@@ -63,6 +64,7 @@ export type UseInboxOptions = {
63
64
  /**
64
65
  * What {@link useInbox} returns.
65
66
  *
67
+ * @sealed
66
68
  * @public
67
69
  */
68
70
  export type UseInboxResult = {
@@ -21,7 +21,11 @@
21
21
  * And one that is a nuisance rather than a stuck turn: the hold key is ignored
22
22
  * while focus is in a text field, where Space is a character.
23
23
  */
24
- /** Options for {@link usePushToTalk}. */
24
+ /**
25
+ * Options for {@link usePushToTalk}.
26
+ *
27
+ * @input
28
+ */
25
29
  export type UsePushToTalkOptions = {
26
30
  /**
27
31
  * The keyboard key that holds the turn open, as a `KeyboardEvent.code` —
@@ -34,6 +38,7 @@ export type UsePushToTalkOptions = {
34
38
  /**
35
39
  * What {@link usePushToTalk} returns.
36
40
  *
41
+ * @sealed
37
42
  * @public
38
43
  */
39
44
  export type UsePushToTalkResult = {
@@ -26,6 +26,7 @@ import { type RouteMethod } from "./route-fetch.ts";
26
26
  /**
27
27
  * Options for {@link useRouteMutation}.
28
28
  *
29
+ * @input
29
30
  * @public
30
31
  */
31
32
  export type UseRouteMutationOptions = {
@@ -45,6 +46,7 @@ export type UseRouteMutationOptions = {
45
46
  /**
46
47
  * Options for one {@link UseRouteMutationResult.run} call.
47
48
  *
49
+ * @input
48
50
  * @public
49
51
  */
50
52
  export type RouteMutationRunOptions = {
@@ -58,6 +60,7 @@ export type RouteMutationRunOptions = {
58
60
  /**
59
61
  * What {@link useRouteMutation} returns.
60
62
  *
63
+ * @sealed
61
64
  * @public
62
65
  */
63
66
  export type UseRouteMutationResult = {
@@ -13,6 +13,7 @@
13
13
  /**
14
14
  * Options for {@link useRoute}.
15
15
  *
16
+ * @input
16
17
  * @public
17
18
  */
18
19
  export type UseRouteOptions = {
@@ -28,6 +29,7 @@ export type UseRouteOptions = {
28
29
  /**
29
30
  * What {@link useRoute} returns.
30
31
  *
32
+ * @sealed
31
33
  * @public
32
34
  */
33
35
  export type UseRouteResult<T> = {
@@ -14,6 +14,7 @@
14
14
  /**
15
15
  * What {@link useSessionControls} returns.
16
16
  *
17
+ * @sealed
17
18
  * @public
18
19
  */
19
20
  export type UseSessionControlsResult = {
@@ -31,6 +31,7 @@
31
31
  /**
32
32
  * Options for {@link useTapToTalk}.
33
33
  *
34
+ * @input
34
35
  * @public
35
36
  */
36
37
  export type UseTapToTalkOptions = {
@@ -56,14 +57,16 @@ export type UseTapToTalkOptions = {
56
57
  /**
57
58
  * What {@link useTapToTalk} returns.
58
59
  *
60
+ * @sealed
59
61
  * @public
60
62
  */
61
63
  export type UseTapToTalkResult = {
62
64
  /**
63
65
  * The call: `"idle"` (not meant to be running), `"connecting"`, or
64
- * `"active"` — up, whatever the agent is doing.
66
+ * `"active"` — up, whatever the agent is doing. Open (`| (string & {})`) so
67
+ * a phase added later is not a break.
65
68
  */
66
- phase: "idle" | "connecting" | "active";
69
+ phase: "idle" | "connecting" | "active" | (string & {});
67
70
  /** Whether the caller is live — mic open, in a realtime conversation. The button is ON. */
68
71
  live: boolean;
69
72
  /**
@@ -33,7 +33,11 @@
33
33
  * @internal
34
34
  */
35
35
  export declare const TRANSCRIBING_PLACEHOLDER = "\u2026";
36
- /** What {@link useUserTranscript} returns. */
36
+ /**
37
+ * What {@link useUserTranscript} returns.
38
+ *
39
+ * @sealed
40
+ */
37
41
  export interface UseUserTranscriptResult {
38
42
  /**
39
43
  * True while the caller holds the turn — from speech detection to the final
@@ -42,6 +42,7 @@ import type { SubmitInputOf } from "./workflow-def-types.ts";
42
42
  * because a form is allowed more than one and a bar over "the upload" would
43
43
  * restart at zero partway through with nothing to say why.
44
44
  *
45
+ * @sealed
45
46
  * @public
46
47
  */
47
48
  export type UploadStatus = UploadProgress & {
@@ -72,6 +73,8 @@ export type UploadStatus = UploadProgress & {
72
73
  * the run is accepted, and `run` is non-`undefined` from before the bytes are
73
74
  * in, so a page can render `<WorkflowProgress>` beside the upload bar instead
74
75
  * of after it. Here the run does not exist until the last byte lands.
76
+ *
77
+ * @sealed
75
78
  */
76
79
  export type WorkflowSubmission<R = unknown, I = unknown> = {
77
80
  /**
@@ -190,7 +193,11 @@ export type WorkflowSubmission<R = unknown, I = unknown> = {
190
193
  /** The submit's own failure (a rejected input), or the watch's. */
191
194
  error: string | undefined;
192
195
  };
193
- /** Options for {@link useWorkflowSubmit}. */
196
+ /**
197
+ * Options for {@link useWorkflowSubmit}.
198
+ *
199
+ * @input
200
+ */
194
201
  export type UseWorkflowSubmitOptions = {
195
202
  /** The client to start runs with. Defaults to one for the page's own agent. */
196
203
  api?: WorkflowApi;
@@ -53,6 +53,11 @@
53
53
  import type { WorkflowApi } from "./workflow-client.ts";
54
54
  /** The slice of the client this needs: one method. */
55
55
  export type RunProgressReader = Pick<WorkflowApi, "streamOutput">;
56
+ /**
57
+ * What {@link useWorkflowProgress} reports.
58
+ *
59
+ * @public @sealed
60
+ */
56
61
  export type UseWorkflowProgressResult<T = string> = {
57
62
  /** Every chunk the run has written, oldest first. */
58
63
  progress: T[];
@@ -23,6 +23,11 @@ export declare const DEFAULT_WORKFLOW_POLL_MS = 2000;
23
23
  * tab is open.
24
24
  */
25
25
  export declare const MAX_MISSING_READS = 3;
26
+ /**
27
+ * What {@link useWorkflowRun} reports.
28
+ *
29
+ * @public @sealed
30
+ */
26
31
  export type UseWorkflowRunResult<R = unknown> = {
27
32
  /** Latest snapshot, or undefined before the first read lands. */
28
33
  run: WorkflowRun<R> | undefined;
@@ -21,7 +21,11 @@
21
21
  * stale.
22
22
  */
23
23
  import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
24
- /** Options for {@link useWorkflowRuns}. */
24
+ /**
25
+ * Options for {@link useWorkflowRuns}.
26
+ *
27
+ * @input
28
+ */
25
29
  export type UseWorkflowRunsOptions = {
26
30
  /** The client to read with. Defaults to one for the page's own agent. */
27
31
  api?: WorkflowApi;
@@ -39,7 +43,11 @@ export type UseWorkflowRunsOptions = {
39
43
  /** Skip the read entirely — for a page that does not know its workflow yet. */
40
44
  skip?: boolean;
41
45
  };
42
- /** What {@link useWorkflowRuns} reports. */
46
+ /**
47
+ * What {@link useWorkflowRuns} reports.
48
+ *
49
+ * @sealed
50
+ */
43
51
  export type UseWorkflowRunsResult<R = unknown> = {
44
52
  /** The runs, newest first. Empty until the first read lands. */
45
53
  runs: WorkflowRun<R>[];
@@ -76,8 +76,9 @@
76
76
  * `stream.ts`'s idle bound (five minutes of no new bytes) is what decides that a
77
77
  * pause has become an abandonment.
78
78
  */
79
- import type { AnyWorkflowDef, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
80
- import type { UseWorkflowSubmitOptions, WorkflowSubmission } from "./use-workflow-form.ts";
79
+ import type { AnyWorkflowDef, UploadParallelOption, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
80
+ import type { WorkflowSubmission } from "./use-workflow-form.ts";
81
+ import type { WorkflowApi } from "./workflow-client.ts";
81
82
  import type { SubmitInputOf } from "./workflow-def-types.ts";
82
83
  /**
83
84
  * Options for {@link useWorkflowStream}.
@@ -103,8 +104,29 @@ import type { SubmitInputOf } from "./workflow-def-types.ts";
103
104
  * the file is readable — that number is the CONTIGUOUS prefix, so a run reading
104
105
  * ahead of the uplink sees the same growing file whether one connection or four
105
106
  * are filling it. What changes is only how fast it grows.
107
+ *
108
+ * Declared on its own rather than `Omit`ted from the submit options, so an
109
+ * option added to one hook is a decision about the other, not a side effect.
110
+ *
111
+ * @input
106
112
  */
107
- export type UseWorkflowStreamOptions = Omit<UseWorkflowSubmitOptions, "wait" | "recover">;
113
+ export type UseWorkflowStreamOptions = {
114
+ /** The client to start runs with. Defaults to one for the page's own agent. */
115
+ api?: WorkflowApi;
116
+ /**
117
+ * Correlation key recorded with the run, for finding it again without the
118
+ * id. Not defaulted here — see above.
119
+ */
120
+ key?: string;
121
+ /** How often the fallback poll re-reads a live run. */
122
+ intervalMs?: number;
123
+ /**
124
+ * Send the file as concurrent parts instead of in one request — on by
125
+ * default, `false` opts out, `{ partBytes, concurrency }` tunes it. See
126
+ * `UploadOptions.parallel`.
127
+ */
128
+ parallel?: UploadParallelOption;
129
+ };
108
130
  /**
109
131
  * What {@link useWorkflowStream} returns: a {@link WorkflowSubmission}, exactly.
110
132
  *
@@ -9,7 +9,11 @@
9
9
  */
10
10
  import type { WorkflowSummary } from "@alexkroman1/aai/workflow-api";
11
11
  import type { WorkflowApi } from "./workflow-client.ts";
12
- /** Options for {@link useWorkflows}. */
12
+ /**
13
+ * Options for {@link useWorkflows}.
14
+ *
15
+ * @input
16
+ */
13
17
  export type UseWorkflowsOptions = {
14
18
  /** The client to read the listing with. Defaults to one for the page's own agent. */
15
19
  api?: WorkflowApi;
@@ -23,7 +27,11 @@ export type UseWorkflowsOptions = {
23
27
  */
24
28
  skip?: boolean;
25
29
  };
26
- /** What {@link useWorkflows} reports. */
30
+ /**
31
+ * What {@link useWorkflows} reports.
32
+ *
33
+ * @sealed
34
+ */
27
35
  export type UseWorkflowsResult = {
28
36
  /** The agent's declared workflows, each with the JSON Schema of its input. */
29
37
  workflows: WorkflowSummary[];
@@ -68,6 +68,11 @@ export type { AgentClient, WorkflowApi, WorkflowInputOf, WorkflowOutputOf, Workf
68
68
  * status predicate beside the status union cannot have.
69
69
  */
70
70
  export { isTerminal } from "@alexkroman1/aai/workflow-api";
71
+ /**
72
+ * Options for {@link createWorkflowApi}.
73
+ *
74
+ * @public @input
75
+ */
71
76
  export type WorkflowApiOptions = {
72
77
  /**
73
78
  * Base URL of the agent. Defaults to the page's own origin + path, which is
@@ -18,9 +18,10 @@
18
18
  * studio's SDK-subpath list: mirroring "would just move the drift somewhere
19
19
  * new".
20
20
  *
21
- * So a NEW control added to `<WorkflowFields>` is a new member of
22
- * {@link WorkflowFieldKind}, and every reader of the rule finds out from the
23
- * type rather than from a table somebody remembered to edit.
21
+ * So a NEW control added to `<WorkflowFields>` is a new value of
22
+ * {@link WorkflowFieldKind}. The set is open, so that addition is not a break:
23
+ * a reader's `switch` keeps a `default` and treats a kind it does not know the
24
+ * way it treats `"none"`.
24
25
  */
25
26
  /**
26
27
  * The control a schema property renders as inside `<WorkflowFields>`.
@@ -37,9 +38,12 @@
37
38
  * text field is — the same value over the wire — so it is a hand-written swap
38
39
  * rather than a shape the schema can ask for.
39
40
  *
41
+ * Open (`| (string & {})`): a control added later is a new value, so read an
42
+ * unknown kind as `"none"`.
43
+ *
40
44
  * @public
41
45
  */
42
- export type WorkflowFieldKind = "text" | "number" | "select" | "checkbox" | "file" | "none";
46
+ export type WorkflowFieldKind = "text" | "number" | "select" | "checkbox" | "file" | "none" | (string & {});
43
47
  /**
44
48
  * A property's declared type, taking the first non-null member of a union.
45
49
  *
@@ -1,4 +1,4 @@
1
1
  /** Raw worklet source — exported so tests can evaluate the processor directly. */
2
- export declare const captureProcessorSource = "\nclass CaptureProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n this.recording = false;\n const opts = options.processorOptions || {};\n // The context runs at the STT rate, so this is both the input and the\n // output rate \u2014 there is nothing to convert.\n this.rate = opts.sampleRate || sampleRate;\n // Int16 accumulation buffer: flushed to the main thread as one transferred\n // ArrayBuffer once ~bufferSeconds of samples are batched. Sized 2x the\n // flush target so a whole render quantum always fits before flushing.\n this.targetSamples = Math.max(1, Math.round(this.rate * (opts.bufferSeconds || 0.1)));\n this.pending = new Int16Array(this.targetSamples * 2);\n this.pendingLen = 0;\n // Dead-mic probe: samples left to inspect before concluding the device\n // delivers nothing but digital silence. Only consumed while recording, so\n // the cost disappears after the window (or after the first real sample).\n this.probeSamplesLeft = Math.round(\n (this.rate * (opts.silenceProbeMs ?? 1500)) / 1000,\n );\n this.port.onmessage = (e) => {\n if (e.data.event === 'start') this.recording = true;\n else if (e.data.event === 'stop') {\n // Final flush so the tail of speech isn't dropped on close, then ack\n // so the host knows the tail chunk (if any) has been posted and it is\n // safe to tear the context down.\n this.flush();\n this.recording = false;\n this.port.postMessage({ event: 'stopped' });\n }\n };\n }\n\n // Convert Float32 -> Int16 and append to the pending batch. Writes through\n // an Int16Array directly (assignment truncates like DataView.setInt16).\n accumulate(samples) {\n let buf = this.pending;\n if (this.pendingLen + samples.length > buf.length) {\n // Defensive: only reachable if a render quantum outproduces the 1x\n // headroom above the flush target (never with 128-sample quanta).\n const grown = new Int16Array((this.pendingLen + samples.length) * 2);\n grown.set(buf.subarray(0, this.pendingLen));\n this.pending = grown;\n buf = grown;\n }\n for (let i = 0; i < samples.length; i++) {\n const s = Math.max(-1, Math.min(1, samples[i]));\n buf[this.pendingLen++] = s < 0 ? s * 0x8000 : s * 0x7fff;\n }\n }\n\n // Post the batched samples as one transferred ArrayBuffer and reset.\n flush() {\n if (this.pendingLen === 0) return;\n const buffer = this.pending.buffer.slice(0, this.pendingLen * 2);\n this.pendingLen = 0;\n this.port.postMessage({ event: 'chunk', buffer }, [buffer]);\n }\n\n // Watch the first window of input for any nonzero sample. One is enough to\n // prove the device is live \u2014 a real mic in a quiet room still carries a\n // noise floor, so all-zeros means muted, wrong input, or no input at all.\n probeForSilence(channel) {\n for (let i = 0; i < channel.length; i++) {\n if (channel[i] !== 0) {\n this.probeSamplesLeft = 0;\n return;\n }\n }\n this.probeSamplesLeft -= channel.length;\n if (this.probeSamplesLeft <= 0) {\n this.port.postMessage({ event: 'silent' });\n }\n }\n\n process(inputs) {\n const input = inputs[0];\n if (!input || !input[0] || !this.recording) return true;\n\n if (this.probeSamplesLeft > 0) this.probeForSilence(input[0]);\n\n this.accumulate(input[0]);\n if (this.pendingLen >= this.targetSamples) this.flush();\n return true;\n }\n}\n\nregisterProcessor('capture-processor', CaptureProcessor);\n";
3
- declare const _default: string;
4
- export default _default;
2
+ export declare const captureProcessorSource: string;
3
+ declare const captureProcessorUrl: string;
4
+ export default captureProcessorUrl;
@@ -92,6 +92,6 @@ class CaptureProcessor extends AudioWorkletProcessor {
92
92
 
93
93
  registerProcessor('capture-processor', CaptureProcessor);
94
94
  `;
95
- var capture_processor_default = workletModuleUrl(captureProcessorSource);
95
+ const captureProcessorUrl = workletModuleUrl(captureProcessorSource);
96
96
  //#endregion
97
- export { captureProcessorSource, capture_processor_default as default };
97
+ export { captureProcessorSource, captureProcessorUrl as default };
@@ -1,4 +1,4 @@
1
1
  /** Raw worklet source — exported so tests can evaluate the processor directly. */
2
- export declare const playbackProcessorSource = "\nclass PlaybackProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n const opts = options.processorOptions || {};\n // The node's context IS the playback context (audio/voice-io.ts asserts its rate),\n // so the worklet-global sampleRate is authoritative; the option exists\n // for the node-less test harness.\n const rate = opts.sampleRate ?? sampleRate;\n // Kept, because every derived quantity below reads it and `bufferedMs` in\n // process() is the one that used to read the worklet global instead. Those\n // agree in production (the option is unset, so `rate` IS `sampleRate`) and\n // diverge only in the node-less harness \u2014 which is the one place a spec\n // could ever assert on the reported backlog, so the divergence made the\n // number untestable rather than wrong.\n this.rate = rate;\n // The one fill target \u2014 for the start of a turn and for recovery after an\n // underrun alike. If 'done' arrives first (short utterance), start\n // immediately instead of waiting for audio that is never coming.\n this.fillSamples = Math.floor((rate * (opts.fillMs ?? 200)) / 1000);\n // Concealment source: a ring of the most recently played samples, looped\n // under a decaying gain to cover a gap. Sized to the fade window, with a\n // per-sample decay that reaches the floor exactly at its end.\n this.concealCapacity = Math.max(1, Math.floor((rate * 40) / 1000));\n this.concealBuf = new Float32Array(this.concealCapacity);\n this.concealDecay = Math.exp(Math.log(0.001) / this.concealCapacity);\n // Float32 ring buffer \u2014 PLAYBACK_BUFFER_SECONDS at the context sample\n // rate. Allocated once for the node's lifetime; per-turn state resets via\n // resetTurn(). writePos and readPos are absolute (monotonic) sample\n // counts; the buffer is indexed modulo capacity so a longer reply keeps\n // playing instead of writing past the end and going silent.\n this.capacity = rate * 60;\n this.samples = new Float32Array(this.capacity);\n // Cadence for the 'progress' report in process(), counted in samples so\n // the hot path never reads a clock. Survives resetTurn(): the host clamps\n // upward only, so a stale count costs at most one extra report.\n this.reportIntervalSamples = Math.floor((rate * 500) / 1000);\n this.sinceReportSamples = 0;\n // Platform endianness probe: the wire format is PCM16 little-endian, so\n // the Int16Array fast path in ingestBytes is only valid on LE hosts\n // (every shipping browser target; the DataView path is the fallback).\n this.littleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;\n this.resetTurn();\n\n this.port.onmessage = (e) => {\n const d = e.data;\n if (d.event === 'write') {\n this.ingestBytes(d.buffer);\n } else if (d.event === 'interrupt') {\n // Applied eagerly, not deferred to the next process(): onmessage and\n // process() never interleave (one audio thread), and a deferred\n // interrupt would let 'write'/'done' frames for the NEXT turn\n // coalesce in ahead of it and be wiped by the reset along with the\n // cancelled turn's audio.\n this.stopTurn('interrupt');\n } else if (d.event === 'done') {\n this.isDone = true;\n // Echoed back on this turn's 'stop' so the host can tell WHICH turn\n // drained: a stop already in flight when a barge-in lands would\n // otherwise settle the next turn's done() the moment it arrives.\n this.doneTurn = d.turn ?? null;\n }\n };\n }\n\n // Reset per-turn state so the node is reusable across replies without\n // reallocating the sample buffer or re-instantiating the worklet.\n resetTurn() {\n this.isDone = false;\n // Host turn id carried by this turn's 'done' (null until one arrives).\n this.doneTurn = null;\n this.playing = false;\n // Whether any real audio has been rendered this turn. Separates a turn's\n // pre-roll (nothing to extrapolate from, and not a defect) from a\n // mid-turn underrun.\n this.hasPlayed = false;\n // Carry-over byte for split samples across chunks\n this.carry = null;\n this.writePos = 0;\n this.readPos = 0;\n // Concealment ring state and the current fade position.\n this.concealLen = 0;\n this.concealWrite = 0;\n this.concealPos = 0;\n this.concealGain = 1;\n // Episode flags, so a multi-quantum gap counts as one event.\n this.concealing = false;\n this.concealedSilence = false;\n // Reported to the host on 'stop'. A fresh object per turn: the one just\n // posted must not be mutated by the next turn.\n this.stats = {\n concealedSamples: 0,\n silentConcealedSamples: 0,\n concealmentEvents: 0,\n silentConcealmentEvents: 0,\n };\n }\n\n // End the current turn: notify the host and rearm for the next reply.\n // Must NOT return false from process() \u2014 a processor that stops is dead\n // for good, forcing a new node (and buffer) per reply.\n // `reason` ('interrupt' | 'done') tells the host which turn boundary this\n // stop belongs to \u2014 interrupt-stops are dropped host-side, flush() having\n // already settled that turn \u2014 and `turn` names WHICH turn drained (the id\n // this turn's 'done' carried), so a drain-stop still in flight when a\n // barge-in lands cannot settle the next turn's done() early.\n stopTurn(reason) {\n this.port.postMessage({ event: 'stop', reason, turn: this.doneTurn, stats: this.stats });\n this.resetTurn();\n }\n\n // Cover a quantum (from `start`) where real audio should have been.\n //\n // Before the turn's first samples there is nothing to extrapolate from, so\n // the gap is plain silence and counted as nothing \u2014 WebRTC likewise only\n // counts concealment once playout has begun. After that, loop the retained\n // tail under a decaying gain: a hard zero-fill is a discontinuity mid-word,\n // which is the click that makes a brief stall sound like breakage.\n coverGap(out, start) {\n if (!this.hasPlayed) {\n out.fill(0, start);\n return;\n }\n if (!this.concealing) {\n this.concealing = true;\n this.concealedSilence = false;\n this.stats.concealmentEvents++;\n }\n const total = out.length - start;\n const len = this.concealLen;\n let silent = 0;\n // Second condition: once the fade has decayed to the floor, every sample\n // the loop would emit is 0 anyway \u2014 bulk-fill instead of running 128\n // branchy iterations per quantum for the whole tail of a long stall.\n if (len === 0 || this.concealGain < 0.001) {\n out.fill(0, start);\n silent = total;\n } else {\n let g = this.concealGain;\n for (let i = start; i < out.length; i++) {\n if (g < 0.001) {\n // The fade has run out: keep counting the gap, but stop looping a\n // fragment that is now inaudible anyway.\n out[i] = 0;\n silent++;\n continue;\n }\n out[i] = this.concealBuf[this.concealPos] * g;\n this.concealPos = this.concealPos + 1 === len ? 0 : this.concealPos + 1;\n g *= this.concealDecay;\n }\n this.concealGain = g;\n }\n this.stats.concealedSamples += total;\n if (silent > 0) {\n this.stats.silentConcealedSamples += silent;\n if (!this.concealedSilence) {\n this.concealedSilence = true;\n this.stats.silentConcealmentEvents++;\n }\n }\n }\n\n // Retain the tail of a rendered quantum as the next gap's concealment\n // source, and close any episode the real audio just ended.\n rememberTail(out, n) {\n const cap = this.concealCapacity;\n const take = Math.min(n, cap);\n // Bulk copies (this runs on every cleanly rendered quantum): the tail is\n // one contiguous source run, landing in at most two ring runs.\n const tail = out.subarray(n - take, n);\n const first = Math.min(take, cap - this.concealWrite);\n this.concealBuf.set(tail.subarray(0, first), this.concealWrite);\n if (take > first) this.concealBuf.set(tail.subarray(first), 0);\n this.concealWrite = (this.concealWrite + take) % cap;\n this.concealLen = Math.min(cap, this.concealLen + take);\n // Read the loop oldest-first; once the ring is full the write cursor is\n // the oldest retained sample.\n this.concealPos = this.concealLen === cap ? this.concealWrite : 0;\n this.concealing = false;\n this.concealGain = 1;\n }\n\n ingestBytes(uint8) {\n let bytes = uint8;\n\n if (this.carry !== null) {\n const merged = new Uint8Array(1 + bytes.length);\n merged[0] = this.carry;\n merged.set(bytes, 1);\n bytes = merged;\n this.carry = null;\n }\n\n if (bytes.length % 2 !== 0) {\n this.carry = bytes[bytes.length - 1];\n bytes = bytes.subarray(0, bytes.length - 1);\n }\n\n if (bytes.length === 0) return;\n const numSamples = bytes.length / 2;\n const cap = this.capacity;\n const samples = this.samples;\n if (this.littleEndian && (bytes.byteOffset & 1) === 0) {\n // Fast path: 2-byte-aligned LE bytes wrap directly as an Int16Array;\n // copy wrap-aware in at most two runs with no per-sample DataView call\n // or modulo. This runs on the realtime audio thread.\n const int16 = new Int16Array(bytes.buffer, bytes.byteOffset, numSamples);\n let src = 0;\n let dst = this.writePos % cap;\n while (src < numSamples) {\n const run = Math.min(numSamples - src, cap - dst);\n for (let j = 0; j < run; j++) {\n samples[dst + j] = int16[src + j] / 0x8000;\n }\n src += run;\n dst = 0;\n }\n } else {\n // Odd byte offset (or big-endian host): fall back to per-sample reads.\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);\n let dst = this.writePos % cap;\n for (let i = 0; i < numSamples; i++) {\n samples[dst] = view.getInt16(i * 2, true) / 0x8000;\n dst++;\n if (dst === cap) dst = 0;\n }\n }\n this.writePos += numSamples;\n // If the producer outran the consumer by more than the buffer holds, drop\n // the oldest unplayed audio rather than reading samples we've overwritten.\n if (this.writePos - this.readPos > this.capacity) {\n this.readPos = this.writePos - this.capacity;\n }\n }\n\n process(inputs, outputs) {\n // No output wired up yet \u2014 nothing to render this quantum. Throwing here\n // would permanently kill the processor (the node is persistent per\n // session), so guard like the capture processor does.\n if (!outputs[0] || !outputs[0][0]) return true;\n const out = outputs[0][0];\n const avail = this.writePos - this.readPos;\n\n // Report the unplayed backlog to the host (`playback_progress`). This IS\n // the closed-loop signal: the host otherwise models playback open-loop \u2014\n // every forwarded chunk assumed to start playing on arrival at exactly\n // 1.0x \u2014 and cannot see a buffer that has run ahead of the wall clock.\n // Measured against a client draining at 0.6x, the host declared the line\n // silent while it still held seconds of the reply, and then opened the\n // speaking-edge gate over speech the caller had not heard.\n //\n // Sent from process() rather than a timer because this is the only place\n // that sees the buffer on the audio thread; the counter is in QUANTA, so\n // it costs an integer compare per render and no clock read. Silence is not\n // reported: an empty buffer is what the host already assumes.\n this.sinceReportSamples += out.length;\n if (this.sinceReportSamples >= this.reportIntervalSamples) {\n this.sinceReportSamples = 0;\n if (avail > 0) {\n this.port.postMessage({ event: 'progress', bufferedMs: (avail / this.rate) * 1000 });\n }\n }\n\n // Filling: wait for the target. 'done' short-circuits it \u2014 what is\n // buffered is all there will be, so there is nothing left to wait for.\n if (!this.playing) {\n if (avail >= this.fillSamples || this.isDone) {\n this.playing = true;\n } else {\n this.coverGap(out, 0);\n return true;\n }\n }\n\n // Underrun: this quantum cannot be filled and more audio is still coming.\n // Go back to filling and cover the gap, leaving\n // readPos untouched \u2014 the fragment stays buffered and plays intact once\n // the buffer recovers, instead of being dribbled out a few samples at a\n // time for the rest of the turn.\n if (avail < out.length && !this.isDone) {\n this.playing = false;\n this.coverGap(out, 0);\n return true;\n }\n\n if (avail > 0) {\n const n = Math.min(avail, out.length);\n // Copy from the ring buffer, splitting across the wrap boundary.\n const start = this.readPos % this.capacity;\n const first = Math.min(n, this.capacity - start);\n out.set(this.samples.subarray(start, start + first), 0);\n if (n > first) out.set(this.samples.subarray(0, n - first), first);\n this.readPos += n;\n // Only reachable with n < out.length on the turn's final partial\n // quantum (the underrun branch above catches every other case).\n out.fill(0, n);\n this.hasPlayed = true;\n this.rememberTail(out, n);\n return true;\n }\n\n // Drained and done: end the turn. Not reachable mid-turn \u2014 an empty\n // buffer with audio still coming is the underrun branch above.\n out.fill(0);\n if (this.isDone) {\n this.stopTurn('done');\n }\n return true;\n }\n}\n\nregisterProcessor('playback-processor', PlaybackProcessor);\n";
3
- declare const _default: string;
4
- export default _default;
2
+ export declare const playbackProcessorSource: string;
3
+ declare const playbackProcessorUrl: string;
4
+ export default playbackProcessorUrl;
@@ -316,6 +316,6 @@ class PlaybackProcessor extends AudioWorkletProcessor {
316
316
 
317
317
  registerProcessor('playback-processor', PlaybackProcessor);
318
318
  `;
319
- var playback_processor_default = workletModuleUrl(playbackProcessorSource);
319
+ const playbackProcessorUrl = workletModuleUrl(playbackProcessorSource);
320
320
  //#endregion
321
- export { playback_processor_default as default, playbackProcessorSource };
321
+ export { playbackProcessorUrl as default, playbackProcessorSource };