@alexkroman1/aai-ui 0.12.2 → 0.13.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 (215) hide show
  1. package/README.md +257 -0
  2. package/dist/_colors-CpZO-88A.js +78 -0
  3. package/dist/_module-url-C13kAJ87.js +15 -0
  4. package/dist/_page-shell.d.ts +11 -0
  5. package/dist/_recover-run.d.ts +79 -0
  6. package/dist/_repeat-until.d.ts +30 -0
  7. package/dist/_run-controls.d.ts +33 -0
  8. package/dist/_submission-state.d.ts +92 -0
  9. package/dist/_tap-to-talk-state.d.ts +88 -0
  10. package/dist/_utils-Mi-SCFA1.js +47 -0
  11. package/dist/_utils.d.ts +26 -0
  12. package/dist/_web-storage.d.ts +43 -0
  13. package/dist/_workflow-api-ref.d.ts +37 -0
  14. package/dist/_workflow-files.d.ts +34 -0
  15. package/dist/_workflow-test-defs.d.ts +38 -0
  16. package/dist/aai-logo-D8pMVn9_.js +85 -0
  17. package/dist/agent-state-labels.d.ts +65 -0
  18. package/dist/agent-state.d.ts +108 -0
  19. package/dist/audio/capture.d.ts +44 -0
  20. package/dist/audio/index.d.ts +18 -0
  21. package/dist/audio/index.js +431 -0
  22. package/dist/audio/preconnect.d.ts +87 -0
  23. package/dist/audio/voice-io.d.ts +112 -0
  24. package/dist/build-default-client.d.ts +1 -0
  25. package/dist/chat-view-BCbI7gD4.js +396 -0
  26. package/dist/client-config-C7fQrioM.js +139 -0
  27. package/dist/client-config.d.ts +119 -0
  28. package/dist/client-dir.d.ts +44 -0
  29. package/dist/client-dir.js +58 -0
  30. package/dist/client-identity.d.ts +131 -0
  31. package/dist/client-tool.d.ts +51 -0
  32. package/dist/components/_colors.d.ts +73 -0
  33. package/dist/components/_form-readiness.d.ts +56 -0
  34. package/dist/components/_form-values.d.ts +19 -0
  35. package/dist/components/aai-logo.d.ts +12 -0
  36. package/dist/components/audio-result.d.ts +94 -0
  37. package/dist/components/auto-scroll.d.ts +74 -0
  38. package/dist/components/bullet-list.d.ts +74 -0
  39. package/dist/components/button.d.ts +58 -0
  40. package/dist/components/button.js +85 -0
  41. package/dist/components/chat-view.d.ts +35 -0
  42. package/dist/components/chat-view.js +3 -0
  43. package/dist/components/console-shell.d.ts +85 -0
  44. package/dist/components/controls.d.ts +33 -0
  45. package/dist/components/controls.js +49 -0
  46. package/dist/components/conversation-view.d.ts +130 -0
  47. package/dist/components/eyebrow.d.ts +12 -0
  48. package/dist/components/facts.d.ts +81 -0
  49. package/dist/components/form-fields.d.ts +142 -0
  50. package/dist/components/form-types.d.ts +73 -0
  51. package/dist/components/form.d.ts +96 -0
  52. package/dist/components/markdown.d.ts +57 -0
  53. package/dist/components/message-list.d.ts +48 -0
  54. package/dist/components/message-list.js +3 -0
  55. package/dist/components/session-controls.d.ts +125 -0
  56. package/dist/components/session-error-banner.d.ts +69 -0
  57. package/dist/components/session-state-dot.d.ts +111 -0
  58. package/dist/components/sidebar-layout.d.ts +42 -0
  59. package/dist/components/sidebar-layout.js +56 -0
  60. package/dist/components/start-screen.d.ts +37 -0
  61. package/dist/components/start-screen.js +71 -0
  62. package/dist/components/tool-call-block.d.ts +28 -0
  63. package/dist/components/tool-call-block.js +3 -0
  64. package/dist/components/tool-call-row.d.ts +75 -0
  65. package/dist/components/tool-config-context.d.ts +22 -0
  66. package/dist/components/upload-progress.d.ts +76 -0
  67. package/dist/components/url-chips.d.ts +29 -0
  68. package/dist/components/workflow-fields.d.ts +46 -0
  69. package/dist/components/workflow-pending-note.d.ts +81 -0
  70. package/dist/components/workflow-progress.d.ts +81 -0
  71. package/dist/components/workflow-run-error.d.ts +48 -0
  72. package/dist/components/workflow-run-panel.d.ts +79 -0
  73. package/dist/context.d.ts +288 -0
  74. package/dist/context.js +382 -0
  75. package/dist/conversation-log.d.ts +88 -0
  76. package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
  77. package/dist/default-client/assets/audio-cGFGID-2.js +1 -0
  78. package/dist/default-client/assets/capture-processor-BUABoo3_.js +90 -0
  79. package/dist/default-client/assets/index-8ihb1Urm.css +2 -0
  80. package/dist/default-client/assets/index-BQURVARl.js +126 -0
  81. package/dist/default-client/assets/playback-processor-ABOa11j0.js +314 -0
  82. package/dist/default-client/favicon.ico +0 -0
  83. package/dist/default-client/index.html +15 -0
  84. package/dist/default-client.d.ts +23 -0
  85. package/dist/define-client.d.ts +162 -26
  86. package/dist/define-client.js +201 -55
  87. package/dist/eyebrow-UfmSz9yy.js +27 -0
  88. package/dist/hooks-DN4gHYsj.js +182 -0
  89. package/dist/hooks.d.ts +172 -0
  90. package/dist/hooks.js +3 -0
  91. package/dist/hooks.test-d.d.ts +14 -0
  92. package/dist/inbox-protocol.d.ts +128 -0
  93. package/dist/inbox.d.ts +132 -0
  94. package/dist/index.d.ts +129 -32
  95. package/dist/index.js +5503 -18
  96. package/dist/internal.d.ts +43 -0
  97. package/dist/internal.js +8 -0
  98. package/dist/linked-client.d.ts +70 -0
  99. package/dist/message-list-DHZcLhRA.js +682 -0
  100. package/dist/notice-player.d.ts +70 -0
  101. package/dist/page.d.ts +114 -0
  102. package/dist/phone.d.ts +50 -0
  103. package/dist/route-fetch.d.ts +79 -0
  104. package/dist/session/audio-effects.d.ts +31 -0
  105. package/dist/session/audio-setup.d.ts +57 -0
  106. package/dist/session/audio-state.d.ts +157 -0
  107. package/dist/session/browser-session.d.ts +30 -0
  108. package/dist/session/close.d.ts +26 -0
  109. package/dist/session/dial.d.ts +65 -0
  110. package/dist/session/handshake.d.ts +60 -0
  111. package/dist/session/index.d.ts +17 -0
  112. package/dist/session/index.js +2 -0
  113. package/dist/session/messages.d.ts +63 -0
  114. package/dist/session/mic.d.ts +64 -0
  115. package/dist/session/preconnect.d.ts +48 -0
  116. package/dist/session/reconnect.d.ts +25 -0
  117. package/dist/session/resume-store.d.ts +57 -0
  118. package/dist/session/state.d.ts +146 -0
  119. package/dist/session/ticket.d.ts +44 -0
  120. package/dist/session/types.d.ts +476 -0
  121. package/dist/session/url.d.ts +17 -0
  122. package/dist/session/user-turn.d.ts +55 -0
  123. package/dist/session-CE0Ipsb3.js +2129 -0
  124. package/dist/stored-value.d.ts +103 -0
  125. package/dist/tool-call-block-uXtMOUWN.js +204 -0
  126. package/dist/tool-config-context-Es4YUzV2.js +19 -0
  127. package/dist/types.d.ts +272 -34
  128. package/dist/types.js +35 -4
  129. package/dist/upload/files.d.ts +83 -0
  130. package/dist/upload/index.d.ts +15 -0
  131. package/dist/upload/pause.d.ts +31 -0
  132. package/dist/upload/recall.d.ts +67 -0
  133. package/dist/upload/report.d.ts +26 -0
  134. package/dist/upload/session.d.ts +99 -0
  135. package/dist/url-chips-oyy_Eiqh.js +239 -0
  136. package/dist/use-client-runs.d.ts +87 -0
  137. package/dist/use-conversation-log.d.ts +86 -0
  138. package/dist/use-conversation.d.ts +131 -0
  139. package/dist/use-copy.d.ts +74 -0
  140. package/dist/use-download-url.d.ts +84 -0
  141. package/dist/use-flash.d.ts +61 -0
  142. package/dist/use-inbox.d.ts +101 -0
  143. package/dist/use-push-to-talk.d.ts +105 -0
  144. package/dist/use-route-mutation.d.ts +113 -0
  145. package/dist/use-route.d.ts +82 -0
  146. package/dist/use-run-key.d.ts +133 -0
  147. package/dist/use-session-controls.d.ts +69 -0
  148. package/dist/use-session-id.d.ts +57 -0
  149. package/dist/use-tap-to-talk.d.ts +138 -0
  150. package/dist/use-user-transcript-uyHhzy4d.js +73 -0
  151. package/dist/use-user-transcript.d.ts +75 -0
  152. package/dist/use-workflow-form.d.ts +285 -0
  153. package/dist/use-workflow-progress.d.ts +137 -0
  154. package/dist/use-workflow-run-DdhWmbx-.js +697 -0
  155. package/dist/use-workflow-run.d.ts +100 -0
  156. package/dist/use-workflow-runs.d.ts +71 -0
  157. package/dist/use-workflow-stream.d.ts +154 -0
  158. package/dist/use-workflows.d.ts +77 -0
  159. package/dist/workflow-client.d.ts +136 -0
  160. package/dist/workflow-def-types.d.ts +31 -0
  161. package/dist/workflow-events.d.ts +39 -0
  162. package/dist/workflow-field-kind.d.ts +95 -0
  163. package/dist/workflow-status-labels.d.ts +35 -0
  164. package/dist/worklets/_module-url.d.ts +10 -0
  165. package/dist/worklets/_playback-bench-harness.d.ts +181 -0
  166. package/dist/worklets/_playback-bench-host.d.ts +63 -0
  167. package/dist/worklets/_playback-bench-page.d.ts +65 -0
  168. package/dist/worklets/_tts-trace-harness.d.ts +142 -0
  169. package/dist/worklets/_worklet-test-utils.d.ts +27 -0
  170. package/dist/worklets/capture-processor.d.ts +4 -2
  171. package/dist/worklets/capture-processor.js +73 -33
  172. package/dist/worklets/playback-processor.d.ts +4 -2
  173. package/dist/worklets/playback-processor.js +258 -42
  174. package/package.json +58 -31
  175. package/styles.css +152 -30
  176. package/dist/_components/app.d.ts +0 -23
  177. package/dist/_components/app.js +0 -41
  178. package/dist/_components/button.d.ts +0 -50
  179. package/dist/_components/button.js +0 -46
  180. package/dist/_components/chat-view.d.ts +0 -29
  181. package/dist/_components/chat-view.js +0 -61
  182. package/dist/_components/controls.d.ts +0 -18
  183. package/dist/_components/controls.js +0 -37
  184. package/dist/_components/error-banner.d.ts +0 -21
  185. package/dist/_components/error-banner.js +0 -27
  186. package/dist/_components/message-bubble.d.ts +0 -23
  187. package/dist/_components/message-bubble.js +0 -35
  188. package/dist/_components/message-list.d.ts +0 -21
  189. package/dist/_components/message-list.js +0 -76
  190. package/dist/_components/sidebar-layout.d.ts +0 -21
  191. package/dist/_components/sidebar-layout.js +0 -36
  192. package/dist/_components/start-screen.d.ts +0 -26
  193. package/dist/_components/start-screen.js +0 -51
  194. package/dist/_components/state-indicator.d.ts +0 -29
  195. package/dist/_components/state-indicator.js +0 -37
  196. package/dist/_components/thinking-indicator.d.ts +0 -16
  197. package/dist/_components/thinking-indicator.js +0 -34
  198. package/dist/_components/tool-call-block.d.ts +0 -27
  199. package/dist/_components/tool-call-block.js +0 -118
  200. package/dist/_components/tool-icons.d.ts +0 -17
  201. package/dist/_components/tool-icons.js +0 -128
  202. package/dist/_components/transcript.d.ts +0 -25
  203. package/dist/_components/transcript.js +0 -35
  204. package/dist/audio.d.ts +0 -45
  205. package/dist/audio.js +0 -121
  206. package/dist/client-context.d.ts +0 -33
  207. package/dist/client-context.js +0 -15
  208. package/dist/client-handler.d.ts +0 -43
  209. package/dist/session-CWB7vmqz.js +0 -429
  210. package/dist/session.d.ts +0 -69
  211. package/dist/session.js +0 -2
  212. package/dist/signals.d.ts +0 -120
  213. package/dist/signals.js +0 -161
  214. package/dist/tsdown.config.d.ts +0 -2
  215. package/dist/types.test-d.d.ts +0 -7
