@alexkroman1/aai-ui 5.14.0 → 6.2.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 (53) hide show
  1. package/README.md +2 -1
  2. package/dist/_repeat-until.d.ts +30 -0
  3. package/dist/_sse.d.ts +56 -0
  4. package/dist/_workflow-api-ref.d.ts +37 -0
  5. package/dist/audio.js +26 -26
  6. package/dist/{chat-view-CgFytvGy.js → chat-view-CK61bWWx.js} +2 -1
  7. package/dist/components/_form-values.d.ts +19 -0
  8. package/dist/components/chat-view.js +1 -1
  9. package/dist/components/form-types.d.ts +67 -0
  10. package/dist/components/form.d.ts +138 -0
  11. package/dist/components/message-list.js +1 -1
  12. package/dist/components/workflow-fields.d.ts +57 -0
  13. package/dist/components/workflow-progress.d.ts +55 -0
  14. package/dist/default-client/assets/audio-fO7SVU64.js +1 -0
  15. package/dist/default-client/assets/{capture-processor-B_5Ive8e.js → capture-processor-Dmc-KEpb.js} +4 -4
  16. package/dist/default-client/assets/client-audio-constants-Ck0IJO4c.js +1 -0
  17. package/dist/default-client/assets/index-CDugAuLK.css +2 -0
  18. package/dist/default-client/assets/index-DCI51Xz_.js +293 -0
  19. package/dist/default-client/assets/{playback-processor-6L8SIQ_l.js → playback-processor-DwQ9tE7X.js} +16 -13
  20. package/dist/default-client/index.html +3 -2
  21. package/dist/define-client.d.ts +40 -1
  22. package/dist/define-client.js +59 -17
  23. package/dist/index.d.ts +10 -0
  24. package/dist/index.js +1595 -5
  25. package/dist/{message-list-CcjgWRVZ.js → message-list-BwA3rdPi.js} +15 -1
  26. package/dist/page.d.ts +88 -0
  27. package/dist/{session-core-BA8H3qtF.js → session-core-ClKdVgRU.js} +245 -112
  28. package/dist/session-core-dial.d.ts +38 -0
  29. package/dist/session-core-handshake.d.ts +16 -1
  30. package/dist/session-core-messages.d.ts +2 -2
  31. package/dist/session-core-reconnect.d.ts +2 -7
  32. package/dist/session-core.js +1 -1
  33. package/dist/session-resume-store.d.ts +43 -0
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.js +2 -2
  36. package/dist/use-user-transcript.d.ts +70 -0
  37. package/dist/use-workflow-form.d.ts +136 -0
  38. package/dist/use-workflow-progress.d.ts +100 -0
  39. package/dist/use-workflow-run.d.ts +56 -0
  40. package/dist/use-workflow-runs.d.ts +71 -0
  41. package/dist/workflow-client.d.ts +97 -0
  42. package/dist/workflow-events.d.ts +39 -0
  43. package/dist/worklets/_playback-bench-harness.d.ts +181 -0
  44. package/dist/worklets/_playback-bench-host.d.ts +63 -0
  45. package/dist/worklets/_playback-bench-page.d.ts +65 -0
  46. package/dist/worklets/_tts-trace-harness.d.ts +142 -0
  47. package/dist/worklets/_worklet-test-utils.d.ts +27 -0
  48. package/dist/worklets/playback-processor.d.ts +1 -1
  49. package/dist/worklets/playback-processor.js +15 -12
  50. package/package.json +9 -8
  51. package/dist/default-client/assets/audio-CsQVQn3f.js +0 -1
  52. package/dist/default-client/assets/index-D35_z2WM.js +0 -293
  53. package/dist/default-client/assets/index-DCjB3qtb.css +0 -2
