@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.
- package/README.md +2 -1
- package/dist/_repeat-until.d.ts +30 -0
- package/dist/_sse.d.ts +56 -0
- package/dist/_workflow-api-ref.d.ts +37 -0
- package/dist/audio.js +26 -26
- package/dist/{chat-view-CgFytvGy.js → chat-view-CK61bWWx.js} +2 -1
- package/dist/components/_form-values.d.ts +19 -0
- package/dist/components/chat-view.js +1 -1
- package/dist/components/form-types.d.ts +67 -0
- package/dist/components/form.d.ts +138 -0
- package/dist/components/message-list.js +1 -1
- package/dist/components/workflow-fields.d.ts +57 -0
- package/dist/components/workflow-progress.d.ts +55 -0
- package/dist/default-client/assets/audio-fO7SVU64.js +1 -0
- package/dist/default-client/assets/{capture-processor-B_5Ive8e.js → capture-processor-Dmc-KEpb.js} +4 -4
- package/dist/default-client/assets/client-audio-constants-Ck0IJO4c.js +1 -0
- package/dist/default-client/assets/index-CDugAuLK.css +2 -0
- package/dist/default-client/assets/index-DCI51Xz_.js +293 -0
- package/dist/default-client/assets/{playback-processor-6L8SIQ_l.js → playback-processor-DwQ9tE7X.js} +16 -13
- package/dist/default-client/index.html +3 -2
- package/dist/define-client.d.ts +40 -1
- package/dist/define-client.js +59 -17
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1595 -5
- package/dist/{message-list-CcjgWRVZ.js → message-list-BwA3rdPi.js} +15 -1
- package/dist/page.d.ts +88 -0
- package/dist/{session-core-BA8H3qtF.js → session-core-ClKdVgRU.js} +245 -112
- package/dist/session-core-dial.d.ts +38 -0
- package/dist/session-core-handshake.d.ts +16 -1
- package/dist/session-core-messages.d.ts +2 -2
- package/dist/session-core-reconnect.d.ts +2 -7
- package/dist/session-core.js +1 -1
- package/dist/session-resume-store.d.ts +43 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -2
- package/dist/use-user-transcript.d.ts +70 -0
- package/dist/use-workflow-form.d.ts +136 -0
- package/dist/use-workflow-progress.d.ts +100 -0
- package/dist/use-workflow-run.d.ts +56 -0
- package/dist/use-workflow-runs.d.ts +71 -0
- package/dist/workflow-client.d.ts +97 -0
- package/dist/workflow-events.d.ts +39 -0
- package/dist/worklets/_playback-bench-harness.d.ts +181 -0
- package/dist/worklets/_playback-bench-host.d.ts +63 -0
- package/dist/worklets/_playback-bench-page.d.ts +65 -0
- package/dist/worklets/_tts-trace-harness.d.ts +142 -0
- package/dist/worklets/_worklet-test-utils.d.ts +27 -0
- package/dist/worklets/playback-processor.d.ts +1 -1
- package/dist/worklets/playback-processor.js +15 -12
- package/package.json +9 -8
- package/dist/default-client/assets/audio-CsQVQn3f.js +0 -1
- package/dist/default-client/assets/index-D35_z2WM.js +0 -293
- 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 —
|
|
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
|
|
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 `
|
|
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>):
|
|
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
|
package/dist/session-core.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createSessionCore } from "./session-core-
|
|
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,
|
|
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,
|
|
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,
|
|
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>;
|