@@ -0,0 +1,697 @@
1
+ import { n as pageBaseUrl } from "./_utils-Mi-SCFA1.js";
2
+ import { errorMessage } from "@alexkroman1/aai";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { omitUndefined } from "@alexkroman1/aai/utils";
5
+ import { createAgentClient, isTerminal, isTerminal as isTerminal$1, readEventStream } from "@alexkroman1/aai/workflow-api";
6
+ //#region src/workflow-client.ts
7
+ /**
8
+ * Browser client for the workflow HTTP API (`aai/host/workflow-api.ts`).
9
+ *
10
+ * This is the whole client half of a WORKFLOW APP: an agent whose front door is
11
+ * a form rather than a microphone (`workflowApp()`) starts runs
12
+ * here and watches them for the answer. It deliberately does NOT go through
13
+ * `BrowserSession` — there is no socket, no audio graph, and no session to resume.
14
+ *
15
+ * **The requests themselves are the SDK's now**
16
+ * (`createWorkflowApiClient`, `@alexkroman1/aai/workflow-api`), and what is left
17
+ * here is the one thing that is genuinely a BROWSER's: the default base URL.
18
+ * Every route, every query, the 404-is-an-answer rule and the `wait` clamp were
19
+ * written three times over — here, in the studio's Workflows card, and in
20
+ * `aai workflow` — and the parts the copies disagreed on were exactly the ones a
21
+ * reader cannot check by eye. The SDK module's doc carries that argument; this
22
+ * file must not grow a second implementation of any of it.
23
+ *
24
+ * The one thing worth knowing before using it: **a run outlives the page.**
25
+ * Starting one resolves as soon as the run is created, so `runId` is the only
26
+ * handle that matters and it stays valid across a reload, a different device, or
27
+ * `curl` — which is what makes `useWorkflowRun` a watch rather than a
28
+ * subscription to something the page owns.
29
+ *
30
+ * The loop that keeps asking lives in `use-workflow-run.ts`, and the streaming
31
+ * fast path under it in `workflow-events.ts`.
32
+ */
33
+ /**
34
+ * Create a client for the agent serving this page.
35
+ *
36
+ * **You usually do not need one.** Every hook and component here builds this
37
+ * exact client lazily and once when no `api` is passed
38
+ * (`_workflow-api-ref.ts`), so `useWorkflowSubmit("digest")` already talks to
39
+ * the right agent. Reach for this when the client has to be DIFFERENT from that
40
+ * default — another agent's `baseUrl`, or a `token` — or when a page wants
41
+ * `config()` and the run calls on one object.
42
+ *
43
+ * If you do build one, hoist it out of the component that uses it.
44
+ * `useWorkflowRun` holds the client in a ref precisely so a fresh object per
45
+ * render does not restart its watch, but a client built in render is still a new
46
+ * `fetch` closure every time and reads as though it were free.
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * import { createWorkflowApi, useWorkflowRun } from "@alexkroman1/aai-ui";
51
+ * import { useState } from "react";
52
+ *
53
+ * // A DIFFERENT agent than the one serving this page, so the client is
54
+ * // explicit — and module scope, not render scope, per above.
55
+ * const api = createWorkflowApi({ baseUrl: "https://agents.example/digest" });
56
+ *
57
+ * function StartDigest() {
58
+ * const [runId, setRunId] = useState<string>();
59
+ * const { run } = useWorkflowRun(runId, { api });
60
+ * return (
61
+ * <button
62
+ * type="button"
63
+ * onClick={() => void api.start("digest", { url: "…" }).then(setRunId)}
64
+ * >
65
+ * {run ? run.status : "Start"}
66
+ * </button>
67
+ * );
68
+ * }
69
+ * ```
70
+ *
71
+ * @param options - See {@link WorkflowApiOptions}. Both fields are optional; the
72
+ * default base URL is the page's own origin and path.
73
+ * @returns Every workflow call plus `config()` and `baseUrl` — see
74
+ * {@link AgentClient}. It was the narrow {@link WorkflowApi}, which left a page
75
+ * that also wanted the agent's own name building a SECOND client (or a bare
76
+ * `fetch` and a hand-written URL join) for one read; the SDK documents
77
+ * `createAgentClient` as a superset of the same routes, so delegating to it
78
+ * widens the return without a second implementation of anything.
79
+ *
80
+ * @public
81
+ */
82
+ function createWorkflowApi(options = {}) {
83
+ return createAgentClient({
84
+ baseUrl: options.baseUrl ?? pageBaseUrl(),
85
+ ...omitUndefined({ token: options.token })
86
+ });
87
+ }
88
+ //#endregion
89
+ //#region src/_workflow-api-ref.ts
90
+ /**
91
+ * The client preamble every workflow hook needs, once.
92
+ *
93
+ * Five hooks (`useWorkflowRun`, `useWorkflowProgress`, `useWorkflowRuns`,
94
+ * `useWorkflows`, `useWorkflowSubmit`) opened with the same two refs and the
95
+ * same two paragraphs explaining them, and both halves are load-bearing rather
96
+ * than stylistic — which is exactly why they should not be re-derived per hook:
97
+ *
98
+ * - **The caller's client lives in a REF, never in an effect's dependency
99
+ * array.** The natural call site is
100
+ * `useWorkflowRun(id, { api: createWorkflowApi() })`, which passes a NEW
101
+ * object every render; as a dependency that tears the effect down and
102
+ * restarts it on each one, and because a restart clears state and re-renders,
103
+ * it schedules the next. The result is an unbounded request loop against the
104
+ * agent — on the platform, against the BROKER — with `error` wiped before
105
+ * anything can read it, presenting as "the page polls forever" rather than as
106
+ * a mistake at the call site.
107
+ * - **The no-client default is built lazily and ONCE.** As a render-time
108
+ * default (`api ?? createWorkflowApi()`) it is a fresh object per render,
109
+ * which is the same hazard one layer down; built inside an effect it is a
110
+ * fresh object per watch.
111
+ *
112
+ * The returned getter is stable for the life of the component and reads the ref
113
+ * on every call, so a caller that SWAPS clients mid-watch — a token arriving
114
+ * after login — is picked up by the next request without the watch restarting.
115
+ */
116
+ /**
117
+ * Resolve the client a workflow hook should use, now.
118
+ *
119
+ * @param api - The caller's client, or undefined for one aimed at the page's
120
+ * own agent.
121
+ * @returns A stable getter. Call it per request, never once per watch.
122
+ *
123
+ * @internal
124
+ */
125
+ function useWorkflowApiRef(api) {
126
+ const apiRef = useRef(api);
127
+ apiRef.current = api;
128
+ const fallbackRef = useRef(void 0);
129
+ return useCallback(() => {
130
+ const current = apiRef.current;
131
+ if (current) return current;
132
+ fallbackRef.current ??= createWorkflowApi();
133
+ return fallbackRef.current;
134
+ }, []);
135
+ }
136
+ //#endregion
137
+ //#region src/_repeat-until.ts
138
+ /**
139
+ * A bounded read, re-armed from the SETTLED read — the loop both workflow
140
+ * watchers are built out of.
141
+ *
142
+ * `pollUntilTerminal` (`use-workflow-run.ts`) and `readProgressUntilComplete`
143
+ * (`use-workflow-progress.ts`) each open a request, decide from its answer
144
+ * whether to come back, and stop when told to. What they had in common was not
145
+ * the decision — one reads a snapshot, the other drains an SSE body — but the
146
+ * scaffold around it, and that scaffold is where the two rules live:
147
+ *
148
+ * - **Re-armed from the settled read, never on an interval.** A slow response
149
+ * would otherwise stack overlapping requests on an agent that is already
150
+ * struggling, and on the platform every one of them BROKERS.
151
+ * - **Cancellation is a signal, not a flag.** The teardown has to reach the
152
+ * in-flight request too — an abandoned progress read otherwise keeps pulling
153
+ * chunks out of a run for a page that has navigated away — so `step` is
154
+ * handed the signal rather than being trusted to check a boolean.
155
+ */
156
+ /**
157
+ * Call `step` until it reports it is finished, or until the returned stop
158
+ * function is called.
159
+ *
160
+ * `step` resolves `true` when there is nothing left to come back for. It is
161
+ * responsible for its own failures: a rejection would leave the loop stopped
162
+ * with nobody told, so each caller decides whether its failure is terminal or
163
+ * just another reason to try again.
164
+ *
165
+ * @internal
166
+ */
167
+ function repeatUntil(intervalMs, step) {
168
+ const controller = new AbortController();
169
+ let timer;
170
+ const tick = async () => {
171
+ if (await step(controller.signal) || controller.signal.aborted) return;
172
+ timer = setTimeout(() => void tick(), intervalMs);
173
+ };
174
+ tick();
175
+ return () => {
176
+ controller.abort();
177
+ if (timer !== void 0) clearTimeout(timer);
178
+ };
179
+ }
180
+ //#endregion
181
+ //#region src/use-workflow-progress.ts
182
+ /**
183
+ * `useWorkflowProgress` — read what a run has WRITTEN while it runs.
184
+ *
185
+ * The sibling of `useWorkflowRun`, and the split between them is the whole
186
+ * reason this exists. That hook reports a run's STATE: the status transitions
187
+ * the world records, which every run has. This reports what the run itself
188
+ * wrote through `getWritable()`, which is the only thing a long run can say
189
+ * before it finishes — a snapshot carries a status and, once terminal, an
190
+ * output, and nothing in between. A page that shows only status shows
191
+ * "Working…" for ten minutes and then a result.
192
+ *
193
+ * ## There is no poll fallback, and that is not an omission
194
+ *
195
+ * `useWorkflowRun` degrades to polling `GET /runs/:id` because a run's STATE is
196
+ * readable that way. A run's written chunks are not: the stream is the only
197
+ * route to them, so an agent that does not serve it has no progress to give and
198
+ * `supported` says so once, rather than a poll pretending to look for something
199
+ * that is not there. A page renders its status line either way.
200
+ *
201
+ * ## Chunks are RETAINED, so this is a replay as much as a tail
202
+ *
203
+ * The run's stream keeps every chunk, so a page that mounts late — a reload, a
204
+ * second tab, a link opened tomorrow — reads the whole history from index 0 and
205
+ * arrives at the same list as one that watched throughout. That is what makes a
206
+ * durable run's progress durable too, and it is why the default `startIndex` is
207
+ * 0 rather than "from now": a tail-only default would make the same page show
208
+ * different things depending on when it opened.
209
+ *
210
+ * ## It RE-OPENS while the run is live, because a progress read is bounded
211
+ *
212
+ * The route answers with the chunks written when the request arrived and then
213
+ * ends, reporting `complete` — whether the run itself was terminal. It has to:
214
+ * a workflow stream signals its end only once CLOSED, and a progress channel
215
+ * written by one step after another is never closed, so a read that waited for
216
+ * the end would hang forever on a finished run. (It did: see the route's own
217
+ * doc.)
218
+ *
219
+ * So this hook re-opens from where it left off until a read comes back
220
+ * `complete`. That is a poll, and the honest description of progress is a durable
221
+ * log rather than a socket — but it is a poll of a CHEAP shape: each read asks
222
+ * only for chunks past the last index it saw, so a quiet run costs an empty
223
+ * answer rather than the whole log again.
224
+ *
225
+ * ## A FAILED read is not an absent route, whether or not it carried a status
226
+ *
227
+ * Because the loop stops for good once it decides the route is absent, that
228
+ * decision is the one place a single bad request can cost a live run its entire
229
+ * narration — and it did. Every non-2xx used to read as "this agent serves no
230
+ * progress route", so one transient answer hid the log permanently while the run
231
+ * carried on and the status line went on saying `running`. See
232
+ * {@link isTransientRead} for the split and `readOnce` for what each arm costs.
233
+ */
234
+ /**
235
+ * How often a live run's progress is re-read once a bounded read has ended.
236
+ *
237
+ * **Five seconds, up from one, because narration is the cheapest thing on the
238
+ * page and it was the most expensive thing on the wire.**
239
+ *
240
+ * On the platform every one of these reads BROKERS (see `useWorkflowRun`'s note
241
+ * on the same hazard), and a page routinely mounts TWO of these hooks against one
242
+ * run — `transcription-workflow` renders `<WorkflowProgress>` for the run's own
243
+ * narration and `<LiveTranscript>` for the segments as they land. At one second
244
+ * that is 2 requests/second from a single tab, which is exactly the platform's
245
+ * whole per-IP surface budget (`WORKFLOW_IP_RATE_LIMIT`, 600 per 5 minutes) — so
246
+ * one tab watching one run, with a history entry expanded, answers
247
+ * `429 Too many workflow requests` partway through its own run.
248
+ *
249
+ * It is also contending for the link with the UPLOAD, which on this page is the
250
+ * thing the reader is actually waiting for: a workflow app's whole wall clock is
251
+ * bytes going out, and progress polling spends the same uplink to describe it.
252
+ *
253
+ * What five costs is that a line appears up to five seconds after the run wrote
254
+ * it. That is the right trade for a log a person SKIMS while waiting minutes —
255
+ * and it is not the run's completion, which arrives on `useWorkflowRun`'s event
256
+ * stream (see {@link DEFAULT_WORKFLOW_POLL_MS}, deliberately left at two seconds
257
+ * because it answers "is it done", not "what is it doing").
258
+ *
259
+ * A page that really wants a live feed passes `intervalMs` and owns the
260
+ * consequence. That option is the authoring surface for this choice, which is why
261
+ * this constant is `/internal` rather than public.
262
+ */
263
+ const DEFAULT_PROGRESS_POLL_MS = 5e3;
264
+ /**
265
+ * Whether a non-2xx says "come back" rather than "there is nothing here".
266
+ *
267
+ * The 408/429/5xx split, and it is a THIRD copy of that rule stated
268
+ * deliberately: the SDK's own `isTransientStatus` is on
269
+ * `@alexkroman1/aai/step` and `RETRYABLE_STATUS` is `sdk/_upload-retry.ts`'s
270
+ * internal, so neither is reachable from a browser bundle — this package may
271
+ * not import the step surface, and an `_`-prefixed module may not be imported
272
+ * cross-package at all. Hoisting one of them onto `/utils` (the SDK's
273
+ * `/step` subpath is where `isTransientStatus` lives today) is the fix that would
274
+ * delete this; it is a published-surface change and therefore not this one.
275
+ *
276
+ * Everything else is treated as a stable answer, which keeps a permanent
277
+ * refusal — a 401 against an agent whose `AAI_WORKFLOW_API_TOKEN` this page has
278
+ * no token for — from brokering a request every interval for as long as the tab
279
+ * is open. A 404 is the specific case the route documents: an agent deployed
280
+ * before progress streams existed, or one serving no workflow API at all.
281
+ */
282
+ function isTransientRead(status) {
283
+ return status === 408 || status === 429 || status >= 500;
284
+ }
285
+ /**
286
+ * Drain one bounded read's frames, reporting how it ended and everything it
287
+ * carried.
288
+ *
289
+ * The chunks are RETURNED rather than handed over one at a time, and that is
290
+ * what lets the hook commit a whole read in one React update: a per-chunk
291
+ * callback re-rendered the page once per progress line and rebuilt the list
292
+ * each time, which for a fan-out writing a line per segment is an O(n²) copy of
293
+ * the log a reader can already only see one frame at a time. A read is bounded
294
+ * by construction (see the module doc), so buffering one is bounded too.
295
+ */
296
+ async function consumeFrames(body, signal) {
297
+ const chunks = [];
298
+ let ending = "partial";
299
+ for await (const frame of readEventStream(body, signal)) if (frame.event === "chunk") chunks.push(frame.data);
300
+ else if (frame.event === "done") ending = frame.data?.complete ? "complete" : "partial";
301
+ else if (frame.event === "missing") return {
302
+ ending: "complete",
303
+ chunks
304
+ };
305
+ return {
306
+ ending,
307
+ chunks
308
+ };
309
+ }
310
+ /**
311
+ * Read one run's progress until it is complete, reporting each read's chunks.
312
+ *
313
+ * Module-level rather than inline in the hook, so the loop reads without React in
314
+ * the way — the same split `useWorkflowRun` makes with `pollUntilTerminal`.
315
+ *
316
+ * Every re-open asks from an ABSOLUTE position past the last chunk read, so a
317
+ * read only ever fetches what this reader has not seen. That is what keeps the
318
+ * poll cheap: a quiet run answers with a bare `done` rather than the whole log
319
+ * again.
320
+ *
321
+ * `next` is a COUNT of chunks consumed, and `startIndex` is an INCLUSIVE floor,
322
+ * so the two are the same number and no adjustment sits between them. That
323
+ * identity is the whole correctness argument here, and it is why the store's
324
+ * floor is inclusive rather than exclusive — read exclusively, this loop lost the
325
+ * chunk sitting AT its cursor on every re-open, so a run writing one line per
326
+ * poll delivered every other line.
327
+ * `packages/aai-runtime/src/workflow-stream-cursor.test.ts` states it as a property
328
+ * over generated polling schedules; this module's own spec pins the URLs.
329
+ *
330
+ * ## A negative `startIndex` is resolved on the FIRST read, not carried
331
+ *
332
+ * "The last N lines" names no position a later read can resume from — the tail
333
+ * it counts back from moves with every line the run writes. Carrying it meant a
334
+ * re-open asking for everything from 0 and dropping `seen` chunks off the
335
+ * FRONT, which is a different set entirely: a reader that opened at
336
+ * `startIndex: -3` on a 10-line log holds lines 7-9, and its next read handed
337
+ * over lines 3 onwards — four lines it never asked for, then the three it
338
+ * already had, in that order. The dedupe the old comment claimed would need the
339
+ * first read's absolute tail, which the reader never learned.
340
+ *
341
+ * So the first read is issued from 0 instead, and only its last N chunks are
342
+ * handed over. The window the caller asked for is unchanged, and the reader now
343
+ * knows exactly where it is — every read after it is the ordinary absolute case.
344
+ * The cost is that the first read transfers the whole log, which is what the
345
+ * DEFAULT (`startIndex: 0`, "replay everything") already does.
346
+ */
347
+ function readProgressUntilComplete(getClient, runId, options, intervalMs, onChunks, onEnded) {
348
+ const start = options.startIndex ?? 0;
349
+ const tail = start < 0 ? -start : void 0;
350
+ let next = tail === void 0 ? start : 0;
351
+ let firstRead = true;
352
+ /** One bounded read. Resolves how it ended. */
353
+ const readOnce = async (signal) => {
354
+ const res = await getClient().streamOutput(runId, {
355
+ ...omitUndefined({
356
+ namespace: options.namespace,
357
+ startIndex: next === 0 ? void 0 : next
358
+ }),
359
+ signal
360
+ });
361
+ if (!res.ok && isTransientRead(res.status)) return "partial";
362
+ if (!(res.ok && res.body)) return "unsupported";
363
+ const { ending, chunks } = await consumeFrames(res.body, signal);
364
+ next += chunks.length;
365
+ const fresh = firstRead && tail !== void 0 ? chunks.slice(-tail) : chunks;
366
+ firstRead = false;
367
+ if (fresh.length > 0 && !signal.aborted) onChunks(fresh);
368
+ return ending;
369
+ };
370
+ return repeatUntil(intervalMs, async (signal) => {
371
+ let ending;
372
+ try {
373
+ ending = await readOnce(signal);
374
+ } catch {
375
+ ending = "partial";
376
+ }
377
+ if (signal.aborted) return true;
378
+ if (ending === "partial") return false;
379
+ onEnded(ending);
380
+ return true;
381
+ });
382
+ }
383
+ /**
384
+ * Follow one run's progress stream.
385
+ *
386
+ * Passing `undefined` (nothing started yet) costs nothing, and reading stops for
387
+ * good once a read reports the run terminal — so a finished run costs one read.
388
+ *
389
+ * @example
390
+ * ```tsx
391
+ * import { useWorkflowProgress } from "@alexkroman1/aai-ui";
392
+ *
393
+ * function Progress({ runId }: { runId?: string }) {
394
+ * const { progress, streaming, supported } = useWorkflowProgress(runId);
395
+ * if (!supported) return null;
396
+ * return (
397
+ * <pre>
398
+ * {progress.join("\n")}
399
+ * {streaming && "\n…"}
400
+ * </pre>
401
+ * );
402
+ * }
403
+ * ```
404
+ *
405
+ * @typeParam T - What the workflow writes. Defaults to `string`, which is what
406
+ * a progress channel usually carries; a workflow writing objects names its own
407
+ * shape. Nothing in the browser can verify it — the route describes no type —
408
+ * so this is the page's assertion about its own agent, narrowed once here
409
+ * rather than at every read.
410
+ *
411
+ * @public
412
+ */
413
+ function useWorkflowProgress(runId, options = {}) {
414
+ const { api, namespace, startIndex, intervalMs = DEFAULT_PROGRESS_POLL_MS } = options;
415
+ const [progress, setProgress] = useState([]);
416
+ const [streaming, setStreaming] = useState(false);
417
+ const [supported, setSupported] = useState(true);
418
+ const getClient = useWorkflowApiRef(api);
419
+ useEffect(() => {
420
+ setProgress([]);
421
+ setSupported(true);
422
+ setStreaming(false);
423
+ if (!runId) return;
424
+ setStreaming(true);
425
+ const stop = readProgressUntilComplete(getClient, runId, {
426
+ namespace,
427
+ startIndex
428
+ }, intervalMs, (chunks) => setProgress((seen) => [...seen, ...chunks]), (ending) => {
429
+ setStreaming(false);
430
+ if (ending === "unsupported") setSupported(false);
431
+ });
432
+ return () => {
433
+ stop();
434
+ setStreaming(false);
435
+ };
436
+ }, [
437
+ runId,
438
+ namespace,
439
+ startIndex,
440
+ intervalMs,
441
+ getClient
442
+ ]);
443
+ return {
444
+ progress,
445
+ latest: progress.at(-1),
446
+ streaming,
447
+ supported
448
+ };
449
+ }
450
+ //#endregion
451
+ //#region src/workflow-events.ts
452
+ /**
453
+ * Watching a run over server-sent events — the PUSH half of `useWorkflowRun`.
454
+ *
455
+ * Its own module because the seam is clean: everything in `workflow-client.ts`
456
+ * is request/response shaping plus the poll, and this is one long-lived stream
457
+ * and the SSE parser it needs.
458
+ *
459
+ * @internal
460
+ */
461
+ /**
462
+ * Watch a run over SSE, falling back to the caller's poll on any failure.
463
+ *
464
+ * The poll stays the fallback rather than being replaced, and that is the whole
465
+ * shape of this: a stream is an optimisation over a mechanism that already
466
+ * works, so every way it can fail — an older agent with no `/events` route, a
467
+ * proxy that buffers, a network that drops it — has to degrade to the thing that
468
+ * does. What it buys is real, though: on the platform every polled read BROKERS,
469
+ * so N open tabs at `DEFAULT_WORKFLOW_POLL_MS` is N/2 brokered requests a
470
+ * second, each able to boot a sandbox. One stream per tab replaces all of it.
471
+ *
472
+ * `EventSource` is not used, for two reasons that both matter here: it cannot
473
+ * send an `Authorization` header (an agent with `AAI_WORKFLOW_API_TOKEN` set
474
+ * would be unreachable), and it reconnects on its own schedule, which would
475
+ * fight the caller's. A `fetch` stream gives both back.
476
+ *
477
+ * Returns a stop function. `onFallback` is called at most once, when this stream
478
+ * cannot be relied on and the poll should take over.
479
+ */
480
+ function watchRunEvents(getClient, runId, onRun, onSettled, onFallback) {
481
+ const controller = new AbortController();
482
+ const handOver = () => {
483
+ if (!controller.signal.aborted) onFallback();
484
+ };
485
+ /**
486
+ * Consume the stream. Resolves `"settled"` when the run reached a state
487
+ * nothing will change, and `"fallback"` for every other ending — including a
488
+ * clean end with no final frame, which is a dropped connection.
489
+ *
490
+ * A named function rather than an inline IIFE so `watchRunEvents` stays under
491
+ * the cognitive-complexity cap, and so the two outcomes are a return value
492
+ * instead of two callbacks invoked from six places.
493
+ */
494
+ const pump = async () => {
495
+ const res = await getClient().watch(runId, controller.signal);
496
+ if (!(res.ok && res.body)) return "fallback";
497
+ for await (const frame of readEventStream(res.body, controller.signal)) {
498
+ if (frame.event === "run" && frame.data) onRun(frame.data);
499
+ const outcome = endingFor(frame.event);
500
+ if (outcome) return outcome;
501
+ }
502
+ return "fallback";
503
+ };
504
+ pump().then((outcome) => outcome === "settled" ? onSettled() : handOver(), () => handOver());
505
+ return () => controller.abort();
506
+ }
507
+ /**
508
+ * Does this frame END the stream, and does the run need watching afterwards?
509
+ *
510
+ * `done` and `missing` are both final and neither wants a reconnect: the run is
511
+ * terminal, or the id will never exist (a 404 is a stable answer — the world's
512
+ * record is durable). `idle` is the stream handing ITSELF back after its
513
+ * duration cap, so that one falls back to the poll. Anything else is not an
514
+ * ending.
515
+ */
516
+ function endingFor(event) {
517
+ if (event === "done" || event === "missing") return "settled";
518
+ return event === "idle" ? "fallback" : void 0;
519
+ }
520
+ //#endregion
521
+ //#region src/use-workflow-run.ts
522
+ /**
523
+ * `useWorkflowRun` — watch one run until it settles.
524
+ *
525
+ * Split from `workflow-client.ts` on the seam that module's doc already draws:
526
+ * everything there is a REQUEST (one call, one answer, no React), and everything
527
+ * here is the loop that keeps asking. They are read for different reasons — the
528
+ * client is what a script or a `curl` equivalent needs, this is what a page needs
529
+ * — and only this half imports React.
530
+ *
531
+ * `workflow-events.ts` sits under it as the streaming fast path, and
532
+ * `use-workflow-form.ts` above it as the form-shaped caller.
533
+ */
534
+ /** How often {@link useWorkflowRun} re-reads a live run when it has to poll. */
535
+ const DEFAULT_WORKFLOW_POLL_MS = 2e3;
536
+ /**
537
+ * Consecutive "no such run" reads {@link useWorkflowRun} tolerates before giving
538
+ * up on the id.
539
+ *
540
+ * Small on purpose: a 404 is a stable answer, so the budget exists only to
541
+ * absorb a first read that races the run's creation — not to keep hoping.
542
+ * Unbounded, a stale id polls (and, on the platform, BROKERS) for as long as the
543
+ * tab is open.
544
+ */
545
+ const MAX_MISSING_READS = 3;
546
+ /**
547
+ * Poll `runId` until it is terminal, reporting each read. Returns a stop
548
+ * function.
549
+ *
550
+ * Module-level rather than inline in the hook below, so neither function carries
551
+ * the whole loop's branching — and so the loop can be read without React in the
552
+ * way.
553
+ */
554
+ function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopped) {
555
+ let missing = 0;
556
+ /**
557
+ * A read that came back empty. Resolves whether the loop should STOP.
558
+ *
559
+ * A 404 is a STABLE answer — a run the agent does not know about now will not
560
+ * appear later — and retrying it unbounded is how a stale id (one restored
561
+ * from `localStorage`, or one whose agent was redeployed onto a fresh
562
+ * database) polls forever: the page stays `polling` and therefore busy, and on
563
+ * the platform every read BROKERS, so a tab's worth of dead ids keeps
564
+ * sandboxes resident. A small budget is kept anyway, because the first read
565
+ * can race a replica that has not yet seen the run.
566
+ */
567
+ const onMissing = (signal) => {
568
+ missing += 1;
569
+ if (missing < 3) return false;
570
+ if (!signal.aborted) onError(`No workflow run ${runId}`);
571
+ return true;
572
+ };
573
+ const read = async (signal) => {
574
+ try {
575
+ const next = await getClient().get(runId);
576
+ if (signal.aborted) return true;
577
+ if (!next) return onMissing(signal);
578
+ missing = 0;
579
+ onRun(next);
580
+ return isTerminal(next);
581
+ } catch (err) {
582
+ if (!signal.aborted) onError(errorMessage(err));
583
+ return false;
584
+ }
585
+ };
586
+ return repeatUntil(intervalMs, async (signal) => {
587
+ if (!await read(signal)) return false;
588
+ if (!signal.aborted) onStopped();
589
+ return true;
590
+ });
591
+ }
592
+ /**
593
+ * Watch one run until it reaches a terminal status.
594
+ *
595
+ * A watch rather than a subscription because a run is durable and the page is
596
+ * not: it can complete while the tab is closed, on a different sandbox, hours
597
+ * later. There is no session to reconnect — the id is the whole state.
598
+ *
599
+ * Which is also the limit of what this hook can do on its own. An id is state a
600
+ * RELOAD destroys, so a page holding nothing else comes back unable to name a
601
+ * run that is still going. The durable handle is `StartOptions.key`, read back
602
+ * with `find(workflow, key)`, and the hook that owns the id is where that
603
+ * belongs: `useWorkflowSubmit({ key, recover: true })` adopts the key's newest
604
+ * run as it mounts and passes the id here. See `_recover-run.ts` — the reason
605
+ * recovery is NOT in this hook is `reset()`, which leaves the owner holding no
606
+ * id on purpose, and a watcher that re-resolved one from a key would undo it.
607
+ *
608
+ * The stream (`GET /runs/:id/events`) is tried first and the poll is its
609
+ * fallback, so an agent deployed before that route existed still works. Watching
610
+ * STOPS on a terminal status, so a finished run costs nothing; passing
611
+ * `undefined` (nothing started yet) also costs nothing.
612
+ *
613
+ * @example
614
+ * ```tsx
615
+ * import type { ToolInputSchema, WorkflowDef } from "@alexkroman1/aai";
616
+ * import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
617
+ * import { useWorkflowRun } from "@alexkroman1/aai-ui";
618
+ *
619
+ * // A real page writes `import type { digest } from "./agent.ts"`. Stood in
620
+ * // for here so the example compiles on its own.
621
+ * declare const digest: WorkflowDef<ToolInputSchema, Promise<{ points: string[] }>>;
622
+ *
623
+ * type Digest = WorkflowOutputOf<typeof digest>;
624
+ *
625
+ * function RunPanel({ runId }: { runId: string | undefined }) {
626
+ * // The type argument is what makes `run.output` a `Digest` below.
627
+ * const { run, error, polling } = useWorkflowRun<Digest>(runId);
628
+ * if (error !== undefined) return <p role="alert">{error}</p>;
629
+ * if (run?.status === "completed") {
630
+ * return (
631
+ * <ul>
632
+ * {run.output.points.map((point) => (
633
+ * <li key={point}>{point}</li>
634
+ * ))}
635
+ * </ul>
636
+ * );
637
+ * }
638
+ * return <p>{polling ? "Working…" : "Nothing running."}</p>;
639
+ * }
640
+ * ```
641
+ *
642
+ * @typeParam R - The workflow's output type. Supplying it is what makes
643
+ * `run.status === "completed"` narrow to a typed `run.output` instead of
644
+ * `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
645
+ * type-only import of `agent.ts` is erased, so it costs the bundle nothing.
646
+ * @param runId - The run to watch. `undefined` costs nothing, so a page may
647
+ * pass its state straight through before a run exists.
648
+ * @param options - `api` when the page holds its own client; `intervalMs` to
649
+ * change the poll interval the stream falls back to.
650
+ * @returns The latest snapshot, the last read's error, and whether the watch
651
+ * is still going — see {@link UseWorkflowRunResult}.
652
+ *
653
+ * @public
654
+ */
655
+ function useWorkflowRun(runId, options = {}) {
656
+ const { api, intervalMs = DEFAULT_WORKFLOW_POLL_MS } = options;
657
+ const [run, setRun] = useState(void 0);
658
+ const [error, setError] = useState(void 0);
659
+ /**
660
+ * Has the watch stopped for a reason the snapshot does not show?
661
+ *
662
+ * Only one such reason exists — an id the agent kept reporting as unknown,
663
+ * past {@link MAX_MISSING_READS} — and it leaves `run` undefined, so `polling`
664
+ * derived from `isTerminal(run)` alone would stay true forever.
665
+ */
666
+ const [stopped, setStopped] = useState(false);
667
+ const getClient = useWorkflowApiRef(api);
668
+ useEffect(() => {
669
+ setRun(void 0);
670
+ setError(void 0);
671
+ setStopped(false);
672
+ if (!runId) return;
673
+ const onRun = (next) => {
674
+ setRun(next);
675
+ setError(void 0);
676
+ };
677
+ let stopPoll;
678
+ const stopStream = watchRunEvents(getClient, runId, onRun, () => setStopped(true), () => {
679
+ stopPoll = pollUntilTerminal(getClient, runId, intervalMs, onRun, setError, () => setStopped(true));
680
+ });
681
+ return () => {
682
+ stopStream();
683
+ stopPoll?.();
684
+ };
685
+ }, [
686
+ runId,
687
+ intervalMs,
688
+ getClient
689
+ ]);
690
+ return {
691
+ run,
692
+ error,
693
+ polling: runId !== void 0 && !stopped && !isTerminal(run)
694
+ };
695
+ }
696
+ //#endregion
697
+ export { useWorkflowProgress as a, isTerminal$1 as c, DEFAULT_PROGRESS_POLL_MS as i, MAX_MISSING_READS as n, useWorkflowApiRef as o, useWorkflowRun as r, createWorkflowApi as s, DEFAULT_WORKFLOW_POLL_MS as t };