@alexkroman1/aai-ui 13.2.0 → 13.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_colors-j8XMToi9.js → _colors-CZ6OlPbL.js} +1 -1
- package/dist/{_module-url-C_4gRVL0.js → _module-url-C13kAJ87.js} +1 -1
- package/dist/_recover-run.d.ts +11 -5
- package/dist/{_utils-B6498_bm.js → _utils-CyzjK0gW.js} +1 -1
- package/dist/{aai-logo-CXZGPSIY.js → aai-logo-CFlomZlS.js} +1 -1
- package/dist/audio.js +1 -1
- package/dist/{chat-view-DDTtrh7N.js → chat-view-Bv5VFJIE.js} +6 -6
- package/dist/{client-config-BT_kWID5.js → client-config-DD820zHn.js} +1 -1
- package/dist/client-dir.js +1 -1
- package/dist/components/button.js +2 -2
- package/dist/components/chat-view.js +1 -1
- package/dist/components/controls.js +2 -2
- package/dist/components/message-list.js +1 -1
- package/dist/components/sidebar-layout.js +1 -1
- package/dist/components/start-screen.js +4 -4
- package/dist/components/tool-call-block.js +1 -1
- package/dist/context.js +1 -1
- package/dist/default-client.d.ts +1 -1
- package/dist/define-client.js +6 -6
- package/dist/{eyebrow-C6ZFuiz6.js → eyebrow-UfmSz9yy.js} +1 -1
- package/dist/hooks.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1168 -1114
- package/dist/internal.js +5 -5
- package/dist/{message-list-BJYyuIcR.js → message-list-C0pL7x41.js} +7 -7
- package/dist/{session-core-DxBYsfHA.js → session-core-C9elBIdu.js} +11 -11
- package/dist/session-core.js +1 -1
- package/dist/{tool-call-block-tcPQAkcP.js → tool-call-block-Bunc6rCw.js} +6 -6
- package/dist/{tool-config-context-DzAofqi_.js → tool-config-context-Bh8p3DtG.js} +1 -1
- package/dist/types.js +1 -1
- package/dist/{url-chips-YqhCjWfQ.js → url-chips-C2u7QPv8.js} +3 -3
- package/dist/use-run-key.d.ts +44 -10
- package/dist/{use-user-transcript-C14qWFu2.js → use-user-transcript-DFTSEuZN.js} +1 -1
- package/dist/use-workflow-form.d.ts +31 -90
- package/dist/{use-workflow-progress-Cu0SxMyg.js → use-workflow-run-CXGEcM0l.js} +256 -256
- package/dist/use-workflow-stream.d.ts +4 -1
- package/dist/use-workflows.d.ts +77 -0
- package/dist/worklets/capture-processor.js +2 -2
- package/dist/worklets/playback-processor.js +2 -2
- package/package.json +6 -6
- package/dist/tsdown.config.d.ts +0 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { t as pageBaseUrl } from "./_utils-
|
|
1
|
+
import { t as pageBaseUrl } from "./_utils-CyzjK0gW.js";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { errorMessage } from "@alexkroman1/aai";
|
|
4
4
|
import { omitUndefined } from "@alexkroman1/aai/utils";
|
|
5
5
|
import { createWorkflowApiClient, isTerminal, isTerminal as isTerminal$1, readEventStream } from "@alexkroman1/aai/workflow-api";
|
|
6
|
-
//#region workflow-client.ts
|
|
6
|
+
//#region src/workflow-client.ts
|
|
7
7
|
/**
|
|
8
8
|
* Browser client for the workflow HTTP API (`aai/host/workflow-api.ts`).
|
|
9
9
|
*
|
|
@@ -73,7 +73,7 @@ function createWorkflowApi(opts = {}) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
//#endregion
|
|
76
|
-
//#region _workflow-api-ref.ts
|
|
76
|
+
//#region src/_workflow-api-ref.ts
|
|
77
77
|
/**
|
|
78
78
|
* The client preamble every workflow hook needs, once.
|
|
79
79
|
*
|
|
@@ -121,7 +121,7 @@ function useWorkflowApiRef(api) {
|
|
|
121
121
|
}, []);
|
|
122
122
|
}
|
|
123
123
|
//#endregion
|
|
124
|
-
//#region _repeat-until.ts
|
|
124
|
+
//#region src/_repeat-until.ts
|
|
125
125
|
/**
|
|
126
126
|
* A bounded read, re-armed from the SETTLED read — the loop both workflow
|
|
127
127
|
* watchers are built out of.
|
|
@@ -165,256 +165,7 @@ function repeatUntil(intervalMs, step) {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
//#endregion
|
|
168
|
-
//#region workflow-
|
|
169
|
-
/**
|
|
170
|
-
* Watching a run over server-sent events — the PUSH half of `useWorkflowRun`.
|
|
171
|
-
*
|
|
172
|
-
* Its own module because the seam is clean: everything in `workflow-client.ts`
|
|
173
|
-
* is request/response shaping plus the poll, and this is one long-lived stream
|
|
174
|
-
* and the SSE parser it needs.
|
|
175
|
-
*
|
|
176
|
-
* @internal
|
|
177
|
-
*/
|
|
178
|
-
/**
|
|
179
|
-
* Watch a run over SSE, falling back to the caller's poll on any failure.
|
|
180
|
-
*
|
|
181
|
-
* The poll stays the fallback rather than being replaced, and that is the whole
|
|
182
|
-
* shape of this: a stream is an optimisation over a mechanism that already
|
|
183
|
-
* works, so every way it can fail — an older agent with no `/events` route, a
|
|
184
|
-
* proxy that buffers, a network that drops it — has to degrade to the thing that
|
|
185
|
-
* does. What it buys is real, though: on the platform every polled read BROKERS,
|
|
186
|
-
* so N open tabs at `DEFAULT_WORKFLOW_POLL_MS` is N/2 brokered requests a
|
|
187
|
-
* second, each able to boot a sandbox. One stream per tab replaces all of it.
|
|
188
|
-
*
|
|
189
|
-
* `EventSource` is not used, for two reasons that both matter here: it cannot
|
|
190
|
-
* send an `Authorization` header (an agent with `AAI_WORKFLOW_API_TOKEN` set
|
|
191
|
-
* would be unreachable), and it reconnects on its own schedule, which would
|
|
192
|
-
* fight the caller's. A `fetch` stream gives both back.
|
|
193
|
-
*
|
|
194
|
-
* Returns a stop function. `onFallback` is called at most once, when this stream
|
|
195
|
-
* cannot be relied on and the poll should take over.
|
|
196
|
-
*/
|
|
197
|
-
function watchRunEvents(getClient, runId, onRun, onSettled, onFallback) {
|
|
198
|
-
const controller = new AbortController();
|
|
199
|
-
let handedOver = false;
|
|
200
|
-
const handOver = () => {
|
|
201
|
-
if (handedOver || controller.signal.aborted) return;
|
|
202
|
-
handedOver = true;
|
|
203
|
-
onFallback();
|
|
204
|
-
};
|
|
205
|
-
/**
|
|
206
|
-
* Consume the stream. Resolves `"settled"` when the run reached a state
|
|
207
|
-
* nothing will change, and `"fallback"` for every other ending — including a
|
|
208
|
-
* clean end with no final frame, which is a dropped connection.
|
|
209
|
-
*
|
|
210
|
-
* A named function rather than an inline IIFE so `watchRunEvents` stays under
|
|
211
|
-
* the cognitive-complexity cap, and so the two outcomes are a return value
|
|
212
|
-
* instead of two callbacks invoked from six places.
|
|
213
|
-
*/
|
|
214
|
-
const pump = async () => {
|
|
215
|
-
const res = await getClient().watch(runId, controller.signal);
|
|
216
|
-
if (!(res.ok && res.body)) return "fallback";
|
|
217
|
-
for await (const frame of readEventStream(res.body, controller.signal)) {
|
|
218
|
-
if (frame.event === "run" && frame.data) onRun(frame.data);
|
|
219
|
-
const outcome = endingFor(frame.event);
|
|
220
|
-
if (outcome) return outcome;
|
|
221
|
-
}
|
|
222
|
-
return "fallback";
|
|
223
|
-
};
|
|
224
|
-
pump().then((outcome) => outcome === "settled" ? onSettled() : handOver(), () => handOver());
|
|
225
|
-
return () => controller.abort();
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Does this frame END the stream, and does the run need watching afterwards?
|
|
229
|
-
*
|
|
230
|
-
* `done` and `missing` are both final and neither wants a reconnect: the run is
|
|
231
|
-
* terminal, or the id will never exist (a 404 is a stable answer — the world's
|
|
232
|
-
* record is durable). `idle` is the stream handing ITSELF back after its
|
|
233
|
-
* duration cap, so that one falls back to the poll. Anything else is not an
|
|
234
|
-
* ending.
|
|
235
|
-
*/
|
|
236
|
-
function endingFor(event) {
|
|
237
|
-
if (event === "done" || event === "missing") return "settled";
|
|
238
|
-
return event === "idle" ? "fallback" : void 0;
|
|
239
|
-
}
|
|
240
|
-
//#endregion
|
|
241
|
-
//#region use-workflow-run.ts
|
|
242
|
-
/**
|
|
243
|
-
* `useWorkflowRun` — watch one run until it settles.
|
|
244
|
-
*
|
|
245
|
-
* Split from `workflow-client.ts` on the seam that module's doc already draws:
|
|
246
|
-
* everything there is a REQUEST (one call, one answer, no React), and everything
|
|
247
|
-
* here is the loop that keeps asking. They are read for different reasons — the
|
|
248
|
-
* client is what a script or a `curl` equivalent needs, this is what a page needs
|
|
249
|
-
* — and only this half imports React.
|
|
250
|
-
*
|
|
251
|
-
* `workflow-events.ts` sits under it as the streaming fast path, and
|
|
252
|
-
* `use-workflow-form.ts` above it as the form-shaped caller.
|
|
253
|
-
*/
|
|
254
|
-
/** How often {@link useWorkflowRun} re-reads a live run when it has to poll. */
|
|
255
|
-
const DEFAULT_WORKFLOW_POLL_MS = 2e3;
|
|
256
|
-
/**
|
|
257
|
-
* Consecutive "no such run" reads {@link useWorkflowRun} tolerates before giving
|
|
258
|
-
* up on the id.
|
|
259
|
-
*
|
|
260
|
-
* Small on purpose: a 404 is a stable answer, so the budget exists only to
|
|
261
|
-
* absorb a first read that races the run's creation — not to keep hoping.
|
|
262
|
-
* Unbounded, a stale id polls (and, on the platform, BROKERS) for as long as the
|
|
263
|
-
* tab is open.
|
|
264
|
-
*/
|
|
265
|
-
const MAX_MISSING_READS = 3;
|
|
266
|
-
/**
|
|
267
|
-
* Poll `runId` until it is terminal, reporting each read. Returns a stop
|
|
268
|
-
* function.
|
|
269
|
-
*
|
|
270
|
-
* Module-level rather than inline in the hook below, so neither function carries
|
|
271
|
-
* the whole loop's branching — and so the loop can be read without React in the
|
|
272
|
-
* way.
|
|
273
|
-
*/
|
|
274
|
-
function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopped) {
|
|
275
|
-
let missing = 0;
|
|
276
|
-
/**
|
|
277
|
-
* A read that came back empty. Resolves whether the loop should STOP.
|
|
278
|
-
*
|
|
279
|
-
* A 404 is a STABLE answer — a run the agent does not know about now will not
|
|
280
|
-
* appear later — and retrying it unbounded is how a stale id (one restored
|
|
281
|
-
* from `localStorage`, or one whose agent was redeployed onto a fresh
|
|
282
|
-
* database) polls forever: the page stays `polling` and therefore busy, and on
|
|
283
|
-
* the platform every read BROKERS, so a tab's worth of dead ids keeps
|
|
284
|
-
* sandboxes resident. A small budget is kept anyway, because the first read
|
|
285
|
-
* can race a replica that has not yet seen the run.
|
|
286
|
-
*/
|
|
287
|
-
const onMissing = (signal) => {
|
|
288
|
-
missing += 1;
|
|
289
|
-
if (missing < 3) return false;
|
|
290
|
-
if (!signal.aborted) onError(`No workflow run ${runId}`);
|
|
291
|
-
return true;
|
|
292
|
-
};
|
|
293
|
-
const read = async (signal) => {
|
|
294
|
-
try {
|
|
295
|
-
const next = await getClient().get(runId);
|
|
296
|
-
if (signal.aborted) return true;
|
|
297
|
-
if (!next) return onMissing(signal);
|
|
298
|
-
missing = 0;
|
|
299
|
-
onRun(next);
|
|
300
|
-
return isTerminal(next);
|
|
301
|
-
} catch (err) {
|
|
302
|
-
if (!signal.aborted) onError(errorMessage(err));
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
return repeatUntil(intervalMs, async (signal) => {
|
|
307
|
-
if (!await read(signal)) return false;
|
|
308
|
-
if (!signal.aborted) onStopped();
|
|
309
|
-
return true;
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Watch one run until it reaches a terminal status.
|
|
314
|
-
*
|
|
315
|
-
* A watch rather than a subscription because a run is durable and the page is
|
|
316
|
-
* not: it can complete while the tab is closed, on a different sandbox, hours
|
|
317
|
-
* later. There is no session to reconnect — the id is the whole state.
|
|
318
|
-
*
|
|
319
|
-
* Which is also the limit of what this hook can do on its own. An id is state a
|
|
320
|
-
* RELOAD destroys, so a page holding nothing else comes back unable to name a
|
|
321
|
-
* run that is still going. The durable handle is `StartOptions.key`, read back
|
|
322
|
-
* with `find(workflow, key)`, and the hook that owns the id is where that
|
|
323
|
-
* belongs: `useWorkflowSubmit({ key, recover: true })` adopts the key's newest
|
|
324
|
-
* run as it mounts and passes the id here. See `_recover-run.ts` — the reason
|
|
325
|
-
* recovery is NOT in this hook is `reset()`, which leaves the owner holding no
|
|
326
|
-
* id on purpose, and a watcher that re-resolved one from a key would undo it.
|
|
327
|
-
*
|
|
328
|
-
* The stream (`GET /runs/:id/events`) is tried first and the poll is its
|
|
329
|
-
* fallback, so an agent deployed before that route existed still works. Watching
|
|
330
|
-
* STOPS on a terminal status, so a finished run costs nothing; passing
|
|
331
|
-
* `undefined` (nothing started yet) also costs nothing.
|
|
332
|
-
*
|
|
333
|
-
* @example
|
|
334
|
-
* ```tsx
|
|
335
|
-
* import type { ToolInputSchema, WorkflowDef } from "@alexkroman1/aai";
|
|
336
|
-
* import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
337
|
-
* import { useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
338
|
-
*
|
|
339
|
-
* // A real page writes `import type { digest } from "./agent.ts"`. Stood in
|
|
340
|
-
* // for here so the example compiles on its own.
|
|
341
|
-
* declare const digest: WorkflowDef<ToolInputSchema, Promise<{ points: string[] }>>;
|
|
342
|
-
*
|
|
343
|
-
* type Digest = WorkflowOutputOf<typeof digest>;
|
|
344
|
-
*
|
|
345
|
-
* function RunPanel({ runId }: { runId: string | undefined }) {
|
|
346
|
-
* // The type argument is what makes `run.output` a `Digest` below.
|
|
347
|
-
* const { run, error, polling } = useWorkflowRun<Digest>(runId);
|
|
348
|
-
* if (error !== undefined) return <p role="alert">{error}</p>;
|
|
349
|
-
* if (run?.status === "completed") {
|
|
350
|
-
* return (
|
|
351
|
-
* <ul>
|
|
352
|
-
* {run.output.points.map((point) => (
|
|
353
|
-
* <li key={point}>{point}</li>
|
|
354
|
-
* ))}
|
|
355
|
-
* </ul>
|
|
356
|
-
* );
|
|
357
|
-
* }
|
|
358
|
-
* return <p>{polling ? "Working…" : "Nothing running."}</p>;
|
|
359
|
-
* }
|
|
360
|
-
* ```
|
|
361
|
-
*
|
|
362
|
-
* @typeParam R - The workflow's output type. Supplying it is what makes
|
|
363
|
-
* `run.status === "completed"` narrow to a typed `run.output` instead of
|
|
364
|
-
* `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
|
|
365
|
-
* type-only import of `agent.ts` is erased, so it costs the bundle nothing.
|
|
366
|
-
* @param runId - The run to watch. `undefined` costs nothing, so a page may
|
|
367
|
-
* pass its state straight through before a run exists.
|
|
368
|
-
* @param opts - `api` when the page holds its own client; `intervalMs` to
|
|
369
|
-
* change the poll interval the stream falls back to.
|
|
370
|
-
* @returns The latest snapshot, the last read's error, and whether the watch
|
|
371
|
-
* is still going — see {@link UseWorkflowRunResult}.
|
|
372
|
-
*
|
|
373
|
-
* @public
|
|
374
|
-
*/
|
|
375
|
-
function useWorkflowRun(runId, opts = {}) {
|
|
376
|
-
const { api, intervalMs = DEFAULT_WORKFLOW_POLL_MS } = opts;
|
|
377
|
-
const [run, setRun] = useState(void 0);
|
|
378
|
-
const [error, setError] = useState(void 0);
|
|
379
|
-
/**
|
|
380
|
-
* Has the watch stopped for a reason the snapshot does not show?
|
|
381
|
-
*
|
|
382
|
-
* Only one such reason exists — an id the agent kept reporting as unknown,
|
|
383
|
-
* past {@link MAX_MISSING_READS} — and it leaves `run` undefined, so `polling`
|
|
384
|
-
* derived from `isTerminal(run)` alone would stay true forever.
|
|
385
|
-
*/
|
|
386
|
-
const [stopped, setStopped] = useState(false);
|
|
387
|
-
const getClient = useWorkflowApiRef(api);
|
|
388
|
-
useEffect(() => {
|
|
389
|
-
setRun(void 0);
|
|
390
|
-
setError(void 0);
|
|
391
|
-
setStopped(false);
|
|
392
|
-
if (!runId) return;
|
|
393
|
-
const onRun = (next) => {
|
|
394
|
-
setRun(next);
|
|
395
|
-
setError(void 0);
|
|
396
|
-
};
|
|
397
|
-
let stopPoll;
|
|
398
|
-
const stopStream = watchRunEvents(getClient, runId, onRun, () => setStopped(true), () => {
|
|
399
|
-
stopPoll = pollUntilTerminal(getClient, runId, intervalMs, onRun, setError, () => setStopped(true));
|
|
400
|
-
});
|
|
401
|
-
return () => {
|
|
402
|
-
stopStream();
|
|
403
|
-
stopPoll?.();
|
|
404
|
-
};
|
|
405
|
-
}, [
|
|
406
|
-
runId,
|
|
407
|
-
intervalMs,
|
|
408
|
-
getClient
|
|
409
|
-
]);
|
|
410
|
-
return {
|
|
411
|
-
run,
|
|
412
|
-
error,
|
|
413
|
-
polling: runId !== void 0 && !stopped && !isTerminal(run)
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
//#endregion
|
|
417
|
-
//#region use-workflow-progress.ts
|
|
168
|
+
//#region src/use-workflow-progress.ts
|
|
418
169
|
/**
|
|
419
170
|
* `useWorkflowProgress` — read what a run has WRITTEN while it runs.
|
|
420
171
|
*
|
|
@@ -560,7 +311,7 @@ async function consumeFrames(body, signal) {
|
|
|
560
311
|
* floor is inclusive rather than exclusive — read exclusively, this loop lost the
|
|
561
312
|
* chunk sitting AT its cursor on every re-open, so a run writing one line per
|
|
562
313
|
* poll delivered every other line.
|
|
563
|
-
* `packages/aai-runtime/workflow-stream-cursor.test.ts` states it as a property
|
|
314
|
+
* `packages/aai-runtime/src/workflow-stream-cursor.test.ts` states it as a property
|
|
564
315
|
* over generated polling schedules; this module's own spec pins the URLs.
|
|
565
316
|
*
|
|
566
317
|
* ## A negative `startIndex` is resolved on the FIRST read, not carried
|
|
@@ -684,4 +435,253 @@ function useWorkflowProgress(runId, opts = {}) {
|
|
|
684
435
|
};
|
|
685
436
|
}
|
|
686
437
|
//#endregion
|
|
687
|
-
|
|
438
|
+
//#region src/workflow-events.ts
|
|
439
|
+
/**
|
|
440
|
+
* Watching a run over server-sent events — the PUSH half of `useWorkflowRun`.
|
|
441
|
+
*
|
|
442
|
+
* Its own module because the seam is clean: everything in `workflow-client.ts`
|
|
443
|
+
* is request/response shaping plus the poll, and this is one long-lived stream
|
|
444
|
+
* and the SSE parser it needs.
|
|
445
|
+
*
|
|
446
|
+
* @internal
|
|
447
|
+
*/
|
|
448
|
+
/**
|
|
449
|
+
* Watch a run over SSE, falling back to the caller's poll on any failure.
|
|
450
|
+
*
|
|
451
|
+
* The poll stays the fallback rather than being replaced, and that is the whole
|
|
452
|
+
* shape of this: a stream is an optimisation over a mechanism that already
|
|
453
|
+
* works, so every way it can fail — an older agent with no `/events` route, a
|
|
454
|
+
* proxy that buffers, a network that drops it — has to degrade to the thing that
|
|
455
|
+
* does. What it buys is real, though: on the platform every polled read BROKERS,
|
|
456
|
+
* so N open tabs at `DEFAULT_WORKFLOW_POLL_MS` is N/2 brokered requests a
|
|
457
|
+
* second, each able to boot a sandbox. One stream per tab replaces all of it.
|
|
458
|
+
*
|
|
459
|
+
* `EventSource` is not used, for two reasons that both matter here: it cannot
|
|
460
|
+
* send an `Authorization` header (an agent with `AAI_WORKFLOW_API_TOKEN` set
|
|
461
|
+
* would be unreachable), and it reconnects on its own schedule, which would
|
|
462
|
+
* fight the caller's. A `fetch` stream gives both back.
|
|
463
|
+
*
|
|
464
|
+
* Returns a stop function. `onFallback` is called at most once, when this stream
|
|
465
|
+
* cannot be relied on and the poll should take over.
|
|
466
|
+
*/
|
|
467
|
+
function watchRunEvents(getClient, runId, onRun, onSettled, onFallback) {
|
|
468
|
+
const controller = new AbortController();
|
|
469
|
+
let handedOver = false;
|
|
470
|
+
const handOver = () => {
|
|
471
|
+
if (handedOver || controller.signal.aborted) return;
|
|
472
|
+
handedOver = true;
|
|
473
|
+
onFallback();
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* Consume the stream. Resolves `"settled"` when the run reached a state
|
|
477
|
+
* nothing will change, and `"fallback"` for every other ending — including a
|
|
478
|
+
* clean end with no final frame, which is a dropped connection.
|
|
479
|
+
*
|
|
480
|
+
* A named function rather than an inline IIFE so `watchRunEvents` stays under
|
|
481
|
+
* the cognitive-complexity cap, and so the two outcomes are a return value
|
|
482
|
+
* instead of two callbacks invoked from six places.
|
|
483
|
+
*/
|
|
484
|
+
const pump = async () => {
|
|
485
|
+
const res = await getClient().watch(runId, controller.signal);
|
|
486
|
+
if (!(res.ok && res.body)) return "fallback";
|
|
487
|
+
for await (const frame of readEventStream(res.body, controller.signal)) {
|
|
488
|
+
if (frame.event === "run" && frame.data) onRun(frame.data);
|
|
489
|
+
const outcome = endingFor(frame.event);
|
|
490
|
+
if (outcome) return outcome;
|
|
491
|
+
}
|
|
492
|
+
return "fallback";
|
|
493
|
+
};
|
|
494
|
+
pump().then((outcome) => outcome === "settled" ? onSettled() : handOver(), () => handOver());
|
|
495
|
+
return () => controller.abort();
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Does this frame END the stream, and does the run need watching afterwards?
|
|
499
|
+
*
|
|
500
|
+
* `done` and `missing` are both final and neither wants a reconnect: the run is
|
|
501
|
+
* terminal, or the id will never exist (a 404 is a stable answer — the world's
|
|
502
|
+
* record is durable). `idle` is the stream handing ITSELF back after its
|
|
503
|
+
* duration cap, so that one falls back to the poll. Anything else is not an
|
|
504
|
+
* ending.
|
|
505
|
+
*/
|
|
506
|
+
function endingFor(event) {
|
|
507
|
+
if (event === "done" || event === "missing") return "settled";
|
|
508
|
+
return event === "idle" ? "fallback" : void 0;
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/use-workflow-run.ts
|
|
512
|
+
/**
|
|
513
|
+
* `useWorkflowRun` — watch one run until it settles.
|
|
514
|
+
*
|
|
515
|
+
* Split from `workflow-client.ts` on the seam that module's doc already draws:
|
|
516
|
+
* everything there is a REQUEST (one call, one answer, no React), and everything
|
|
517
|
+
* here is the loop that keeps asking. They are read for different reasons — the
|
|
518
|
+
* client is what a script or a `curl` equivalent needs, this is what a page needs
|
|
519
|
+
* — and only this half imports React.
|
|
520
|
+
*
|
|
521
|
+
* `workflow-events.ts` sits under it as the streaming fast path, and
|
|
522
|
+
* `use-workflow-form.ts` above it as the form-shaped caller.
|
|
523
|
+
*/
|
|
524
|
+
/** How often {@link useWorkflowRun} re-reads a live run when it has to poll. */
|
|
525
|
+
const DEFAULT_WORKFLOW_POLL_MS = 2e3;
|
|
526
|
+
/**
|
|
527
|
+
* Consecutive "no such run" reads {@link useWorkflowRun} tolerates before giving
|
|
528
|
+
* up on the id.
|
|
529
|
+
*
|
|
530
|
+
* Small on purpose: a 404 is a stable answer, so the budget exists only to
|
|
531
|
+
* absorb a first read that races the run's creation — not to keep hoping.
|
|
532
|
+
* Unbounded, a stale id polls (and, on the platform, BROKERS) for as long as the
|
|
533
|
+
* tab is open.
|
|
534
|
+
*/
|
|
535
|
+
const MAX_MISSING_READS = 3;
|
|
536
|
+
/**
|
|
537
|
+
* Poll `runId` until it is terminal, reporting each read. Returns a stop
|
|
538
|
+
* function.
|
|
539
|
+
*
|
|
540
|
+
* Module-level rather than inline in the hook below, so neither function carries
|
|
541
|
+
* the whole loop's branching — and so the loop can be read without React in the
|
|
542
|
+
* way.
|
|
543
|
+
*/
|
|
544
|
+
function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopped) {
|
|
545
|
+
let missing = 0;
|
|
546
|
+
/**
|
|
547
|
+
* A read that came back empty. Resolves whether the loop should STOP.
|
|
548
|
+
*
|
|
549
|
+
* A 404 is a STABLE answer — a run the agent does not know about now will not
|
|
550
|
+
* appear later — and retrying it unbounded is how a stale id (one restored
|
|
551
|
+
* from `localStorage`, or one whose agent was redeployed onto a fresh
|
|
552
|
+
* database) polls forever: the page stays `polling` and therefore busy, and on
|
|
553
|
+
* the platform every read BROKERS, so a tab's worth of dead ids keeps
|
|
554
|
+
* sandboxes resident. A small budget is kept anyway, because the first read
|
|
555
|
+
* can race a replica that has not yet seen the run.
|
|
556
|
+
*/
|
|
557
|
+
const onMissing = (signal) => {
|
|
558
|
+
missing += 1;
|
|
559
|
+
if (missing < 3) return false;
|
|
560
|
+
if (!signal.aborted) onError(`No workflow run ${runId}`);
|
|
561
|
+
return true;
|
|
562
|
+
};
|
|
563
|
+
const read = async (signal) => {
|
|
564
|
+
try {
|
|
565
|
+
const next = await getClient().get(runId);
|
|
566
|
+
if (signal.aborted) return true;
|
|
567
|
+
if (!next) return onMissing(signal);
|
|
568
|
+
missing = 0;
|
|
569
|
+
onRun(next);
|
|
570
|
+
return isTerminal(next);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
if (!signal.aborted) onError(errorMessage(err));
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
return repeatUntil(intervalMs, async (signal) => {
|
|
577
|
+
if (!await read(signal)) return false;
|
|
578
|
+
if (!signal.aborted) onStopped();
|
|
579
|
+
return true;
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Watch one run until it reaches a terminal status.
|
|
584
|
+
*
|
|
585
|
+
* A watch rather than a subscription because a run is durable and the page is
|
|
586
|
+
* not: it can complete while the tab is closed, on a different sandbox, hours
|
|
587
|
+
* later. There is no session to reconnect — the id is the whole state.
|
|
588
|
+
*
|
|
589
|
+
* Which is also the limit of what this hook can do on its own. An id is state a
|
|
590
|
+
* RELOAD destroys, so a page holding nothing else comes back unable to name a
|
|
591
|
+
* run that is still going. The durable handle is `StartOptions.key`, read back
|
|
592
|
+
* with `find(workflow, key)`, and the hook that owns the id is where that
|
|
593
|
+
* belongs: `useWorkflowSubmit({ key, recover: true })` adopts the key's newest
|
|
594
|
+
* run as it mounts and passes the id here. See `_recover-run.ts` — the reason
|
|
595
|
+
* recovery is NOT in this hook is `reset()`, which leaves the owner holding no
|
|
596
|
+
* id on purpose, and a watcher that re-resolved one from a key would undo it.
|
|
597
|
+
*
|
|
598
|
+
* The stream (`GET /runs/:id/events`) is tried first and the poll is its
|
|
599
|
+
* fallback, so an agent deployed before that route existed still works. Watching
|
|
600
|
+
* STOPS on a terminal status, so a finished run costs nothing; passing
|
|
601
|
+
* `undefined` (nothing started yet) also costs nothing.
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* ```tsx
|
|
605
|
+
* import type { ToolInputSchema, WorkflowDef } from "@alexkroman1/aai";
|
|
606
|
+
* import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
607
|
+
* import { useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
608
|
+
*
|
|
609
|
+
* // A real page writes `import type { digest } from "./agent.ts"`. Stood in
|
|
610
|
+
* // for here so the example compiles on its own.
|
|
611
|
+
* declare const digest: WorkflowDef<ToolInputSchema, Promise<{ points: string[] }>>;
|
|
612
|
+
*
|
|
613
|
+
* type Digest = WorkflowOutputOf<typeof digest>;
|
|
614
|
+
*
|
|
615
|
+
* function RunPanel({ runId }: { runId: string | undefined }) {
|
|
616
|
+
* // The type argument is what makes `run.output` a `Digest` below.
|
|
617
|
+
* const { run, error, polling } = useWorkflowRun<Digest>(runId);
|
|
618
|
+
* if (error !== undefined) return <p role="alert">{error}</p>;
|
|
619
|
+
* if (run?.status === "completed") {
|
|
620
|
+
* return (
|
|
621
|
+
* <ul>
|
|
622
|
+
* {run.output.points.map((point) => (
|
|
623
|
+
* <li key={point}>{point}</li>
|
|
624
|
+
* ))}
|
|
625
|
+
* </ul>
|
|
626
|
+
* );
|
|
627
|
+
* }
|
|
628
|
+
* return <p>{polling ? "Working…" : "Nothing running."}</p>;
|
|
629
|
+
* }
|
|
630
|
+
* ```
|
|
631
|
+
*
|
|
632
|
+
* @typeParam R - The workflow's output type. Supplying it is what makes
|
|
633
|
+
* `run.status === "completed"` narrow to a typed `run.output` instead of
|
|
634
|
+
* `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
|
|
635
|
+
* type-only import of `agent.ts` is erased, so it costs the bundle nothing.
|
|
636
|
+
* @param runId - The run to watch. `undefined` costs nothing, so a page may
|
|
637
|
+
* pass its state straight through before a run exists.
|
|
638
|
+
* @param opts - `api` when the page holds its own client; `intervalMs` to
|
|
639
|
+
* change the poll interval the stream falls back to.
|
|
640
|
+
* @returns The latest snapshot, the last read's error, and whether the watch
|
|
641
|
+
* is still going — see {@link UseWorkflowRunResult}.
|
|
642
|
+
*
|
|
643
|
+
* @public
|
|
644
|
+
*/
|
|
645
|
+
function useWorkflowRun(runId, opts = {}) {
|
|
646
|
+
const { api, intervalMs = DEFAULT_WORKFLOW_POLL_MS } = opts;
|
|
647
|
+
const [run, setRun] = useState(void 0);
|
|
648
|
+
const [error, setError] = useState(void 0);
|
|
649
|
+
/**
|
|
650
|
+
* Has the watch stopped for a reason the snapshot does not show?
|
|
651
|
+
*
|
|
652
|
+
* Only one such reason exists — an id the agent kept reporting as unknown,
|
|
653
|
+
* past {@link MAX_MISSING_READS} — and it leaves `run` undefined, so `polling`
|
|
654
|
+
* derived from `isTerminal(run)` alone would stay true forever.
|
|
655
|
+
*/
|
|
656
|
+
const [stopped, setStopped] = useState(false);
|
|
657
|
+
const getClient = useWorkflowApiRef(api);
|
|
658
|
+
useEffect(() => {
|
|
659
|
+
setRun(void 0);
|
|
660
|
+
setError(void 0);
|
|
661
|
+
setStopped(false);
|
|
662
|
+
if (!runId) return;
|
|
663
|
+
const onRun = (next) => {
|
|
664
|
+
setRun(next);
|
|
665
|
+
setError(void 0);
|
|
666
|
+
};
|
|
667
|
+
let stopPoll;
|
|
668
|
+
const stopStream = watchRunEvents(getClient, runId, onRun, () => setStopped(true), () => {
|
|
669
|
+
stopPoll = pollUntilTerminal(getClient, runId, intervalMs, onRun, setError, () => setStopped(true));
|
|
670
|
+
});
|
|
671
|
+
return () => {
|
|
672
|
+
stopStream();
|
|
673
|
+
stopPoll?.();
|
|
674
|
+
};
|
|
675
|
+
}, [
|
|
676
|
+
runId,
|
|
677
|
+
intervalMs,
|
|
678
|
+
getClient
|
|
679
|
+
]);
|
|
680
|
+
return {
|
|
681
|
+
run,
|
|
682
|
+
error,
|
|
683
|
+
polling: runId !== void 0 && !stopped && !isTerminal(run)
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
//#endregion
|
|
687
|
+
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 };
|
|
@@ -93,7 +93,10 @@ import type { SubmitInputOf } from "./workflow-def-types.ts";
|
|
|
93
93
|
* would sit waiting for bytes nobody is sending until its own abandonment
|
|
94
94
|
* bound. That is the same reason `_upload-recall.ts` deliberately does not
|
|
95
95
|
* recall for this hook, one layer up: here the id is part of a run's INPUT.
|
|
96
|
-
* `key` itself still works, and still makes the run findable
|
|
96
|
+
* `key` itself still works, and still makes the run findable — but it is NOT
|
|
97
|
+
* defaulted here the way `useWorkflowSubmit` defaults it, because the whole
|
|
98
|
+
* value of that default is the lookup this hook refuses, and minting a key
|
|
99
|
+
* nothing will ever read back is a slot left in storage for no one.
|
|
97
100
|
*
|
|
98
101
|
* `parallel` COMPOSES with what this hook is for rather than competing with it.
|
|
99
102
|
* The run still starts before the bytes, and the store still publishes how far
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent's declared workflows — what a form is rendered FROM.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `use-workflow-form.ts` at the 500-line cap, along the seam that
|
|
5
|
+
* file's own doc already drew: it held "the two hooks a FORM needs", and only
|
|
6
|
+
* one of them is about a RUN. This is the other one, and it shares nothing with
|
|
7
|
+
* its former neighbours but the client ref every hook here uses — no state, no
|
|
8
|
+
* run id, no upload.
|
|
9
|
+
*/
|
|
10
|
+
import type { WorkflowSummary } from "@alexkroman1/aai/workflow-api";
|
|
11
|
+
import type { WorkflowApi } from "./workflow-client.ts";
|
|
12
|
+
/** Options for {@link useWorkflows}. */
|
|
13
|
+
export type UseWorkflowsOptions = {
|
|
14
|
+
/** The client to read the listing with. Defaults to one for the page's own agent. */
|
|
15
|
+
api?: WorkflowApi;
|
|
16
|
+
/**
|
|
17
|
+
* Skip the lookup entirely, reporting an empty listing that is not loading.
|
|
18
|
+
*
|
|
19
|
+
* For a caller that may or may not need the listing and cannot decide with a
|
|
20
|
+
* conditional hook — `<WorkflowFields>` handed a summary rather than a name is
|
|
21
|
+
* the one in this package. It reports `loading: false`, because a skipped
|
|
22
|
+
* lookup is finished rather than pending.
|
|
23
|
+
*/
|
|
24
|
+
skip?: boolean;
|
|
25
|
+
};
|
|
26
|
+
/** What {@link useWorkflows} reports. */
|
|
27
|
+
export type UseWorkflowsResult = {
|
|
28
|
+
/** The agent's declared workflows, each with the JSON Schema of its input. */
|
|
29
|
+
workflows: WorkflowSummary[];
|
|
30
|
+
/** True until the listing lands, so a form can hold its fields back. */
|
|
31
|
+
loading: boolean;
|
|
32
|
+
/** The lookup's failure. Set alongside an EMPTY list, which is why it exists. */
|
|
33
|
+
error: string | undefined;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Read the agent's declared workflows.
|
|
37
|
+
*
|
|
38
|
+
* What `<WorkflowFields>` renders a form FROM: each summary carries the JSON
|
|
39
|
+
* Schema of that workflow's input, converted server-side precisely so a browser
|
|
40
|
+
* can read it.
|
|
41
|
+
*
|
|
42
|
+
* The failure is reported rather than swallowed, because the alternative is an
|
|
43
|
+
* empty list — which renders as a form with no fields and reads as "this agent
|
|
44
|
+
* declares no workflows" about an agent that was merely unreachable.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* import { useWorkflows } from "@alexkroman1/aai-ui";
|
|
49
|
+
*
|
|
50
|
+
* // A page rendering its own chrome from the listing — a picker, say. A form
|
|
51
|
+
* // for ONE workflow wants `<WorkflowFields workflow="name" />` instead,
|
|
52
|
+
* // which does this lookup itself.
|
|
53
|
+
* function WorkflowPicker({ onPick }: { onPick: (name: string) => void }) {
|
|
54
|
+
* const { workflows, loading, error } = useWorkflows();
|
|
55
|
+
* if (loading) return <p>Loading…</p>;
|
|
56
|
+
* if (error !== undefined) return <p role="alert">{error}</p>;
|
|
57
|
+
* return (
|
|
58
|
+
* <ul>
|
|
59
|
+
* {workflows.map((summary) => (
|
|
60
|
+
* <li key={summary.name}>
|
|
61
|
+
* <button type="button" onClick={() => onPick(summary.name)}>
|
|
62
|
+
* {summary.description ?? summary.name}
|
|
63
|
+
* </button>
|
|
64
|
+
* </li>
|
|
65
|
+
* ))}
|
|
66
|
+
* </ul>
|
|
67
|
+
* );
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @param opts - See {@link UseWorkflowsOptions}.
|
|
72
|
+
* @returns The listing, its loading flag and its failure — see
|
|
73
|
+
* {@link UseWorkflowsResult}.
|
|
74
|
+
*
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
export declare function useWorkflows(opts?: UseWorkflowsOptions): UseWorkflowsResult;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MIC_BUFFER_SECONDS, MIC_SILENCE_PROBE_MS } from "../types.js";
|
|
2
|
-
import { t as workletModuleUrl } from "../_module-url-
|
|
3
|
-
//#region worklets/capture-processor.ts
|
|
2
|
+
import { t as workletModuleUrl } from "../_module-url-C13kAJ87.js";
|
|
3
|
+
//#region src/worklets/capture-processor.ts
|
|
4
4
|
/** Raw worklet source — exported so tests can evaluate the processor directly. */
|
|
5
5
|
const captureProcessorSource = `
|
|
6
6
|
class CaptureProcessor extends AudioWorkletProcessor {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS } from "../types.js";
|
|
2
|
-
import { t as workletModuleUrl } from "../_module-url-
|
|
3
|
-
//#region worklets/playback-processor.ts
|
|
2
|
+
import { t as workletModuleUrl } from "../_module-url-C13kAJ87.js";
|
|
3
|
+
//#region src/worklets/playback-processor.ts
|
|
4
4
|
/** Raw worklet source — exported so tests can evaluate the processor directly. */
|
|
5
5
|
const playbackProcessorSource = `
|
|
6
6
|
class PlaybackProcessor extends AudioWorkletProcessor {
|