@@ -4,8 +4,23 @@ export declare const HANDSHAKE_ERROR: SessionError;
4
4
  export type HandshakeGuard = {
5
5
  /** Start the deadline for the attempt that just opened. */
6
6
  arm(): void;
7
- /** Stop it — `config` arrived, or this socket is closing. */
7
+ /** Stop it — this socket is closing, or the connection is being torn down. */
8
8
  disarm(): void;
9
+ /**
10
+ * The `config` frame arrived: stop the deadline AND spend nothing.
11
+ *
12
+ * Separate from {@link HandshakeGuard.disarm} because the budget is
13
+ * CONSECUTIVE, and only a completed handshake proves the peer is healthy. One
14
+ * guard covers a whole `connect()`, partysocket's retries included, so with a
15
+ * plain disarm the count survived every successful session in between: an
16
+ * hour-long call whose socket dropped three times, each drop timing out once
17
+ * before the next attempt succeeded, surfaced the permanent
18
+ * "Agent did not complete the session handshake" error against a peer that
19
+ * had answered every time. A close must NOT reset it — a wedged peer closes
20
+ * and reopens on its own, and resetting there is the unbounded re-dial loop
21
+ * the budget exists to bound.
22
+ */
23
+ succeeded(): void;
9
24
  };
10
25
  /**
11
26
  * Watch one connection's handshake.
@@ -1,7 +1,7 @@
1
1
  import type { ConnState, SessionSnapshot } from "./session-core-types.ts";
2
2
  /**
3
3
  * Snapshot fields cleared when a session's conversation state is wiped —
4
- * shared by the initial snapshot, `resetState()`, and the server `reset` event.
4
+ * shared by the initial snapshot, `resetState()`, and `session.reset`.
5
5
  * The empty arrays are safe to share: snapshot collections are never mutated
6
6
  * in place, only replaced.
7
7
  */
