@alexkroman1/aai-ui 9.2.0 → 10.0.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 +1 -1
- package/dist/_recover-run.d.ts +73 -0
- package/dist/default-client/assets/{index-CA2w_SUi.js → index-DS-RCrri.js} +25 -25
- package/dist/default-client/index.html +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +232 -6
- package/dist/internal.js +1 -1
- package/dist/use-run-key.d.ts +99 -0
- package/dist/use-workflow-form.d.ts +23 -0
- package/dist/{use-workflow-progress-C4KgVldr.js → use-workflow-progress-Cu0SxMyg.js} +79 -2
- package/dist/use-workflow-progress.d.ts +39 -2
- package/dist/use-workflow-run.d.ts +9 -0
- package/dist/use-workflow-stream.d.ts +10 -1
- package/package.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>aai</title>
|
|
7
7
|
<link rel="icon" href="./favicon.ico" />
|
|
8
8
|
<style>html, body { background: #FBF8F2; margin: 0; }</style>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-DS-RCrri.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="./assets/client-audio-constants-Ck0IJO4c.js">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="./assets/index-S5fkKi6B.css">
|
|
12
12
|
</head>
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type { AgentCustomEvent, SessionCore, SessionSnapshot, } from "./session-
|
|
|
25
25
|
export type { AgentState, ChatMessage, ClientTheme, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
|
|
26
26
|
export { type ConversationItem, type UseConversationResult, useConversation, } from "./use-conversation.ts";
|
|
27
27
|
export { type UseDownloadUrlOptions, type UseDownloadUrlResult, useDownloadUrl, } from "./use-download-url.ts";
|
|
28
|
+
export { useRunKey } from "./use-run-key.ts";
|
|
28
29
|
export { type UseUserTranscriptResult, useUserTranscript } from "./use-user-transcript.ts";
|
|
29
30
|
export { type UploadStatus, type UseWorkflowSubmitOptions, type UseWorkflowsOptions, type UseWorkflowsResult, useWorkflowSubmit, useWorkflows, type WorkflowSubmission, } from "./use-workflow-form.ts";
|
|
30
31
|
export { type UseWorkflowProgressResult, useWorkflowProgress, } from "./use-workflow-progress.ts";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { n as useUserTranscript } from "./use-user-transcript-C14qWFu2.js";
|
|
|
10
10
|
import { n as ToolCallRow } from "./tool-call-block-B7KlhaWl.js";
|
|
11
11
|
import { SidebarLayout } from "./components/sidebar-layout.js";
|
|
12
12
|
import { StartScreen } from "./components/start-screen.js";
|
|
13
|
-
import { a as useWorkflowRun, c as isTerminal, n as useWorkflowProgress, o as useWorkflowApiRef, s as createWorkflowApi } from "./use-workflow-progress-
|
|
13
|
+
import { a as useWorkflowRun, c as isTerminal, n as useWorkflowProgress, o as useWorkflowApiRef, s as createWorkflowApi } from "./use-workflow-progress-Cu0SxMyg.js";
|
|
14
14
|
import { t as createSessionCore } from "./session-core-0hClhPz-.js";
|
|
15
15
|
import { client, mountRoot, resolveContainer } from "./define-client.js";
|
|
16
16
|
import { useAgentState, useEvent, useToolCallStart, useToolResult } from "./hooks.js";
|
|
@@ -760,6 +760,94 @@ function UploadProgressBar({ upload, onPause, onResume, className }) {
|
|
|
760
760
|
});
|
|
761
761
|
}
|
|
762
762
|
//#endregion
|
|
763
|
+
//#region _recover-run.ts
|
|
764
|
+
/**
|
|
765
|
+
* Finding a run again when the page has lost its id.
|
|
766
|
+
*
|
|
767
|
+
* A run is durable and a page is not — which `useWorkflowRun`'s doc says, and
|
|
768
|
+
* which was only half true of the hooks above it: the run id lived in plain
|
|
769
|
+
* `useState`, so a refresh (or a same-tab navigation, or a crashed tab) left a
|
|
770
|
+
* live run with nothing anywhere able to name it. The run really did continue;
|
|
771
|
+
* the person really could not get back to it.
|
|
772
|
+
*
|
|
773
|
+
* `StartOptions.key` is the handle that survives that, and it always was — a
|
|
774
|
+
* caller's own name for a run, indexed by the agent, read back with
|
|
775
|
+
* `find(workflow, key)`. What was missing is the two lines that ASK. This is
|
|
776
|
+
* them, plus the four decisions they turn out to carry.
|
|
777
|
+
*
|
|
778
|
+
* ## It is a MOUNT-time act, not "whenever there is no run"
|
|
779
|
+
*
|
|
780
|
+
* The tempting spelling is "if we hold no run id, look one up", and it breaks
|
|
781
|
+
* `reset()`: a form put back to its initial state holds no run id, so the next
|
|
782
|
+
* pass would re-adopt the very run the person had just dismissed — a Clear
|
|
783
|
+
* button that clears nothing. So the lookup runs once per mount (and again only
|
|
784
|
+
* if the KEY changes, which is a different person's run), and every later
|
|
785
|
+
* absence of a run id is taken at face value.
|
|
786
|
+
*
|
|
787
|
+
* ## The lookup NEVER wins a race against a submit
|
|
788
|
+
*
|
|
789
|
+
* A person who reloads and immediately submits has started the run they want,
|
|
790
|
+
* and an answer that was already in flight names an older one. The caller
|
|
791
|
+
* therefore adopts through `current ?? found`: the recovered id fills an empty
|
|
792
|
+
* slot and never replaces a full one.
|
|
793
|
+
*
|
|
794
|
+
* ## A failed lookup is REPORTED
|
|
795
|
+
*
|
|
796
|
+
* The alternative is a page that quietly shows an empty form to somebody whose
|
|
797
|
+
* run is live, who then starts a second one — the duplicated work the key
|
|
798
|
+
* exists to prevent, and on a workflow app that is real money. A person who has
|
|
799
|
+
* never run anything pays a banner they can ignore. Same trade as
|
|
800
|
+
* `useWorkflows`, for the same reason: an empty answer here is a confident
|
|
801
|
+
* false statement.
|
|
802
|
+
*
|
|
803
|
+
* ## It is OPT-IN
|
|
804
|
+
*
|
|
805
|
+
* A `key` on its own still means only "record this with the run", which is what
|
|
806
|
+
* a voice agent's `ctx.workflows.start({ key })` means and what a page passing
|
|
807
|
+
* an account id may well want. Adopting a run is a decision about the PAGE, so
|
|
808
|
+
* it is `recover: true` and the two together read as what they do.
|
|
809
|
+
*/
|
|
810
|
+
/**
|
|
811
|
+
* Look up the newest run for a key, once, as the component mounts.
|
|
812
|
+
*
|
|
813
|
+
* @param opts - See {@link RecoverRunOptions}.
|
|
814
|
+
* @returns Whether the lookup is still out. A caller folds it into its own
|
|
815
|
+
* `pending`, because a form offering Submit while a live run is arriving is a
|
|
816
|
+
* form inviting a second one.
|
|
817
|
+
*
|
|
818
|
+
* @internal
|
|
819
|
+
*/
|
|
820
|
+
function useRecoveredRun(opts) {
|
|
821
|
+
const { workflow, key, enabled, getClient } = opts;
|
|
822
|
+
const [recovering, setRecovering] = useState(enabled && key !== void 0);
|
|
823
|
+
const handlers = useRef(opts);
|
|
824
|
+
handlers.current = opts;
|
|
825
|
+
useEffect(() => {
|
|
826
|
+
if (!enabled || key === void 0) return;
|
|
827
|
+
let cancelled = false;
|
|
828
|
+
setRecovering(true);
|
|
829
|
+
getClient().find(workflow, key, { limit: 1 }).then((found) => {
|
|
830
|
+
if (cancelled) return;
|
|
831
|
+
const newest = found[0];
|
|
832
|
+
if (newest !== void 0) handlers.current.onFound(newest.runId);
|
|
833
|
+
setRecovering(false);
|
|
834
|
+
}).catch((err) => {
|
|
835
|
+
if (cancelled) return;
|
|
836
|
+
handlers.current.onError(errorMessage(err));
|
|
837
|
+
setRecovering(false);
|
|
838
|
+
});
|
|
839
|
+
return () => {
|
|
840
|
+
cancelled = true;
|
|
841
|
+
};
|
|
842
|
+
}, [
|
|
843
|
+
enabled,
|
|
844
|
+
key,
|
|
845
|
+
workflow,
|
|
846
|
+
getClient
|
|
847
|
+
]);
|
|
848
|
+
return recovering;
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
763
851
|
//#region _run-controls.ts
|
|
764
852
|
/**
|
|
765
853
|
* The two things a page does TO a run it started, bound to the run it has.
|
|
@@ -838,7 +926,7 @@ function useRunControls(runId, getClient) {
|
|
|
838
926
|
* under a blocking policy, and an upload that cannot be REMEMBERED must degrade
|
|
839
927
|
* to the upload we would have done anyway rather than failing to start.
|
|
840
928
|
*/
|
|
841
|
-
const PREFIX = "aai:upload:";
|
|
929
|
+
const PREFIX$1 = "aai:upload:";
|
|
842
930
|
/**
|
|
843
931
|
* How many ids one form keeps.
|
|
844
932
|
*
|
|
@@ -850,7 +938,7 @@ const PREFIX = "aai:upload:";
|
|
|
850
938
|
const MAX_REMEMBERED = 32;
|
|
851
939
|
/** One form's slot in storage. */
|
|
852
940
|
function keyFor(scope) {
|
|
853
|
-
return `${PREFIX}${scope}`;
|
|
941
|
+
return `${PREFIX$1}${scope}`;
|
|
854
942
|
}
|
|
855
943
|
/**
|
|
856
944
|
* What names this file across a reload.
|
|
@@ -1406,7 +1494,7 @@ function useWorkflows(opts = {}) {
|
|
|
1406
1494
|
* @public
|
|
1407
1495
|
*/
|
|
1408
1496
|
function useWorkflowSubmit(workflow, opts = {}) {
|
|
1409
|
-
const { api, key, wait, intervalMs, parallel } = opts;
|
|
1497
|
+
const { api, key, recover = false, wait, intervalMs, parallel } = opts;
|
|
1410
1498
|
const [runId, setRunId] = useState(void 0);
|
|
1411
1499
|
const [starting, setStarting] = useState(false);
|
|
1412
1500
|
const [startError, setStartError] = useState(void 0);
|
|
@@ -1418,6 +1506,16 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1418
1506
|
intervalMs
|
|
1419
1507
|
}));
|
|
1420
1508
|
const { wake, cancel } = useRunControls(runId, getClient);
|
|
1509
|
+
const recovering = useRecoveredRun({
|
|
1510
|
+
workflow,
|
|
1511
|
+
key,
|
|
1512
|
+
enabled: recover,
|
|
1513
|
+
getClient,
|
|
1514
|
+
onFound: (found) => {
|
|
1515
|
+
setRunId((current) => current ?? found);
|
|
1516
|
+
},
|
|
1517
|
+
onError: setStartError
|
|
1518
|
+
});
|
|
1421
1519
|
const submit = useCallback(async (input) => {
|
|
1422
1520
|
const client = getClient();
|
|
1423
1521
|
setStarting(true);
|
|
@@ -1466,7 +1564,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1466
1564
|
pauseUpload,
|
|
1467
1565
|
resumeUpload,
|
|
1468
1566
|
run: tracked.run,
|
|
1469
|
-
pending: starting || tracked.polling,
|
|
1567
|
+
pending: recovering || starting || tracked.polling,
|
|
1470
1568
|
upload,
|
|
1471
1569
|
error: startError ?? tracked.error
|
|
1472
1570
|
};
|
|
@@ -1814,6 +1912,134 @@ function useDownloadUrl(uploadId, opts = {}) {
|
|
|
1814
1912
|
return state;
|
|
1815
1913
|
}
|
|
1816
1914
|
//#endregion
|
|
1915
|
+
//#region use-run-key.ts
|
|
1916
|
+
/**
|
|
1917
|
+
* The handle a page keeps on the runs it started, across a reload.
|
|
1918
|
+
*
|
|
1919
|
+
* `useWorkflowSubmit({ key, recover: true })` is what makes a run survivable —
|
|
1920
|
+
* the run id is that hook's own state, so a refresh loses it while the run
|
|
1921
|
+
* carries on — and the `key` is deliberately the caller's to choose, because it
|
|
1922
|
+
* is a lookup CAPABILITY: there is no per-user filtering behind `find`, so the
|
|
1923
|
+
* key IS the scoping mechanism. Choosing one is easy to get wrong in three
|
|
1924
|
+
* separate ways, and six shipped templates had each written the same twenty
|
|
1925
|
+
* lines to get it right. This is those lines.
|
|
1926
|
+
*
|
|
1927
|
+
* ## Three properties, and the rejected alternatives are why each one matters
|
|
1928
|
+
*
|
|
1929
|
+
* - **Opaque** — `crypto.randomUUID()`, never derived from what was submitted.
|
|
1930
|
+
* A key derived from the input collides the moment two people submit the same
|
|
1931
|
+
* thing, and they then recover each other's runs; it also carries what they
|
|
1932
|
+
* typed into a lookup token the platform deliberately stopped logging.
|
|
1933
|
+
* - **Short.** A `randomUUID` is 36 characters, well inside the 256 that
|
|
1934
|
+
* `POST /workflows/runs` allows a key.
|
|
1935
|
+
* - **Minted once per load and written back for the next one**, which is the
|
|
1936
|
+
* whole mechanism: the load that presses the button records the key with the
|
|
1937
|
+
* run, and the load after it finds the run by producing the same key.
|
|
1938
|
+
*
|
|
1939
|
+
* Storage rather than the page's own URL, for all of them. A `?key=` parameter
|
|
1940
|
+
* survives more (a new tab, a bookmark, a shared link) and that is the problem:
|
|
1941
|
+
* a URL is pasted into chats, copied into referrers and kept in history, and
|
|
1942
|
+
* what a leaked one buys is somebody else's work — reading it, and `cancel()` on
|
|
1943
|
+
* it. An app with accounts should pass the ACCOUNT's own id here instead, and
|
|
1944
|
+
* then a run follows the person to a new device, which is a promise only a login
|
|
1945
|
+
* can keep.
|
|
1946
|
+
*
|
|
1947
|
+
* ## The storage is the caller's decision, and it is not a detail
|
|
1948
|
+
*
|
|
1949
|
+
* `"session"` (the default) dies with the tab, which covers exactly the
|
|
1950
|
+
* interruption most pages have — a reload, a same-tab navigation, a crashed tab
|
|
1951
|
+
* — and is the same lifetime as this package's other two stores, the session
|
|
1952
|
+
* resume id (`session-resume-store.ts`) and the upload recall
|
|
1953
|
+
* (`_upload-recall.ts`), so both halves of a reload make the same promise.
|
|
1954
|
+
*
|
|
1955
|
+
* `"local"` is for a run that outlives all of that BY DESIGN — one that sleeps
|
|
1956
|
+
* between digests and may live a month, where closing the browser on Tuesday and
|
|
1957
|
+
* coming back on Friday to press Stop is the ordinary case rather than an edge
|
|
1958
|
+
* one, and a tab-scoped key would answer that with an empty form beside a run
|
|
1959
|
+
* still posting somewhere. It is as far as a key can go without a login, and no
|
|
1960
|
+
* further. `podcast-digest` is that template; the other five ship the default.
|
|
1961
|
+
*
|
|
1962
|
+
* ## Anything ELSE a page stores back must be VALIDATED on read
|
|
1963
|
+
*
|
|
1964
|
+
* This key needs no validation, and it is worth saying why, because it is the
|
|
1965
|
+
* exception: any string is a legal key, so a value from storage can only fail to
|
|
1966
|
+
* match a run. A page that remembers something more — which MODE submitted, say
|
|
1967
|
+
* — is remembering a value it will turn into a name, and storage hands back a
|
|
1968
|
+
* string some earlier version of that page wrote: a renamed mode, a hand-edited
|
|
1969
|
+
* value, a slot another app on the origin happens to share. Unchecked, that
|
|
1970
|
+
* starts a run called `undefined` and answers a 400 nobody typed. Check it
|
|
1971
|
+
* against the page's own list on the way out (`recalledMode` in
|
|
1972
|
+
* `transcription-workflow/recover.ts` is the worked example) — the recall is the
|
|
1973
|
+
* page's, the validation is not optional.
|
|
1974
|
+
*
|
|
1975
|
+
* ## The slot is keyed by the page's own URL
|
|
1976
|
+
*
|
|
1977
|
+
* Every deployed agent is served from one origin at `/:slug/`, so a fixed name
|
|
1978
|
+
* would have two agents scaffolded from the same template recover each other's
|
|
1979
|
+
* runs. The key is the page's own directory — resolved through `"./"`, which
|
|
1980
|
+
* drops the query and the hash, since a reload carrying `?foo` or `#bar` has to
|
|
1981
|
+
* find the same key. Same call `session-resume-store.ts` makes, for the same
|
|
1982
|
+
* reason.
|
|
1983
|
+
*
|
|
1984
|
+
* One key per PAGE is right even for a page driving several workflows: `find` is
|
|
1985
|
+
* scoped by workflow as well as by key, so three hooks sharing one key recover
|
|
1986
|
+
* three separate runs. `transcription-workflow` is that page.
|
|
1987
|
+
*
|
|
1988
|
+
* Every access is guarded. Storage THROWS outright in some contexts (Safari
|
|
1989
|
+
* private mode, an iframe blocked by policy) and is ABSENT in others (any
|
|
1990
|
+
* server-side render), and a page that cannot remember its key must degrade to
|
|
1991
|
+
* the behaviour it would have had anyway — one run per load — rather than
|
|
1992
|
+
* failing to render.
|
|
1993
|
+
*/
|
|
1994
|
+
/** Where a run key lives, namespaced like this package's two other stores. */
|
|
1995
|
+
const PREFIX = "aai:run-key:";
|
|
1996
|
+
/** This page's own slot — see "The slot is keyed by the page's own URL". */
|
|
1997
|
+
function slotFor() {
|
|
1998
|
+
const href = globalThis.location?.href;
|
|
1999
|
+
if (href === void 0) return PREFIX;
|
|
2000
|
+
try {
|
|
2001
|
+
return `${PREFIX}${new URL("./", href).href}`;
|
|
2002
|
+
} catch {
|
|
2003
|
+
return `${PREFIX}${href}`;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Read the key this page already has, or mint and remember one.
|
|
2008
|
+
*
|
|
2009
|
+
* Not exported: a page that wants a key wants it for the life of a component,
|
|
2010
|
+
* which is what the hook is. Calling this per render would mint a fresh key and
|
|
2011
|
+
* hand `recover` one nothing was ever started under.
|
|
2012
|
+
*/
|
|
2013
|
+
function mintRunKey(storage) {
|
|
2014
|
+
try {
|
|
2015
|
+
const store = storage === "local" ? globalThis.localStorage : globalThis.sessionStorage;
|
|
2016
|
+
const slot = slotFor();
|
|
2017
|
+
const stored = store?.getItem(slot);
|
|
2018
|
+
if (stored !== null && stored !== void 0) return stored;
|
|
2019
|
+
const minted = crypto.randomUUID();
|
|
2020
|
+
store?.setItem(slot, minted);
|
|
2021
|
+
return minted;
|
|
2022
|
+
} catch {
|
|
2023
|
+
return crypto.randomUUID();
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* A lookup key for `useWorkflowSubmit({ key, recover: true })`, stable across
|
|
2028
|
+
* reloads.
|
|
2029
|
+
*
|
|
2030
|
+
* @param options - See the module doc for the whole argument. The storage kind
|
|
2031
|
+
* is read once, when the key is minted: a value that changed afterwards would
|
|
2032
|
+
* be asking to move a key that has already been recorded with a run.
|
|
2033
|
+
* @returns The key to record runs under and to look them up by — the same one
|
|
2034
|
+
* for the life of the component, and for the next load in the same tab (or the
|
|
2035
|
+
* same browser, under `"local"`).
|
|
2036
|
+
*/
|
|
2037
|
+
function useRunKey(options = {}) {
|
|
2038
|
+
const { storage = "session" } = options;
|
|
2039
|
+
const [key] = useState(() => mintRunKey(storage));
|
|
2040
|
+
return key;
|
|
2041
|
+
}
|
|
2042
|
+
//#endregion
|
|
1817
2043
|
//#region use-workflow-runs.ts
|
|
1818
2044
|
/**
|
|
1819
2045
|
* The RUNS a workflow has had — the list a page shows beside its form.
|
|
@@ -2217,4 +2443,4 @@ const WORKFLOW_STATUS_LABELS = {
|
|
|
2217
2443
|
cancelled: "Cancelled"
|
|
2218
2444
|
};
|
|
2219
2445
|
//#endregion
|
|
2220
|
-
export { AutoScroll, Button, ChatView, CheckboxField, ConsoleShell, Controls, Field, FileField, Form, Markdown, MessageList, NumberField, SelectField, SidebarLayout, StartScreen, SubmitButton, TextAreaField, TextField, ToolCallRow, UploadProgressBar, WORKFLOW_STATUS_LABELS, WorkflowFields, WorkflowProgress, client, createSessionCore, createWorkflowApi, fetchClientConfig, isTerminal, page, useAgentState, useConversation, useDownloadUrl, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowStream, useWorkflowSubmit, useWorkflows };
|
|
2446
|
+
export { AutoScroll, Button, ChatView, CheckboxField, ConsoleShell, Controls, Field, FileField, Form, Markdown, MessageList, NumberField, SelectField, SidebarLayout, StartScreen, SubmitButton, TextAreaField, TextField, ToolCallRow, UploadProgressBar, WORKFLOW_STATUS_LABELS, WorkflowFields, WorkflowProgress, client, createSessionCore, createWorkflowApi, fetchClientConfig, isTerminal, page, useAgentState, useConversation, useDownloadUrl, useEvent, useRunKey, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowStream, useWorkflowSubmit, useWorkflows };
|
package/dist/internal.js
CHANGED
|
@@ -3,6 +3,6 @@ import { SessionProvider, ThemeProvider } from "./context.js";
|
|
|
3
3
|
import { n as SessionUrlChips, r as UiUrlChip, t as ApiUrlChip } from "./url-chips-DxXKgyL9.js";
|
|
4
4
|
import { t as TRANSCRIBING_PLACEHOLDER } from "./use-user-transcript-C14qWFu2.js";
|
|
5
5
|
import { t as ToolConfigContext } from "./tool-config-context-DzAofqi_.js";
|
|
6
|
-
import { i as MAX_MISSING_READS, r as DEFAULT_WORKFLOW_POLL_MS, t as DEFAULT_PROGRESS_POLL_MS } from "./use-workflow-progress-
|
|
6
|
+
import { i as MAX_MISSING_READS, r as DEFAULT_WORKFLOW_POLL_MS, t as DEFAULT_PROGRESS_POLL_MS } from "./use-workflow-progress-Cu0SxMyg.js";
|
|
7
7
|
import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
|
|
8
8
|
export { ApiUrlChip, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, MAX_MISSING_READS, SessionProvider, SessionUrlChips, TRANSCRIBING_PLACEHOLDER, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, loadClientConfig };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handle a page keeps on the runs it started, across a reload.
|
|
3
|
+
*
|
|
4
|
+
* `useWorkflowSubmit({ key, recover: true })` is what makes a run survivable —
|
|
5
|
+
* the run id is that hook's own state, so a refresh loses it while the run
|
|
6
|
+
* carries on — and the `key` is deliberately the caller's to choose, because it
|
|
7
|
+
* is a lookup CAPABILITY: there is no per-user filtering behind `find`, so the
|
|
8
|
+
* key IS the scoping mechanism. Choosing one is easy to get wrong in three
|
|
9
|
+
* separate ways, and six shipped templates had each written the same twenty
|
|
10
|
+
* lines to get it right. This is those lines.
|
|
11
|
+
*
|
|
12
|
+
* ## Three properties, and the rejected alternatives are why each one matters
|
|
13
|
+
*
|
|
14
|
+
* - **Opaque** — `crypto.randomUUID()`, never derived from what was submitted.
|
|
15
|
+
* A key derived from the input collides the moment two people submit the same
|
|
16
|
+
* thing, and they then recover each other's runs; it also carries what they
|
|
17
|
+
* typed into a lookup token the platform deliberately stopped logging.
|
|
18
|
+
* - **Short.** A `randomUUID` is 36 characters, well inside the 256 that
|
|
19
|
+
* `POST /workflows/runs` allows a key.
|
|
20
|
+
* - **Minted once per load and written back for the next one**, which is the
|
|
21
|
+
* whole mechanism: the load that presses the button records the key with the
|
|
22
|
+
* run, and the load after it finds the run by producing the same key.
|
|
23
|
+
*
|
|
24
|
+
* Storage rather than the page's own URL, for all of them. A `?key=` parameter
|
|
25
|
+
* survives more (a new tab, a bookmark, a shared link) and that is the problem:
|
|
26
|
+
* a URL is pasted into chats, copied into referrers and kept in history, and
|
|
27
|
+
* what a leaked one buys is somebody else's work — reading it, and `cancel()` on
|
|
28
|
+
* it. An app with accounts should pass the ACCOUNT's own id here instead, and
|
|
29
|
+
* then a run follows the person to a new device, which is a promise only a login
|
|
30
|
+
* can keep.
|
|
31
|
+
*
|
|
32
|
+
* ## The storage is the caller's decision, and it is not a detail
|
|
33
|
+
*
|
|
34
|
+
* `"session"` (the default) dies with the tab, which covers exactly the
|
|
35
|
+
* interruption most pages have — a reload, a same-tab navigation, a crashed tab
|
|
36
|
+
* — and is the same lifetime as this package's other two stores, the session
|
|
37
|
+
* resume id (`session-resume-store.ts`) and the upload recall
|
|
38
|
+
* (`_upload-recall.ts`), so both halves of a reload make the same promise.
|
|
39
|
+
*
|
|
40
|
+
* `"local"` is for a run that outlives all of that BY DESIGN — one that sleeps
|
|
41
|
+
* between digests and may live a month, where closing the browser on Tuesday and
|
|
42
|
+
* coming back on Friday to press Stop is the ordinary case rather than an edge
|
|
43
|
+
* one, and a tab-scoped key would answer that with an empty form beside a run
|
|
44
|
+
* still posting somewhere. It is as far as a key can go without a login, and no
|
|
45
|
+
* further. `podcast-digest` is that template; the other five ship the default.
|
|
46
|
+
*
|
|
47
|
+
* ## Anything ELSE a page stores back must be VALIDATED on read
|
|
48
|
+
*
|
|
49
|
+
* This key needs no validation, and it is worth saying why, because it is the
|
|
50
|
+
* exception: any string is a legal key, so a value from storage can only fail to
|
|
51
|
+
* match a run. A page that remembers something more — which MODE submitted, say
|
|
52
|
+
* — is remembering a value it will turn into a name, and storage hands back a
|
|
53
|
+
* string some earlier version of that page wrote: a renamed mode, a hand-edited
|
|
54
|
+
* value, a slot another app on the origin happens to share. Unchecked, that
|
|
55
|
+
* starts a run called `undefined` and answers a 400 nobody typed. Check it
|
|
56
|
+
* against the page's own list on the way out (`recalledMode` in
|
|
57
|
+
* `transcription-workflow/recover.ts` is the worked example) — the recall is the
|
|
58
|
+
* page's, the validation is not optional.
|
|
59
|
+
*
|
|
60
|
+
* ## The slot is keyed by the page's own URL
|
|
61
|
+
*
|
|
62
|
+
* Every deployed agent is served from one origin at `/:slug/`, so a fixed name
|
|
63
|
+
* would have two agents scaffolded from the same template recover each other's
|
|
64
|
+
* runs. The key is the page's own directory — resolved through `"./"`, which
|
|
65
|
+
* drops the query and the hash, since a reload carrying `?foo` or `#bar` has to
|
|
66
|
+
* find the same key. Same call `session-resume-store.ts` makes, for the same
|
|
67
|
+
* reason.
|
|
68
|
+
*
|
|
69
|
+
* One key per PAGE is right even for a page driving several workflows: `find` is
|
|
70
|
+
* scoped by workflow as well as by key, so three hooks sharing one key recover
|
|
71
|
+
* three separate runs. `transcription-workflow` is that page.
|
|
72
|
+
*
|
|
73
|
+
* Every access is guarded. Storage THROWS outright in some contexts (Safari
|
|
74
|
+
* private mode, an iframe blocked by policy) and is ABSENT in others (any
|
|
75
|
+
* server-side render), and a page that cannot remember its key must degrade to
|
|
76
|
+
* the behaviour it would have had anyway — one run per load — rather than
|
|
77
|
+
* failing to render.
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* A lookup key for `useWorkflowSubmit({ key, recover: true })`, stable across
|
|
81
|
+
* reloads.
|
|
82
|
+
*
|
|
83
|
+
* @param options - See the module doc for the whole argument. The storage kind
|
|
84
|
+
* is read once, when the key is minted: a value that changed afterwards would
|
|
85
|
+
* be asking to move a key that has already been recorded with a run.
|
|
86
|
+
* @returns The key to record runs under and to look them up by — the same one
|
|
87
|
+
* for the life of the component, and for the next load in the same tab (or the
|
|
88
|
+
* same browser, under `"local"`).
|
|
89
|
+
*/
|
|
90
|
+
export declare function useRunKey(options?: {
|
|
91
|
+
/**
|
|
92
|
+
* Which store keeps the key between loads.
|
|
93
|
+
*
|
|
94
|
+
* `"session"` (the default) dies with the tab; `"local"` survives the
|
|
95
|
+
* browser closing, which is what a run that sleeps for days needs. See "The
|
|
96
|
+
* storage is the caller's decision".
|
|
97
|
+
*/
|
|
98
|
+
storage?: "session" | "local";
|
|
99
|
+
}): string;
|
|
@@ -231,6 +231,29 @@ export type UseWorkflowSubmitOptions = {
|
|
|
231
231
|
api?: WorkflowApi;
|
|
232
232
|
/** Correlation key recorded with the run, for finding it again without the id. */
|
|
233
233
|
key?: string;
|
|
234
|
+
/**
|
|
235
|
+
* On mount, adopt the newest run this `key` already has.
|
|
236
|
+
*
|
|
237
|
+
* **This is what makes a reload survivable.** The run id is this hook's own
|
|
238
|
+
* state, so a refresh loses it while the run carries on — and a page that
|
|
239
|
+
* cannot name a run cannot show it, cancel it or wake it. With a `key` and
|
|
240
|
+
* this flag the hook asks `find(workflow, key)` once as it mounts and follows
|
|
241
|
+
* whatever comes back, so the answer, the progress and the controls are all
|
|
242
|
+
* there again.
|
|
243
|
+
*
|
|
244
|
+
* Inert without a `key`, because the key IS the lookup. Opt-in because a
|
|
245
|
+
* `key` on its own means only "record this with the run", which is what a
|
|
246
|
+
* page passing an account id may well want; adopting a run is a decision
|
|
247
|
+
* about the page.
|
|
248
|
+
*
|
|
249
|
+
* The key has to be one the next load can produce, and choosing it is the
|
|
250
|
+
* caller's: it is a lookup CAPABILITY (there is no per-user filtering behind
|
|
251
|
+
* `find`), it must fit the route's 256-character bound, and anything derived
|
|
252
|
+
* from a person's own input both collides and carries what they typed.
|
|
253
|
+
* `useRunKey()` is that key, and its module argues every alternative; a page
|
|
254
|
+
* with accounts passes the account's own id instead.
|
|
255
|
+
*/
|
|
256
|
+
recover?: boolean;
|
|
234
257
|
/**
|
|
235
258
|
* Hold the `POST` open until the run settles, up to this many ms — the
|
|
236
259
|
* synchronous mode. Omitted (the default) returns as soon as the run exists.
|
|
@@ -316,6 +316,15 @@ function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopp
|
|
|
316
316
|
* not: it can complete while the tab is closed, on a different sandbox, hours
|
|
317
317
|
* later. There is no session to reconnect — the id is the whole state.
|
|
318
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
|
+
*
|
|
319
328
|
* The stream (`GET /runs/:id/events`) is tried first and the poll is its
|
|
320
329
|
* fallback, so an agent deployed before that route existed still works. Watching
|
|
321
330
|
* STOPS on a terminal status, so a finished run costs nothing; passing
|
|
@@ -448,9 +457,67 @@ function useWorkflowRun(runId, opts = {}) {
|
|
|
448
457
|
* log rather than a socket — but it is a poll of a CHEAP shape: each read asks
|
|
449
458
|
* only for chunks past the last index it saw, so a quiet run costs an empty
|
|
450
459
|
* answer rather than the whole log again.
|
|
460
|
+
*
|
|
461
|
+
* ## A FAILED read is not an absent route, whether or not it carried a status
|
|
462
|
+
*
|
|
463
|
+
* Because the loop stops for good once it decides the route is absent, that
|
|
464
|
+
* decision is the one place a single bad request can cost a live run its entire
|
|
465
|
+
* narration — and it did. Every non-2xx used to read as "this agent serves no
|
|
466
|
+
* progress route", so one transient answer hid the log permanently while the run
|
|
467
|
+
* carried on and the status line went on saying `running`. See
|
|
468
|
+
* {@link isTransientRead} for the split and `readOnce` for what each arm costs.
|
|
469
|
+
*/
|
|
470
|
+
/**
|
|
471
|
+
* How often a live run's progress is re-read once a bounded read has ended.
|
|
472
|
+
*
|
|
473
|
+
* **Five seconds, up from one, because narration is the cheapest thing on the
|
|
474
|
+
* page and it was the most expensive thing on the wire.**
|
|
475
|
+
*
|
|
476
|
+
* On the platform every one of these reads BROKERS (see `useWorkflowRun`'s note
|
|
477
|
+
* on the same hazard), and a page routinely mounts TWO of these hooks against one
|
|
478
|
+
* run — `transcription-workflow` renders `<WorkflowProgress>` for the run's own
|
|
479
|
+
* narration and `<LiveTranscript>` for the segments as they land. At one second
|
|
480
|
+
* that is 2 requests/second from a single tab, which is exactly the platform's
|
|
481
|
+
* whole per-IP surface budget (`WORKFLOW_IP_RATE_LIMIT`, 600 per 5 minutes) — so
|
|
482
|
+
* one tab watching one run, with a history entry expanded, answers
|
|
483
|
+
* `429 Too many workflow requests` partway through its own run.
|
|
484
|
+
*
|
|
485
|
+
* It is also contending for the link with the UPLOAD, which on this page is the
|
|
486
|
+
* thing the reader is actually waiting for: a workflow app's whole wall clock is
|
|
487
|
+
* bytes going out, and progress polling spends the same uplink to describe it.
|
|
488
|
+
*
|
|
489
|
+
* What five costs is that a line appears up to five seconds after the run wrote
|
|
490
|
+
* it. That is the right trade for a log a person SKIMS while waiting minutes —
|
|
491
|
+
* and it is not the run's completion, which arrives on `useWorkflowRun`'s event
|
|
492
|
+
* stream (see {@link DEFAULT_WORKFLOW_POLL_MS}, deliberately left at two seconds
|
|
493
|
+
* because it answers "is it done", not "what is it doing").
|
|
494
|
+
*
|
|
495
|
+
* A page that really wants a live feed passes `intervalMs` and owns the
|
|
496
|
+
* consequence. That option is the authoring surface for this choice, which is why
|
|
497
|
+
* this constant is `/internal` rather than public.
|
|
451
498
|
*/
|
|
452
|
-
|
|
453
|
-
|
|
499
|
+
const DEFAULT_PROGRESS_POLL_MS = 5e3;
|
|
500
|
+
/**
|
|
501
|
+
* Whether a non-2xx says "come back" rather than "there is nothing here".
|
|
502
|
+
*
|
|
503
|
+
* The 408/429/5xx split, and it is a THIRD copy of that rule stated
|
|
504
|
+
* deliberately: the SDK's own `isTransientStatus` is on
|
|
505
|
+
* `@alexkroman1/aai/step` and `RETRYABLE_STATUS` is `sdk/_upload-retry.ts`'s
|
|
506
|
+
* internal, so neither is reachable from a browser bundle — this package may
|
|
507
|
+
* not import the step surface, and an `_`-prefixed module may not be imported
|
|
508
|
+
* cross-package at all. Hoisting one of them onto `/utils` (where this guide's
|
|
509
|
+
* own prose already claims `isTransientStatus` lives) is the fix that would
|
|
510
|
+
* delete this; it is a published-surface change and therefore not this one.
|
|
511
|
+
*
|
|
512
|
+
* Everything else is treated as a stable answer, which keeps a permanent
|
|
513
|
+
* refusal — a 401 against an agent whose `AAI_WORKFLOW_API_TOKEN` this page has
|
|
514
|
+
* no token for — from brokering a request every interval for as long as the tab
|
|
515
|
+
* is open. A 404 is the specific case the route documents: an agent deployed
|
|
516
|
+
* before progress streams existed, or one serving no workflow API at all.
|
|
517
|
+
*/
|
|
518
|
+
function isTransientRead(status) {
|
|
519
|
+
return status === 408 || status === 429 || status >= 500;
|
|
520
|
+
}
|
|
454
521
|
/**
|
|
455
522
|
* Drain one bounded read's frames, reporting how it ended and everything it
|
|
456
523
|
* carried.
|
|
@@ -487,6 +554,15 @@ async function consumeFrames(body, signal) {
|
|
|
487
554
|
* poll cheap: a quiet run answers with a bare `done` rather than the whole log
|
|
488
555
|
* again.
|
|
489
556
|
*
|
|
557
|
+
* `next` is a COUNT of chunks consumed, and `startIndex` is an INCLUSIVE floor,
|
|
558
|
+
* so the two are the same number and no adjustment sits between them. That
|
|
559
|
+
* identity is the whole correctness argument here, and it is why the store's
|
|
560
|
+
* floor is inclusive rather than exclusive — read exclusively, this loop lost the
|
|
561
|
+
* chunk sitting AT its cursor on every re-open, so a run writing one line per
|
|
562
|
+
* poll delivered every other line.
|
|
563
|
+
* `packages/aai-runtime/workflow-stream-cursor.test.ts` states it as a property
|
|
564
|
+
* over generated polling schedules; this module's own spec pins the URLs.
|
|
565
|
+
*
|
|
490
566
|
* ## A negative `startIndex` is resolved on the FIRST read, not carried
|
|
491
567
|
*
|
|
492
568
|
* "The last N lines" names no position a later read can resume from — the tail
|
|
@@ -518,6 +594,7 @@ function readProgressUntilComplete(getClient, runId, options, intervalMs, onChun
|
|
|
518
594
|
}),
|
|
519
595
|
signal
|
|
520
596
|
});
|
|
597
|
+
if (!res.ok && isTransientRead(res.status)) return "partial";
|
|
521
598
|
if (!(res.ok && res.body)) return "unsupported";
|
|
522
599
|
const { ending, chunks } = await consumeFrames(res.body, signal);
|
|
523
600
|
next += chunks.length;
|
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
* log rather than a socket — but it is a poll of a CHEAP shape: each read asks
|
|
41
41
|
* only for chunks past the last index it saw, so a quiet run costs an empty
|
|
42
42
|
* answer rather than the whole log again.
|
|
43
|
+
*
|
|
44
|
+
* ## A FAILED read is not an absent route, whether or not it carried a status
|
|
45
|
+
*
|
|
46
|
+
* Because the loop stops for good once it decides the route is absent, that
|
|
47
|
+
* decision is the one place a single bad request can cost a live run its entire
|
|
48
|
+
* narration — and it did. Every non-2xx used to read as "this agent serves no
|
|
49
|
+
* progress route", so one transient answer hid the log permanently while the run
|
|
50
|
+
* carried on and the status line went on saying `running`. See
|
|
51
|
+
* {@link isTransientRead} for the split and `readOnce` for what each arm costs.
|
|
43
52
|
*/
|
|
44
53
|
import type { WorkflowApi } from "./workflow-client.ts";
|
|
45
54
|
/** The slice of the client this needs: one method. */
|
|
@@ -60,8 +69,36 @@ export type UseWorkflowProgressResult<T = string> = {
|
|
|
60
69
|
*/
|
|
61
70
|
supported: boolean;
|
|
62
71
|
};
|
|
63
|
-
/**
|
|
64
|
-
|
|
72
|
+
/**
|
|
73
|
+
* How often a live run's progress is re-read once a bounded read has ended.
|
|
74
|
+
*
|
|
75
|
+
* **Five seconds, up from one, because narration is the cheapest thing on the
|
|
76
|
+
* page and it was the most expensive thing on the wire.**
|
|
77
|
+
*
|
|
78
|
+
* On the platform every one of these reads BROKERS (see `useWorkflowRun`'s note
|
|
79
|
+
* on the same hazard), and a page routinely mounts TWO of these hooks against one
|
|
80
|
+
* run — `transcription-workflow` renders `<WorkflowProgress>` for the run's own
|
|
81
|
+
* narration and `<LiveTranscript>` for the segments as they land. At one second
|
|
82
|
+
* that is 2 requests/second from a single tab, which is exactly the platform's
|
|
83
|
+
* whole per-IP surface budget (`WORKFLOW_IP_RATE_LIMIT`, 600 per 5 minutes) — so
|
|
84
|
+
* one tab watching one run, with a history entry expanded, answers
|
|
85
|
+
* `429 Too many workflow requests` partway through its own run.
|
|
86
|
+
*
|
|
87
|
+
* It is also contending for the link with the UPLOAD, which on this page is the
|
|
88
|
+
* thing the reader is actually waiting for: a workflow app's whole wall clock is
|
|
89
|
+
* bytes going out, and progress polling spends the same uplink to describe it.
|
|
90
|
+
*
|
|
91
|
+
* What five costs is that a line appears up to five seconds after the run wrote
|
|
92
|
+
* it. That is the right trade for a log a person SKIMS while waiting minutes —
|
|
93
|
+
* and it is not the run's completion, which arrives on `useWorkflowRun`'s event
|
|
94
|
+
* stream (see {@link DEFAULT_WORKFLOW_POLL_MS}, deliberately left at two seconds
|
|
95
|
+
* because it answers "is it done", not "what is it doing").
|
|
96
|
+
*
|
|
97
|
+
* A page that really wants a live feed passes `intervalMs` and owns the
|
|
98
|
+
* consequence. That option is the authoring surface for this choice, which is why
|
|
99
|
+
* this constant is `/internal` rather than public.
|
|
100
|
+
*/
|
|
101
|
+
export declare const DEFAULT_PROGRESS_POLL_MS = 5000;
|
|
65
102
|
/**
|
|
66
103
|
* Follow one run's progress stream.
|
|
67
104
|
*
|
|
@@ -38,6 +38,15 @@ export type UseWorkflowRunResult<R = unknown> = {
|
|
|
38
38
|
* not: it can complete while the tab is closed, on a different sandbox, hours
|
|
39
39
|
* later. There is no session to reconnect — the id is the whole state.
|
|
40
40
|
*
|
|
41
|
+
* Which is also the limit of what this hook can do on its own. An id is state a
|
|
42
|
+
* RELOAD destroys, so a page holding nothing else comes back unable to name a
|
|
43
|
+
* run that is still going. The durable handle is `StartOptions.key`, read back
|
|
44
|
+
* with `find(workflow, key)`, and the hook that owns the id is where that
|
|
45
|
+
* belongs: `useWorkflowSubmit({ key, recover: true })` adopts the key's newest
|
|
46
|
+
* run as it mounts and passes the id here. See `_recover-run.ts` — the reason
|
|
47
|
+
* recovery is NOT in this hook is `reset()`, which leaves the owner holding no
|
|
48
|
+
* id on purpose, and a watcher that re-resolved one from a key would undo it.
|
|
49
|
+
*
|
|
41
50
|
* The stream (`GET /runs/:id/events`) is tried first and the poll is its
|
|
42
51
|
* fallback, so an agent deployed before that route existed still works. Watching
|
|
43
52
|
* STOPS on a terminal status, so a finished run costs nothing; passing
|
|
@@ -86,13 +86,22 @@ import type { SubmitInputOf } from "./workflow-def-types.ts";
|
|
|
86
86
|
* mode: it holds the `POST` open until the run settles, and here the run is
|
|
87
87
|
* started before its bytes are, so there is nothing left to hold it for.
|
|
88
88
|
*
|
|
89
|
+
* Without `recover` either, and REFUSED rather than ignored: an option a hook
|
|
90
|
+
* accepts and does nothing with is the silent-no-op failure this repo keeps
|
|
91
|
+
* paying for. Adopting an earlier run by key would hand this hook a run whose
|
|
92
|
+
* input names an upload id it did not mint and is not filling — so the run
|
|
93
|
+
* would sit waiting for bytes nobody is sending until its own abandonment
|
|
94
|
+
* bound. That is the same reason `_upload-recall.ts` deliberately does not
|
|
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.
|
|
97
|
+
*
|
|
89
98
|
* `parallel` COMPOSES with what this hook is for rather than competing with it.
|
|
90
99
|
* The run still starts before the bytes, and the store still publishes how far
|
|
91
100
|
* the file is readable — that number is the CONTIGUOUS prefix, so a run reading
|
|
92
101
|
* ahead of the uplink sees the same growing file whether one connection or four
|
|
93
102
|
* are filling it. What changes is only how fast it grows.
|
|
94
103
|
*/
|
|
95
|
-
export type UseWorkflowStreamOptions = Omit<UseWorkflowSubmitOptions, "wait">;
|
|
104
|
+
export type UseWorkflowStreamOptions = Omit<UseWorkflowSubmitOptions, "wait" | "recover">;
|
|
96
105
|
/**
|
|
97
106
|
* What {@link useWorkflowStream} returns: a {@link WorkflowSubmission}, exactly.
|
|
98
107
|
*
|