@@ -41,7 +41,7 @@ type MessageHandlers = {
41
41
  handleMessage(data: unknown): SessionConfigMessage | undefined;
42
42
  /**
43
43
  * Wait for `io`'s playback queue to drain, then transition to `"listening"`
44
- * — guarded by the same turn-boundary generation the live `audio_done`
44
+ * — guarded by the same turn-boundary generation the live `audio.completed`
45
45
  * path uses. `initAudioCapture` routes the pre-init greeting replay
46
46
  * through this so a barge-in mid-greeting can't be stomped by the
47
47
  * replayed completion resolving late.
@@ -1,16 +1,11 @@
1
- /**
2
- * Automatic reconnection for the browser session socket, built on
3
- * partysocket's `ReconnectingWebSocket`. Kept out of `session-core.ts` so
4
- * the state machine there reads as protocol logic, not socket plumbing.
5
- */
6
- import ReconnectingWebSocket from "partysocket/ws";
1
+ import type { WebSocketConstructor } from "./types.ts";
7
2
  /**
8
3
  * Open partysocket's reconnecting WebSocket. The URL is a *provider*,
9
4
  * re-evaluated on every attempt (async supported), so each retry picks up
10
5
  * the current broker-named endpoint and resume URL rather than the ones the
11
6
  * session started with.
12
7
  */
13
- export declare function openReconnectingSocket(urlProvider: () => Promise<string>): ReconnectingWebSocket;
8
+ export declare function openReconnectingSocket(urlProvider: () => Promise<string>): InstanceType<WebSocketConstructor>;
14
9
  /**
15
10
  * True while `socket` is a reconnecting socket that will retry after the
16
11
  * close event currently being handled. partysocket schedules the retry
@@ -1,2 +1,2 @@
1
- import { t as createSessionCore } from "./session-core-BA8H3qtF.js";
1
+ import { t as createSessionCore } from "./session-core-ClKdVgRU.js";
2
2
  export { createSessionCore };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Where a session id survives a page RELOAD.
3
+ *
4
+ * The id is what `?sessionId=` presents on reconnect, and it is the key the
5
+ * agent's slot state and event log live under — so a reload that cannot produce
6
+ * it starts a brand-new session, and a UI driven by `useAgentState` comes back
7
+ * empty even though the agent still holds the cart. The server side of the
8
+ * reconstitution was already built (`pushStateSnapshot` force-pushes the
9
+ * projection after hydration on every start, `state.updated` lands in
10
+ * `agentState`); what was missing is that nothing in the browser remembered the
11
+ * id across a reload. `onSessionId`/`resumeSessionId` let a client wire it by
12
+ * hand and exactly one of fourteen templates did, which is the shape of a
13
+ * default in the wrong place.
14
+ *
15
+ * **`sessionStorage`, deliberately, and this is the opposite call from the
16
+ * studio's session token.** A reload and a same-tab navigation survive it; a new
17
+ * tab and a visit tomorrow do not, which is what we want here rather than a
18
+ * limitation: presenting a day-old id suppresses the greeting
19
+ * (`parseWsUpgradeParams` keys that off the id's mere presence) and rejoins a
20
+ * conversation whose context is long gone. The studio token is a credential
21
+ * whose value is not being asked to sign out; this is a pointer into a live call.
22
+ *
23
+ * Keyed by the agent's own URL, so two agents served from one origin — which is
24
+ * every deployed agent, at `/:slug/` — cannot inherit each other's session.
25
+ *
26
+ * Every access is guarded: storage throws outright in some contexts (Safari
27
+ * private mode, storage blocked by policy), and a session that cannot be
28
+ * remembered must degrade to today's behaviour rather than failing to start.
29
+ */
30
+ /** The stored session id for this agent, or undefined. @internal */
31
+ export declare function readStoredSessionId(platformUrl: string): string | undefined;
32
+ /** Remember this agent's session id for the next load. @internal */
33
+ export declare function writeStoredSessionId(platformUrl: string, sessionId: string): void;
34
+ /**
35
+ * Forget it, so the next load is a NEW session.
36
+ *
37
+ * Called from `end()`, which is the clear-and-forget the "New Conversation"
38
+ * button runs: leaving the id behind there would have the next load rejoin the
39
+ * conversation the user just discarded, greeting suppressed.
40
+ *
41
+ * @internal
42
+ */
43
+ export declare function clearStoredSessionId(platformUrl: string): void;
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { DefaultToolResult } from "@alexkroman1/aai";
2
2
  import type { SessionErrorCode } from "@alexkroman1/aai/protocol";
3
- export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_PROGRESS_INTERVAL_MS, PLAYBACK_REFILL_MS, } from "@alexkroman1/aai";
3
+ export { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PACER_BURST_MS, PIPELINE_PLAYBACK_GRACE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS, } from "@alexkroman1/aai/internal";
4
4
  /**
5
5
  * `getUserMedia` audio constraints for every capture path in this package.
6
6
  *
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_PROGRESS_INTERVAL_MS, PLAYBACK_REFILL_MS } from "@alexkroman1/aai";
1
+ import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PACER_BURST_MS, PIPELINE_PLAYBACK_GRACE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS } from "@alexkroman1/aai/internal";
2
2
  //#region types.ts
3
3
  /**
4
4
  * `getUserMedia` audio constraints for every capture path in this package.
@@ -32,4 +32,4 @@ const VOICE_CAPTURE_CONSTRAINTS = {
32
32
  voiceIsolation: false
33
33
  };
34
34
  //#endregion
35
- export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_PROGRESS_INTERVAL_MS, PLAYBACK_REFILL_MS, VOICE_CAPTURE_CONSTRAINTS };
35
+ export { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PACER_BURST_MS, PIPELINE_PLAYBACK_GRACE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS, VOICE_CAPTURE_CONSTRAINTS };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `useUserTranscript` — what the caller is saying RIGHT NOW, read correctly.
3
+ *
4
+ * `SessionSnapshot.userTranscript` is `string | null`, and the two falsy values
5
+ * mean different things:
6
+ *
7
+ * - `null` — nobody is speaking. There is no partial turn.
8
+ * - `""` — speech HAS been detected and no words have come back yet. A live
9
+ * session sits here for a few hundred milliseconds at the start of every turn.
10
+ *
11
+ * Read as one falsy check, those collapse and the indicator never appears at the
12
+ * start of a turn — which is the moment it is for. So every custom chrome writes
13
+ * `transcript !== null && (transcript === "" ? "…" : transcript)`, three
14
+ * templates did exactly that, and each one re-derived a protocol distinction
15
+ * from the type rather than from anything that told them.
16
+ *
17
+ * This is the same distinction as two named booleans, so a component can say
18
+ * what it means: render on `speaking`, show `text`, and use the SDK's own
19
+ * placeholder when there is nothing to show yet.
20
+ */
21
+ /**
22
+ * Placeholder for "listening, no words yet" — the `""` case above.
23
+ *
24
+ * A one-character ellipsis rather than three dots, because it is read by a
25
+ * screen reader as an ellipsis and it does not reflow the row when the first
26
+ * real word replaces it.
27
+ *
28
+ * @public
29
+ */
30
+ export declare const TRANSCRIBING_PLACEHOLDER = "\u2026";
31
+ /** What {@link useUserTranscript} returns. */
32
+ export interface UseUserTranscriptResult {
33
+ /**
34
+ * True while the caller holds the turn — from speech detection to the final
35
+ * transcript. This is the flag a live-transcript row renders on.
36
+ */
37
+ speaking: boolean;
38
+ /**
39
+ * The words so far, or {@link TRANSCRIBING_PLACEHOLDER} while there are none.
40
+ * Empty string when nobody is speaking.
41
+ */
42
+ text: string;
43
+ /**
44
+ * The raw partial: the words so far, `""` while there are none, and `null`
45
+ * when nobody is speaking. For a chrome that wants to render its own
46
+ * placeholder (or none).
47
+ */
48
+ partial: string | null;
49
+ }
50
+ /**
51
+ * Subscribe to the caller's in-progress turn.
52
+ *
53
+ * Narrowly subscribed — a component using this re-renders at STT-partial rate,
54
+ * which is exactly what it is for and exactly what a whole-page `useSession()`
55
+ * should not do.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * import { useUserTranscript } from "@alexkroman1/aai-ui";
60
+ *
61
+ * function LiveTranscript() {
62
+ * const { speaking, text } = useUserTranscript();
63
+ * if (!speaking) return null;
64
+ * return <div className="italic opacity-60">{text}</div>;
65
+ * }
66
+ * ```
67
+ *
68
+ * @public
69
+ */
70
+ export declare function useUserTranscript(): UseUserTranscriptResult;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The two hooks a FORM needs, as against the one a status view does.
3
+ *
4
+ * `useWorkflowRun` (`workflow-client.ts`) watches a run you already have.
5
+ * These two are what comes before it: `useWorkflows` reads the declared
6
+ * workflows so `<WorkflowFields>` can render a form from a schema, and
7
+ * `useWorkflowSubmit` starts a run and hands the id straight to
8
+ * `useWorkflowRun`.
9
+ *
10
+ * ## `useWorkflowSubmit` — a form's two halves in one hook
11
+ *
12
+ * A page that submits a workflow always needs the same four pieces of state:
13
+ * the run id, whether a submit is in flight, whether the RUN is still going, and
14
+ * whichever of the two failed. `link-digest` writes them out by hand, which is
15
+ * the right shape for a template teaching the primitives and the wrong shape to
16
+ * write a third time — and it is easy to get subtly wrong: dropping the previous
17
+ * run id before the new `POST` returns is what stops a finished result sitting
18
+ * under a form that is already submitting again.
19
+ *
20
+ * So this is `api.start` plus {@link useWorkflowRun}, with the state between
21
+ * them. It adds no transport of its own and holds no run state of its own; the
22
+ * watching (stream first, poll as its fallback, terminal stops) is entirely
23
+ * `useWorkflowRun`'s, and `run` here IS its run.
24
+ *
25
+ * ## Why it starts ASYNCHRONOUSLY even though a synchronous call exists
26
+ *
27
+ * `api.startAndWait` would collapse this to one request, and it is the wrong
28
+ * default for a page: it holds a socket open for up to a minute, answers nothing
29
+ * until it settles, and a page has `useWorkflowRun` — which survives a reload,
30
+ * shows progress, and costs one stream. The synchronous call is for callers with
31
+ * nowhere to put a watch (a script, a cron, a form POST from a server). Pass
32
+ * `wait` here when the page really does want one request, and the run is
33
+ * followed from the same id either way.
34
+ */
35
+ import { type WorkflowSummary } from "@alexkroman1/aai";
36
+ import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
37
+ /** Options for {@link useWorkflows}. */
38
+ export type UseWorkflowsOptions = {
39
+ /** The client to read the listing with. Defaults to one for the page's own agent. */
40
+ api?: WorkflowApi;
41
+ /**
42
+ * Skip the lookup entirely, reporting an empty listing that is not loading.
43
+ *
44
+ * For a caller that may or may not need the listing and cannot decide with a
45
+ * conditional hook — `<WorkflowFields>` handed a summary rather than a name is
46
+ * the one in this package. It reports `loading: false`, because a skipped
47
+ * lookup is finished rather than pending.
48
+ */
49
+ skip?: boolean;
50
+ };
51
+ /** What {@link useWorkflows} reports. */
52
+ export type UseWorkflowsResult = {
53
+ /** The agent's declared workflows, each with the JSON Schema of its input. */
54
+ workflows: WorkflowSummary[];
55
+ /** True until the listing lands, so a form can hold its fields back. */
56
+ loading: boolean;
57
+ /** The lookup's failure. Set alongside an EMPTY list, which is why it exists. */
58
+ error: string | undefined;
59
+ };
60
+ /**
61
+ * Read the agent's declared workflows.
62
+ *
63
+ * What `<WorkflowFields>` renders a form FROM: each summary carries the JSON
64
+ * Schema of that workflow's input, converted server-side precisely so a browser
65
+ * can read it.
66
+ *
67
+ * The failure is reported rather than swallowed, because the alternative is an
68
+ * empty list — which renders as a form with no fields and reads as "this agent
69
+ * declares no workflows" about an agent that was merely unreachable.
70
+ *
71
+ * @public
72
+ */
73
+ export declare function useWorkflows(opts?: UseWorkflowsOptions): UseWorkflowsResult;
74
+ /** What {@link useWorkflowSubmit} returns. */
75
+ export type WorkflowSubmission<R = unknown> = {
76
+ /**
77
+ * Start a run with this input. Resolves once the run EXISTS — progress
78
+ * arrives through `run` — so a `<Form>`'s handler can await it to know the
79
+ * submission was accepted.
80
+ */
81
+ submit: (input: unknown) => Promise<void>;
82
+ /** Clear the run and any error, putting the form back to its initial state. */
83
+ reset: () => void;
84
+ /** The run, once started, followed to completion. */
85
+ run: WorkflowRun<R> | undefined;
86
+ /**
87
+ * True from `submit()` until the run reaches a terminal status.
88
+ *
89
+ * The WORK, not the request: a run outlives its `POST`, and a submit button
90
+ * that re-enabled on the response would invite a second submission of work
91
+ * already in flight.
92
+ */
93
+ pending: boolean;
94
+ /** The submit's own failure (a rejected input), or the watch's. */
95
+ error: string | undefined;
96
+ };
97
+ /** Options for {@link useWorkflowSubmit}. */
98
+ export type UseWorkflowSubmitOptions = {
99
+ /** The client to start runs with. Defaults to one for the page's own agent. */
100
+ api?: WorkflowApi;
101
+ /** Correlation key recorded with the run, for finding it again without the id. */
102
+ key?: string;
103
+ /**
104
+ * Hold the `POST` open until the run settles, up to this many ms — the
105
+ * synchronous mode. Omitted (the default) returns as soon as the run exists.
106
+ */
107
+ wait?: number;
108
+ /** How often the fallback poll re-reads a live run. */
109
+ intervalMs?: number;
110
+ };
111
+ /**
112
+ * Start a workflow from a form, and follow the run it creates.
113
+ *
114
+ * @typeParam R - The workflow's output type, which is what makes
115
+ * `run.status === "completed"` narrow to a typed `run.output`. Derive it with
116
+ * `WorkflowOutputOf<typeof myWorkflow>`.
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * import { Form, SubmitButton, TextField, useWorkflowSubmit } from "@alexkroman1/aai-ui";
121
+ *
122
+ * function DigestForm() {
123
+ * const { submit, run, pending, error } = useWorkflowSubmit("digest");
124
+ * return (
125
+ * <Form onSubmit={(values) => submit(values)} error={error}>
126
+ * <TextField name="url" label="Link" type="url" required />
127
+ * <SubmitButton pending={pending}>Digest</SubmitButton>
128
+ * {run?.status === "completed" && <p>Done.</p>}
129
+ * </Form>
130
+ * );
131
+ * }
132
+ * ```
133
+ *
134
+ * @public
135
+ */
136
+ export declare function useWorkflowSubmit<R = unknown>(workflow: string, opts?: UseWorkflowSubmitOptions): WorkflowSubmission<R>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `useWorkflowProgress` — read what a run has WRITTEN while it runs.
3
+ *
4
+ * The sibling of `useWorkflowRun`, and the split between them is the whole
5
+ * reason this exists. That hook reports a run's STATE: the status transitions
6
+ * the world records, which every run has. This reports what the run itself
7
+ * wrote through `getWritable()`, which is the only thing a long run can say
8
+ * before it finishes — a snapshot carries a status and, once terminal, an
9
+ * output, and nothing in between. A page that shows only status shows
10
+ * "Working…" for ten minutes and then a result.
11
+ *
12
+ * ## There is no poll fallback, and that is not an omission
13
+ *
14
+ * `useWorkflowRun` degrades to polling `GET /runs/:id` because a run's STATE is
15
+ * readable that way. A run's written chunks are not: the stream is the only
16
+ * route to them, so an agent that does not serve it has no progress to give and
17
+ * `supported` says so once, rather than a poll pretending to look for something
18
+ * that is not there. A page renders its status line either way.
19
+ *
20
+ * ## Chunks are RETAINED, so this is a replay as much as a tail
21
+ *
22
+ * The run's stream keeps every chunk, so a page that mounts late — a reload, a
23
+ * second tab, a link opened tomorrow — reads the whole history from index 0 and
24
+ * arrives at the same list as one that watched throughout. That is what makes a
25
+ * durable run's progress durable too, and it is why the default `startIndex` is
26
+ * 0 rather than "from now": a tail-only default would make the same page show
27
+ * different things depending on when it opened.
28
+ *
29
+ * ## It RE-OPENS while the run is live, because a progress read is bounded
30
+ *
31
+ * The route answers with the chunks written when the request arrived and then
32
+ * ends, reporting `complete` — whether the run itself was terminal. It has to:
33
+ * a workflow stream signals its end only once CLOSED, and a progress channel
34
+ * written by one step after another is never closed, so a read that waited for
35
+ * the end would hang forever on a finished run. (It did: see the route's own
36
+ * doc.)
37
+ *
38
+ * So this hook re-opens from where it left off until a read comes back
39
+ * `complete`. That is a poll, and the honest description of progress is a durable
40
+ * log rather than a socket — but it is a poll of a CHEAP shape: each read asks
41
+ * only for chunks past the last index it saw, so a quiet run costs an empty
42
+ * answer rather than the whole log again.
43
+ */
44
+ import type { WorkflowApi } from "./workflow-client.ts";
45
+ /** The slice of the client this needs: one method. */
46
+ export type RunProgressReader = Pick<WorkflowApi, "streamOutput">;
47
+ export type UseWorkflowProgressResult<T = string> = {
48
+ /** Every chunk the run has written, oldest first. */
49
+ progress: T[];
50
+ /** The newest chunk, or undefined before the first one lands. */
51
+ latest: T | undefined;
52
+ /** True while the run is still being read — it may yet say more. */
53
+ streaming: boolean;
54
+ /**
55
+ * False once the agent has answered that it does not serve this route.
56
+ *
57
+ * Distinguishes "this deploy predates progress streams" from "the run has not
58
+ * written anything yet", which look identical from `progress` alone. A page
59
+ * uses it to hide the section rather than show an empty one forever.
60
+ */
61
+ supported: boolean;
62
+ };
63
+ /** How often a live run's progress is re-read once a bounded read has ended. */
64
+ export declare const DEFAULT_PROGRESS_POLL_MS = 1000;
65
+ /**
66
+ * Follow one run's progress stream.
67
+ *
68
+ * Passing `undefined` (nothing started yet) costs nothing, and reading stops for
69
+ * good once a read reports the run terminal — so a finished run costs one read.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * import { useWorkflowProgress } from "@alexkroman1/aai-ui";
74
+ *
75
+ * function Progress({ runId }: { runId?: string }) {
76
+ * const { progress, streaming, supported } = useWorkflowProgress(runId);
77
+ * if (!supported) return null;
78
+ * return (
79
+ * <pre>
80
+ * {progress.join("\n")}
81
+ * {streaming && "\n…"}
82
+ * </pre>
83
+ * );
84
+ * }
85
+ * ```
86
+ *
87
+ * @typeParam T - What the workflow writes. Defaults to `string`, which is what
88
+ * a progress channel usually carries; a workflow writing objects names its own
89
+ * shape. Nothing in the browser can verify it — the route describes no type —
90
+ * so this is the page's assertion about its own agent, narrowed once here
91
+ * rather than at every read.
92
+ *
93
+ * @public
94
+ */
95
+ export declare function useWorkflowProgress<T = string>(runId: string | undefined, opts?: {
96
+ api?: WorkflowApi;
97
+ namespace?: string;
98
+ startIndex?: number;
99
+ intervalMs?: number;
100
+ }): UseWorkflowProgressResult<T>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `useWorkflowRun` — watch one run until it settles.
3
+ *
4
+ * Split from `workflow-client.ts` on the seam that module's doc already draws:
5
+ * everything there is a REQUEST (one call, one answer, no React), and everything
6
+ * here is the loop that keeps asking. They are read for different reasons — the
7
+ * client is what a script or a `curl` equivalent needs, this is what a page needs
8
+ * — and only this half imports React.
9
+ *
10
+ * `workflow-events.ts` sits under it as the streaming fast path, and
11
+ * `use-workflow-form.ts` above it as the form-shaped caller.
12
+ */
13
+ import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
14
+ /** How often {@link useWorkflowRun} re-reads a live run when it has to poll. */
15
+ export declare const DEFAULT_WORKFLOW_POLL_MS = 2000;
16
+ /**
17
+ * Consecutive "no such run" reads {@link useWorkflowRun} tolerates before giving
18
+ * up on the id.
19
+ *
20
+ * Small on purpose: a 404 is a stable answer, so the budget exists only to
21
+ * absorb a first read that races the run's creation — not to keep hoping.
22
+ * Unbounded, a stale id polls (and, on the platform, BROKERS) for as long as the
23
+ * tab is open.
24
+ */
25
+ export declare const MAX_MISSING_READS = 3;
26
+ export type UseWorkflowRunResult<R = unknown> = {
27
+ /** Latest snapshot, or undefined before the first read lands. */
28
+ run: WorkflowRun<R> | undefined;
29
+ /** The last read's failure, cleared by the next successful one. */
30
+ error: string | undefined;
31
+ /** True while a non-terminal run is still being watched. */
32
+ polling: boolean;
33
+ };
34
+ /**
35
+ * Watch one run until it reaches a terminal status.
36
+ *
37
+ * A watch rather than a subscription because a run is durable and the page is
38
+ * not: it can complete while the tab is closed, on a different sandbox, hours
39
+ * later. There is no session to reconnect — the id is the whole state.
40
+ *
41
+ * The stream (`GET /runs/:id/events`) is tried first and the poll is its
42
+ * fallback, so an agent deployed before that route existed still works. Watching
43
+ * STOPS on a terminal status, so a finished run costs nothing; passing
44
+ * `undefined` (nothing started yet) also costs nothing.
45
+ *
46
+ * @typeParam R - The workflow's output type. Supplying it is what makes
47
+ * `run.status === "completed"` narrow to a typed `run.output` instead of
48
+ * `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
49
+ * type-only import of `agent.ts` is erased, so it costs the bundle nothing.
50
+ *
51
+ * @public
52
+ */
53
+ export declare function useWorkflowRun<R = unknown>(runId: string | undefined, opts?: {
54
+ api?: WorkflowApi;
55
+ intervalMs?: number;
56
+ }): UseWorkflowRunResult<R>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The RUNS a workflow has had — the list a page shows beside its form.
3
+ *
4
+ * `useWorkflowRun` watches one run you already hold an id for, which is the
5
+ * right shape for the run a page just started and the wrong one for everything
6
+ * before it. A workflow app that only offers that leaves its own history
7
+ * unreachable: a page reload drops the id, and the only way back to yesterday's
8
+ * transcript is to have written the id down — which is what
9
+ * `transcription-workflow` asked people to do, with a text box for pasting one.
10
+ *
11
+ * `GET /workflows/runs?workflow=…` has always been able to answer this. This is
12
+ * the hook over it, so a page renders history instead of asking for an id.
13
+ *
14
+ * ## It re-reads on demand, and does not poll on its own
15
+ *
16
+ * A list is not a live view: the run a page cares about right now is already
17
+ * being watched by `useWorkflowRun`, and a second polling loop over the whole
18
+ * history would broker N requests a minute on the platform to re-learn what the
19
+ * first one already knows. So this reads once and hands back `refresh` — which
20
+ * a page calls when its own run settles, which is exactly when the list is
21
+ * stale.
22
+ */
23
+ import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
24
+ /** Options for {@link useWorkflowRuns}. */
25
+ export type UseWorkflowRunsOptions = {
26
+ /** The client to read with. Defaults to one for the page's own agent. */
27
+ api?: WorkflowApi;
28
+ /** Most runs to return, newest first. The agent clamps its own ceiling. */
29
+ limit?: number;
30
+ /**
31
+ * Narrow to one correlation key — the `key` a run was started with.
32
+ *
33
+ * Omitted, the list is every recent run of the workflow, which is what an
34
+ * operator's page wants. A page showing "your" runs passes the key it started
35
+ * them with; there is no per-user filtering behind this, so the key IS the
36
+ * scoping mechanism.
37
+ */
38
+ key?: string;
39
+ /** Skip the read entirely — for a page that does not know its workflow yet. */
40
+ skip?: boolean;
41
+ };
42
+ /** What {@link useWorkflowRuns} reports. */
43
+ export type UseWorkflowRunsResult<R = unknown> = {
44
+ /** The runs, newest first. Empty until the first read lands. */
45
+ runs: WorkflowRun<R>[];
46
+ /** True until the first read settles, and during an explicit refresh. */
47
+ loading: boolean;
48
+ /** The read's failure, alongside an empty list — which is why it exists. */
49
+ error: string | undefined;
50
+ /** Re-read now. Call it when a run this page started reaches a terminal status. */
51
+ refresh: () => void;
52
+ };
53
+ /**
54
+ * Read a workflow's recent runs.
55
+ *
56
+ * @typeParam R - The workflow's output type, so a completed run's `output` is
57
+ * typed rather than `unknown`. Derive it with `WorkflowOutputOf`.
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * import { useWorkflowRuns } from "@alexkroman1/aai-ui";
62
+ *
63
+ * function History() {
64
+ * const { runs } = useWorkflowRuns("transcribe", { limit: 10 });
65
+ * return <ul>{runs.map((run) => <li key={run.runId}>{run.status}</li>)}</ul>;
66
+ * }
67
+ * ```
68
+ *
69
+ * @public
70
+ */
71
+ export declare function useWorkflowRuns<R = unknown>(workflow: string | undefined, opts?: UseWorkflowRunsOptions): UseWorkflowRunsResult<R>;