@alexkroman1/aai-ui 7.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_colors-CcAi2FOU.js → _colors-j8XMToi9.js} +13 -2
- package/dist/_upload-pause.d.ts +31 -0
- package/dist/{chat-view-BKsFfFZJ.js → chat-view-BJ6sOARu.js} +2 -2
- package/dist/{client-config-B4nznRvH.js → client-config-BT_kWID5.js} +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/chat-view.js +1 -1
- package/dist/components/controls.js +1 -1
- package/dist/components/message-list.js +1 -1
- package/dist/components/start-screen.js +1 -1
- package/dist/components/tool-call-block.js +1 -1
- package/dist/default-client/assets/index-Bp1iPrLa.js +299 -0
- package/dist/default-client/index.html +1 -1
- package/dist/define-client.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +141 -746
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +5 -3
- package/dist/{message-list-DHddO4QC.js → message-list-Dtl03sGr.js} +4 -68
- package/dist/{session-core-C2JtLArh.js → session-core-CMEpwRP1.js} +4 -4
- package/dist/session-core-audio-setup.d.ts +2 -2
- package/dist/session-core-messages.d.ts +1 -1
- package/dist/session-core.js +1 -1
- package/dist/{tool-call-block-DoF-cSIZ.js → tool-call-block-B7KlhaWl.js} +1 -1
- package/dist/{url-chips-DpM7Oocj.js → url-chips-DxXKgyL9.js} +1 -1
- package/dist/use-user-transcript-C14qWFu2.js +72 -0
- package/dist/use-user-transcript.d.ts +7 -2
- package/dist/use-workflow-progress-C4KgVldr.js +610 -0
- package/package.json +2 -2
- package/dist/default-client/assets/index-fEkrcZgo.js +0 -293
package/dist/index.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { n as fetchClientConfig } from "./client-config-
|
|
2
|
-
import {
|
|
1
|
+
import { n as fetchClientConfig } from "./client-config-BT_kWID5.js";
|
|
2
|
+
import { i as AutoScroll, n as Markdown, r as useConversation, t as MessageList } from "./message-list-Dtl03sGr.js";
|
|
3
3
|
import { ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
|
|
4
|
-
import { r as inkTint } from "./_colors-
|
|
4
|
+
import { r as inkTint } from "./_colors-j8XMToi9.js";
|
|
5
5
|
import { Button } from "./components/button.js";
|
|
6
|
-
import { n as ConsoleShell, t as ChatView } from "./chat-view-
|
|
7
|
-
import { n as setPageTitle
|
|
6
|
+
import { n as ConsoleShell, t as ChatView } from "./chat-view-BJ6sOARu.js";
|
|
7
|
+
import { n as setPageTitle } from "./_utils-B6498_bm.js";
|
|
8
8
|
import { Controls } from "./components/controls.js";
|
|
9
|
-
import { n as
|
|
9
|
+
import { n as useUserTranscript } from "./use-user-transcript-C14qWFu2.js";
|
|
10
|
+
import { n as ToolCallRow } from "./tool-call-block-B7KlhaWl.js";
|
|
10
11
|
import { SidebarLayout } from "./components/sidebar-layout.js";
|
|
11
12
|
import { StartScreen } from "./components/start-screen.js";
|
|
12
|
-
import {
|
|
13
|
+
import { a as useWorkflowRun, c as isTerminal, n as useWorkflowProgress, o as useWorkflowApiRef, s as createWorkflowApi } from "./use-workflow-progress-C4KgVldr.js";
|
|
14
|
+
import { t as createSessionCore } from "./session-core-CMEpwRP1.js";
|
|
13
15
|
import { client, mountRoot, resolveContainer } from "./define-client.js";
|
|
14
16
|
import { useAgentState, useEvent, useToolCallStart, useToolResult } from "./hooks.js";
|
|
15
17
|
import clsx from "clsx";
|
|
16
18
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
17
19
|
import { createContext, createElement, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
18
20
|
import { errorMessage } from "@alexkroman1/aai";
|
|
19
|
-
import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
20
|
-
import { createWorkflowApiClient, isTerminal, isTerminal as isTerminal$1, readEventStream } from "@alexkroman1/aai/workflow-api";
|
|
21
|
+
import { formatBytes, isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
21
22
|
import { createEpoch } from "@alexkroman1/aai/internal";
|
|
22
23
|
//#region components/_form-readiness.ts
|
|
23
24
|
/**
|
|
@@ -657,30 +658,6 @@ function SubmitButton({ children, pending = false, pendingLabel = "Working…",
|
|
|
657
658
|
* as a groove against the page.
|
|
658
659
|
*/
|
|
659
660
|
const TRACK_TINT_PCT = 12;
|
|
660
|
-
/** Bytes per KB/MB/GB step, as a file manager counts them. */
|
|
661
|
-
const BYTE_STEP = 1024;
|
|
662
|
-
/** Unit labels, largest last — the index is how many times the size divides. */
|
|
663
|
-
const BYTE_UNITS = [
|
|
664
|
-
"B",
|
|
665
|
-
"KB",
|
|
666
|
-
"MB",
|
|
667
|
-
"GB"
|
|
668
|
-
];
|
|
669
|
-
/**
|
|
670
|
-
* A size a person can read, because the number that matters is the SCALE.
|
|
671
|
-
*
|
|
672
|
-
* `12.4 MB of 48.0 MB` says how much is left in the terms someone chose the
|
|
673
|
-
* file in; `13002343 of 50331648` says the same thing and answers nothing.
|
|
674
|
-
*/
|
|
675
|
-
function formatBytes(bytes) {
|
|
676
|
-
let value = bytes;
|
|
677
|
-
let unit = 0;
|
|
678
|
-
while (value >= BYTE_STEP && unit < BYTE_UNITS.length - 1) {
|
|
679
|
-
value /= BYTE_STEP;
|
|
680
|
-
unit += 1;
|
|
681
|
-
}
|
|
682
|
-
return `${unit === 0 ? value : value.toFixed(1)} ${BYTE_UNITS[unit]}`;
|
|
683
|
-
}
|
|
684
661
|
/**
|
|
685
662
|
* How far a form's files have got, rendered as a bar.
|
|
686
663
|
*
|
|
@@ -1245,405 +1222,43 @@ async function uploadFiles(api, input, report, parallel, session) {
|
|
|
1245
1222
|
return out;
|
|
1246
1223
|
}
|
|
1247
1224
|
//#endregion
|
|
1248
|
-
//#region
|
|
1249
|
-
/**
|
|
1250
|
-
* Browser client for the workflow HTTP API (`aai/host/workflow-api.ts`).
|
|
1251
|
-
*
|
|
1252
|
-
* This is the whole client half of a WORKFLOW APP: an agent whose front door is
|
|
1253
|
-
* a form rather than a microphone (`workflowApp()`) starts runs
|
|
1254
|
-
* here and watches them for the answer. It deliberately does NOT go through
|
|
1255
|
-
* `SessionCore` — there is no socket, no audio graph, and no session to resume.
|
|
1256
|
-
*
|
|
1257
|
-
* **The requests themselves are the SDK's now**
|
|
1258
|
-
* (`createWorkflowApiClient`, `@alexkroman1/aai/workflow-api`), and what is left
|
|
1259
|
-
* here is the one thing that is genuinely a BROWSER's: the default base URL.
|
|
1260
|
-
* Every route, every query, the 404-is-an-answer rule and the `wait` clamp were
|
|
1261
|
-
* written three times over — here, in the studio's Workflows card, and in
|
|
1262
|
-
* `aai workflow` — and the parts the copies disagreed on were exactly the ones a
|
|
1263
|
-
* reader cannot check by eye. The SDK module's doc carries that argument; this
|
|
1264
|
-
* file must not grow a second implementation of any of it.
|
|
1265
|
-
*
|
|
1266
|
-
* The one thing worth knowing before using it: **a run outlives the page.**
|
|
1267
|
-
* Starting one resolves as soon as the run is created, so `runId` is the only
|
|
1268
|
-
* handle that matters and it stays valid across a reload, a different device, or
|
|
1269
|
-
* `curl` — which is what makes `useWorkflowRun` a watch rather than a
|
|
1270
|
-
* subscription to something the page owns.
|
|
1271
|
-
*
|
|
1272
|
-
* The loop that keeps asking lives in `use-workflow-run.ts`, and the streaming
|
|
1273
|
-
* fast path under it in `workflow-events.ts`.
|
|
1274
|
-
*/
|
|
1275
|
-
/**
|
|
1276
|
-
* Create a workflow API client aimed at the agent serving this page.
|
|
1277
|
-
*
|
|
1278
|
-
* Hoist it out of the component that uses it. `useWorkflowRun` holds the client
|
|
1279
|
-
* in a ref precisely so a fresh object per render does not restart its watch,
|
|
1280
|
-
* but a client built in render is still a new `fetch` closure every time and
|
|
1281
|
-
* reads as though it were free.
|
|
1282
|
-
*
|
|
1283
|
-
* @example
|
|
1284
|
-
* ```tsx
|
|
1285
|
-
* import { createWorkflowApi, useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
1286
|
-
* import { useState } from "react";
|
|
1287
|
-
*
|
|
1288
|
-
* // Module scope, not render scope — see above.
|
|
1289
|
-
* const api = createWorkflowApi();
|
|
1290
|
-
*
|
|
1291
|
-
* function StartDigest() {
|
|
1292
|
-
* const [runId, setRunId] = useState<string>();
|
|
1293
|
-
* const { run } = useWorkflowRun(runId, { api });
|
|
1294
|
-
* return (
|
|
1295
|
-
* <button
|
|
1296
|
-
* type="button"
|
|
1297
|
-
* onClick={() => void api.start("digest", { url: "…" }).then(setRunId)}
|
|
1298
|
-
* >
|
|
1299
|
-
* {run ? run.status : "Start"}
|
|
1300
|
-
* </button>
|
|
1301
|
-
* );
|
|
1302
|
-
* }
|
|
1303
|
-
* ```
|
|
1304
|
-
*
|
|
1305
|
-
* @param opts - See {@link WorkflowApiOptions}. Both fields are optional; the
|
|
1306
|
-
* default base URL is the page's own origin and path.
|
|
1307
|
-
* @returns The call set — see {@link WorkflowApi}.
|
|
1308
|
-
*
|
|
1309
|
-
* @public
|
|
1310
|
-
*/
|
|
1311
|
-
function createWorkflowApi(opts = {}) {
|
|
1312
|
-
return createWorkflowApiClient({
|
|
1313
|
-
baseUrl: opts.baseUrl ?? pageBaseUrl(),
|
|
1314
|
-
...omitUndefined({ token: opts.token })
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
//#endregion
|
|
1318
|
-
//#region _workflow-api-ref.ts
|
|
1319
|
-
/**
|
|
1320
|
-
* The client preamble every workflow hook needs, once.
|
|
1321
|
-
*
|
|
1322
|
-
* Five hooks (`useWorkflowRun`, `useWorkflowProgress`, `useWorkflowRuns`,
|
|
1323
|
-
* `useWorkflows`, `useWorkflowSubmit`) opened with the same two refs and the
|
|
1324
|
-
* same two paragraphs explaining them, and both halves are load-bearing rather
|
|
1325
|
-
* than stylistic — which is exactly why they should not be re-derived per hook:
|
|
1326
|
-
*
|
|
1327
|
-
* - **The caller's client lives in a REF, never in an effect's dependency
|
|
1328
|
-
* array.** The natural call site is
|
|
1329
|
-
* `useWorkflowRun(id, { api: createWorkflowApi() })`, which passes a NEW
|
|
1330
|
-
* object every render; as a dependency that tears the effect down and
|
|
1331
|
-
* restarts it on each one, and because a restart clears state and re-renders,
|
|
1332
|
-
* it schedules the next. The result is an unbounded request loop against the
|
|
1333
|
-
* agent — on the platform, against the BROKER — with `error` wiped before
|
|
1334
|
-
* anything can read it, presenting as "the page polls forever" rather than as
|
|
1335
|
-
* a mistake at the call site.
|
|
1336
|
-
* - **The no-client default is built lazily and ONCE.** As a render-time
|
|
1337
|
-
* default (`api ?? createWorkflowApi()`) it is a fresh object per render,
|
|
1338
|
-
* which is the same hazard one layer down; built inside an effect it is a
|
|
1339
|
-
* fresh object per watch.
|
|
1340
|
-
*
|
|
1341
|
-
* The returned getter is stable for the life of the component and reads the ref
|
|
1342
|
-
* on every call, so a caller that SWAPS clients mid-watch — a token arriving
|
|
1343
|
-
* after login — is picked up by the next request without the watch restarting.
|
|
1344
|
-
*/
|
|
1345
|
-
/**
|
|
1346
|
-
* Resolve the client a workflow hook should use, now.
|
|
1347
|
-
*
|
|
1348
|
-
* @param api - The caller's client, or undefined for one aimed at the page's
|
|
1349
|
-
* own agent.
|
|
1350
|
-
* @returns A stable getter. Call it per request, never once per watch.
|
|
1351
|
-
*
|
|
1352
|
-
* @internal
|
|
1353
|
-
*/
|
|
1354
|
-
function useWorkflowApiRef(api) {
|
|
1355
|
-
const apiRef = useRef(api);
|
|
1356
|
-
apiRef.current = api;
|
|
1357
|
-
const fallbackRef = useRef(void 0);
|
|
1358
|
-
return useCallback(() => {
|
|
1359
|
-
const current = apiRef.current;
|
|
1360
|
-
if (current) return current;
|
|
1361
|
-
fallbackRef.current ??= createWorkflowApi();
|
|
1362
|
-
return fallbackRef.current;
|
|
1363
|
-
}, []);
|
|
1364
|
-
}
|
|
1365
|
-
//#endregion
|
|
1366
|
-
//#region _repeat-until.ts
|
|
1367
|
-
/**
|
|
1368
|
-
* A bounded read, re-armed from the SETTLED read — the loop both workflow
|
|
1369
|
-
* watchers are built out of.
|
|
1370
|
-
*
|
|
1371
|
-
* `pollUntilTerminal` (`use-workflow-run.ts`) and `readProgressUntilComplete`
|
|
1372
|
-
* (`use-workflow-progress.ts`) each open a request, decide from its answer
|
|
1373
|
-
* whether to come back, and stop when told to. What they had in common was not
|
|
1374
|
-
* the decision — one reads a snapshot, the other drains an SSE body — but the
|
|
1375
|
-
* scaffold around it, and that scaffold is where the two rules live:
|
|
1376
|
-
*
|
|
1377
|
-
* - **Re-armed from the settled read, never on an interval.** A slow response
|
|
1378
|
-
* would otherwise stack overlapping requests on an agent that is already
|
|
1379
|
-
* struggling, and on the platform every one of them BROKERS.
|
|
1380
|
-
* - **Cancellation is a signal, not a flag.** The teardown has to reach the
|
|
1381
|
-
* in-flight request too — an abandoned progress read otherwise keeps pulling
|
|
1382
|
-
* chunks out of a run for a page that has navigated away — so `step` is
|
|
1383
|
-
* handed the signal rather than being trusted to check a boolean.
|
|
1384
|
-
*/
|
|
1225
|
+
//#region _upload-pause.ts
|
|
1385
1226
|
/**
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1227
|
+
* The two buttons a page puts over a live upload, bound to whichever gate the
|
|
1228
|
+
* submission is currently holding.
|
|
1388
1229
|
*
|
|
1389
|
-
* `
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1230
|
+
* `useWorkflowSubmit` and `useWorkflowStream` both own an {@link UploadGate} and
|
|
1231
|
+
* both reported the pause the same way: park the gate, then fold `paused` into
|
|
1232
|
+
* the status the bar is already drawing. Folded rather than replaced, because
|
|
1233
|
+
* everything else about that status — which file, how far, of how many — is
|
|
1234
|
+
* still true. Two copies of that rule are two copies that can stop agreeing
|
|
1235
|
+
* about what a paused bar says.
|
|
1395
1236
|
*/
|
|
1396
|
-
function repeatUntil(intervalMs, step) {
|
|
1397
|
-
const controller = new AbortController();
|
|
1398
|
-
let timer;
|
|
1399
|
-
const tick = async () => {
|
|
1400
|
-
if (await step(controller.signal) || controller.signal.aborted) return;
|
|
1401
|
-
timer = setTimeout(() => void tick(), intervalMs);
|
|
1402
|
-
};
|
|
1403
|
-
tick();
|
|
1404
|
-
return () => {
|
|
1405
|
-
controller.abort();
|
|
1406
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
//#endregion
|
|
1410
|
-
//#region workflow-events.ts
|
|
1411
1237
|
/**
|
|
1412
|
-
*
|
|
1238
|
+
* Bind pause/resume to the live submission's gate.
|
|
1413
1239
|
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1240
|
+
* @param getGate - Reads the gate out of the caller's ref, so the callbacks stay
|
|
1241
|
+
* stable across submissions rather than being re-created per gate.
|
|
1242
|
+
* @param setUpload - The status setter the progress bar renders from.
|
|
1417
1243
|
*
|
|
1418
1244
|
* @internal
|
|
1419
1245
|
*/
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
* does. What it buys is real, though: on the platform every polled read BROKERS,
|
|
1428
|
-
* so N open tabs at `DEFAULT_WORKFLOW_POLL_MS` is N/2 brokered requests a
|
|
1429
|
-
* second, each able to boot a sandbox. One stream per tab replaces all of it.
|
|
1430
|
-
*
|
|
1431
|
-
* `EventSource` is not used, for two reasons that both matter here: it cannot
|
|
1432
|
-
* send an `Authorization` header (an agent with `AAI_WORKFLOW_API_TOKEN` set
|
|
1433
|
-
* would be unreachable), and it reconnects on its own schedule, which would
|
|
1434
|
-
* fight the caller's. A `fetch` stream gives both back.
|
|
1435
|
-
*
|
|
1436
|
-
* Returns a stop function. `onFallback` is called at most once, when this stream
|
|
1437
|
-
* cannot be relied on and the poll should take over.
|
|
1438
|
-
*/
|
|
1439
|
-
function watchRunEvents(getClient, runId, onRun, onSettled, onFallback) {
|
|
1440
|
-
const controller = new AbortController();
|
|
1441
|
-
let handedOver = false;
|
|
1442
|
-
const handOver = () => {
|
|
1443
|
-
if (handedOver || controller.signal.aborted) return;
|
|
1444
|
-
handedOver = true;
|
|
1445
|
-
onFallback();
|
|
1446
|
-
};
|
|
1447
|
-
/**
|
|
1448
|
-
* Consume the stream. Resolves `"settled"` when the run reached a state
|
|
1449
|
-
* nothing will change, and `"fallback"` for every other ending — including a
|
|
1450
|
-
* clean end with no final frame, which is a dropped connection.
|
|
1451
|
-
*
|
|
1452
|
-
* A named function rather than an inline IIFE so `watchRunEvents` stays under
|
|
1453
|
-
* the cognitive-complexity cap, and so the two outcomes are a return value
|
|
1454
|
-
* instead of two callbacks invoked from six places.
|
|
1455
|
-
*/
|
|
1456
|
-
const pump = async () => {
|
|
1457
|
-
const res = await getClient().watch(runId, controller.signal);
|
|
1458
|
-
if (!(res.ok && res.body)) return "fallback";
|
|
1459
|
-
for await (const frame of readEventStream(res.body, controller.signal)) {
|
|
1460
|
-
if (frame.event === "run" && frame.data) onRun(frame.data);
|
|
1461
|
-
const outcome = endingFor(frame.event);
|
|
1462
|
-
if (outcome) return outcome;
|
|
1463
|
-
}
|
|
1464
|
-
return "fallback";
|
|
1465
|
-
};
|
|
1466
|
-
pump().then((outcome) => outcome === "settled" ? onSettled() : handOver(), () => handOver());
|
|
1467
|
-
return () => controller.abort();
|
|
1468
|
-
}
|
|
1469
|
-
/**
|
|
1470
|
-
* Does this frame END the stream, and does the run need watching afterwards?
|
|
1471
|
-
*
|
|
1472
|
-
* `done` and `missing` are both final and neither wants a reconnect: the run is
|
|
1473
|
-
* terminal, or the id will never exist (a 404 is a stable answer — the world's
|
|
1474
|
-
* record is durable). `idle` is the stream handing ITSELF back after its
|
|
1475
|
-
* duration cap, so that one falls back to the poll. Anything else is not an
|
|
1476
|
-
* ending.
|
|
1477
|
-
*/
|
|
1478
|
-
function endingFor(event) {
|
|
1479
|
-
if (event === "done" || event === "missing") return "settled";
|
|
1480
|
-
return event === "idle" ? "fallback" : void 0;
|
|
1481
|
-
}
|
|
1482
|
-
//#endregion
|
|
1483
|
-
//#region use-workflow-run.ts
|
|
1484
|
-
/**
|
|
1485
|
-
* `useWorkflowRun` — watch one run until it settles.
|
|
1486
|
-
*
|
|
1487
|
-
* Split from `workflow-client.ts` on the seam that module's doc already draws:
|
|
1488
|
-
* everything there is a REQUEST (one call, one answer, no React), and everything
|
|
1489
|
-
* here is the loop that keeps asking. They are read for different reasons — the
|
|
1490
|
-
* client is what a script or a `curl` equivalent needs, this is what a page needs
|
|
1491
|
-
* — and only this half imports React.
|
|
1492
|
-
*
|
|
1493
|
-
* `workflow-events.ts` sits under it as the streaming fast path, and
|
|
1494
|
-
* `use-workflow-form.ts` above it as the form-shaped caller.
|
|
1495
|
-
*/
|
|
1496
|
-
/** How often {@link useWorkflowRun} re-reads a live run when it has to poll. */
|
|
1497
|
-
const DEFAULT_WORKFLOW_POLL_MS = 2e3;
|
|
1498
|
-
/**
|
|
1499
|
-
* Consecutive "no such run" reads {@link useWorkflowRun} tolerates before giving
|
|
1500
|
-
* up on the id.
|
|
1501
|
-
*
|
|
1502
|
-
* Small on purpose: a 404 is a stable answer, so the budget exists only to
|
|
1503
|
-
* absorb a first read that races the run's creation — not to keep hoping.
|
|
1504
|
-
* Unbounded, a stale id polls (and, on the platform, BROKERS) for as long as the
|
|
1505
|
-
* tab is open.
|
|
1506
|
-
*/
|
|
1507
|
-
const MAX_MISSING_READS = 3;
|
|
1508
|
-
/**
|
|
1509
|
-
* Poll `runId` until it is terminal, reporting each read. Returns a stop
|
|
1510
|
-
* function.
|
|
1511
|
-
*
|
|
1512
|
-
* Module-level rather than inline in the hook below, so neither function carries
|
|
1513
|
-
* the whole loop's branching — and so the loop can be read without React in the
|
|
1514
|
-
* way.
|
|
1515
|
-
*/
|
|
1516
|
-
function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopped) {
|
|
1517
|
-
let missing = 0;
|
|
1518
|
-
/**
|
|
1519
|
-
* A read that came back empty. Resolves whether the loop should STOP.
|
|
1520
|
-
*
|
|
1521
|
-
* A 404 is a STABLE answer — a run the agent does not know about now will not
|
|
1522
|
-
* appear later — and retrying it unbounded is how a stale id (one restored
|
|
1523
|
-
* from `localStorage`, or one whose agent was redeployed onto a fresh
|
|
1524
|
-
* database) polls forever: the page stays `polling` and therefore busy, and on
|
|
1525
|
-
* the platform every read BROKERS, so a tab's worth of dead ids keeps
|
|
1526
|
-
* sandboxes resident. A small budget is kept anyway, because the first read
|
|
1527
|
-
* can race a replica that has not yet seen the run.
|
|
1528
|
-
*/
|
|
1529
|
-
const onMissing = (signal) => {
|
|
1530
|
-
missing += 1;
|
|
1531
|
-
if (missing < 3) return false;
|
|
1532
|
-
if (!signal.aborted) onError(`No workflow run ${runId}`);
|
|
1533
|
-
return true;
|
|
1534
|
-
};
|
|
1535
|
-
const read = async (signal) => {
|
|
1536
|
-
try {
|
|
1537
|
-
const next = await getClient().get(runId);
|
|
1538
|
-
if (signal.aborted) return true;
|
|
1539
|
-
if (!next) return onMissing(signal);
|
|
1540
|
-
missing = 0;
|
|
1541
|
-
onRun(next);
|
|
1542
|
-
return isTerminal$1(next);
|
|
1543
|
-
} catch (err) {
|
|
1544
|
-
if (!signal.aborted) onError(errorMessage(err));
|
|
1545
|
-
return false;
|
|
1546
|
-
}
|
|
1547
|
-
};
|
|
1548
|
-
return repeatUntil(intervalMs, async (signal) => {
|
|
1549
|
-
if (!await read(signal)) return false;
|
|
1550
|
-
if (!signal.aborted) onStopped();
|
|
1551
|
-
return true;
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
/**
|
|
1555
|
-
* Watch one run until it reaches a terminal status.
|
|
1556
|
-
*
|
|
1557
|
-
* A watch rather than a subscription because a run is durable and the page is
|
|
1558
|
-
* not: it can complete while the tab is closed, on a different sandbox, hours
|
|
1559
|
-
* later. There is no session to reconnect — the id is the whole state.
|
|
1560
|
-
*
|
|
1561
|
-
* The stream (`GET /runs/:id/events`) is tried first and the poll is its
|
|
1562
|
-
* fallback, so an agent deployed before that route existed still works. Watching
|
|
1563
|
-
* STOPS on a terminal status, so a finished run costs nothing; passing
|
|
1564
|
-
* `undefined` (nothing started yet) also costs nothing.
|
|
1565
|
-
*
|
|
1566
|
-
* @example
|
|
1567
|
-
* ```tsx
|
|
1568
|
-
* import type { ToolInputSchema, WorkflowDef } from "@alexkroman1/aai";
|
|
1569
|
-
* import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
1570
|
-
* import { useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
1571
|
-
*
|
|
1572
|
-
* // A real page writes `import type { digest } from "./agent.ts"`. Stood in
|
|
1573
|
-
* // for here so the example compiles on its own.
|
|
1574
|
-
* declare const digest: WorkflowDef<ToolInputSchema, Promise<{ points: string[] }>>;
|
|
1575
|
-
*
|
|
1576
|
-
* type Digest = WorkflowOutputOf<typeof digest>;
|
|
1577
|
-
*
|
|
1578
|
-
* function RunPanel({ runId }: { runId: string | undefined }) {
|
|
1579
|
-
* // The type argument is what makes `run.output` a `Digest` below.
|
|
1580
|
-
* const { run, error, polling } = useWorkflowRun<Digest>(runId);
|
|
1581
|
-
* if (error !== undefined) return <p role="alert">{error}</p>;
|
|
1582
|
-
* if (run?.status === "completed") {
|
|
1583
|
-
* return (
|
|
1584
|
-
* <ul>
|
|
1585
|
-
* {run.output.points.map((point) => (
|
|
1586
|
-
* <li key={point}>{point}</li>
|
|
1587
|
-
* ))}
|
|
1588
|
-
* </ul>
|
|
1589
|
-
* );
|
|
1590
|
-
* }
|
|
1591
|
-
* return <p>{polling ? "Working…" : "Nothing running."}</p>;
|
|
1592
|
-
* }
|
|
1593
|
-
* ```
|
|
1594
|
-
*
|
|
1595
|
-
* @typeParam R - The workflow's output type. Supplying it is what makes
|
|
1596
|
-
* `run.status === "completed"` narrow to a typed `run.output` instead of
|
|
1597
|
-
* `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
|
|
1598
|
-
* type-only import of `agent.ts` is erased, so it costs the bundle nothing.
|
|
1599
|
-
* @param runId - The run to watch. `undefined` costs nothing, so a page may
|
|
1600
|
-
* pass its state straight through before a run exists.
|
|
1601
|
-
* @param opts - `api` when the page holds its own client; `intervalMs` to
|
|
1602
|
-
* change the poll interval the stream falls back to.
|
|
1603
|
-
* @returns The latest snapshot, the last read's error, and whether the watch
|
|
1604
|
-
* is still going — see {@link UseWorkflowRunResult}.
|
|
1605
|
-
*
|
|
1606
|
-
* @public
|
|
1607
|
-
*/
|
|
1608
|
-
function useWorkflowRun(runId, opts = {}) {
|
|
1609
|
-
const { api, intervalMs = DEFAULT_WORKFLOW_POLL_MS } = opts;
|
|
1610
|
-
const [run, setRun] = useState(void 0);
|
|
1611
|
-
const [error, setError] = useState(void 0);
|
|
1612
|
-
/**
|
|
1613
|
-
* Has the watch stopped for a reason the snapshot does not show?
|
|
1614
|
-
*
|
|
1615
|
-
* Only one such reason exists — an id the agent kept reporting as unknown,
|
|
1616
|
-
* past {@link MAX_MISSING_READS} — and it leaves `run` undefined, so `polling`
|
|
1617
|
-
* derived from `isTerminal(run)` alone would stay true forever.
|
|
1618
|
-
*/
|
|
1619
|
-
const [stopped, setStopped] = useState(false);
|
|
1620
|
-
const getClient = useWorkflowApiRef(api);
|
|
1621
|
-
useEffect(() => {
|
|
1622
|
-
setRun(void 0);
|
|
1623
|
-
setError(void 0);
|
|
1624
|
-
setStopped(false);
|
|
1625
|
-
if (!runId) return;
|
|
1626
|
-
const onRun = (next) => {
|
|
1627
|
-
setRun(next);
|
|
1628
|
-
setError(void 0);
|
|
1629
|
-
};
|
|
1630
|
-
let stopPoll;
|
|
1631
|
-
const stopStream = watchRunEvents(getClient, runId, onRun, () => setStopped(true), () => {
|
|
1632
|
-
stopPoll = pollUntilTerminal(getClient, runId, intervalMs, onRun, setError, () => setStopped(true));
|
|
1633
|
-
});
|
|
1634
|
-
return () => {
|
|
1635
|
-
stopStream();
|
|
1636
|
-
stopPoll?.();
|
|
1637
|
-
};
|
|
1638
|
-
}, [
|
|
1639
|
-
runId,
|
|
1640
|
-
intervalMs,
|
|
1641
|
-
getClient
|
|
1642
|
-
]);
|
|
1246
|
+
function useUploadPause(getGate, setUpload) {
|
|
1247
|
+
const setPaused = useCallback((paused) => {
|
|
1248
|
+
setUpload((current) => current ? {
|
|
1249
|
+
...current,
|
|
1250
|
+
paused
|
|
1251
|
+
} : current);
|
|
1252
|
+
}, [setUpload]);
|
|
1643
1253
|
return {
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1254
|
+
pauseUpload: useCallback(() => {
|
|
1255
|
+
getGate()?.pause();
|
|
1256
|
+
setPaused(true);
|
|
1257
|
+
}, [getGate, setPaused]),
|
|
1258
|
+
resumeUpload: useCallback(() => {
|
|
1259
|
+
getGate()?.resume();
|
|
1260
|
+
setPaused(false);
|
|
1261
|
+
}, [getGate, setPaused])
|
|
1647
1262
|
};
|
|
1648
1263
|
}
|
|
1649
1264
|
//#endregion
|
|
@@ -1793,61 +1408,52 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1793
1408
|
intervalMs
|
|
1794
1409
|
}));
|
|
1795
1410
|
const { wake, cancel } = useRunControls(runId, getClient);
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
setUpload(void 0);
|
|
1819
|
-
}
|
|
1411
|
+
const submit = useCallback(async (input) => {
|
|
1412
|
+
const client = getClient();
|
|
1413
|
+
setStarting(true);
|
|
1414
|
+
setStartError(void 0);
|
|
1415
|
+
setRunId(void 0);
|
|
1416
|
+
session.current?.gate.cancel();
|
|
1417
|
+
const current = createUploadSession(workflow);
|
|
1418
|
+
session.current = current;
|
|
1419
|
+
try {
|
|
1420
|
+
const options = omitUndefined({ key });
|
|
1421
|
+
const started = await uploadFiles(client, input, setUpload, parallel, current);
|
|
1422
|
+
setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
|
|
1423
|
+
...options,
|
|
1424
|
+
wait
|
|
1425
|
+
})).runId);
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
if (!current.gate.cancelled) setStartError(errorMessage(err));
|
|
1428
|
+
} finally {
|
|
1429
|
+
if (session.current === current) {
|
|
1430
|
+
session.current = void 0;
|
|
1431
|
+
setStarting(false);
|
|
1432
|
+
setUpload(void 0);
|
|
1820
1433
|
}
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1434
|
+
}
|
|
1435
|
+
}, [
|
|
1436
|
+
workflow,
|
|
1437
|
+
key,
|
|
1438
|
+
wait,
|
|
1439
|
+
parallel,
|
|
1440
|
+
getClient
|
|
1441
|
+
]);
|
|
1442
|
+
const reset = useCallback(() => {
|
|
1443
|
+
session.current?.gate.cancel();
|
|
1444
|
+
session.current = void 0;
|
|
1445
|
+
setRunId(void 0);
|
|
1446
|
+
setStartError(void 0);
|
|
1447
|
+
setUpload(void 0);
|
|
1448
|
+
}, []);
|
|
1449
|
+
const { pauseUpload, resumeUpload } = useUploadPause(useCallback(() => session.current?.gate, []), setUpload);
|
|
1450
|
+
return {
|
|
1451
|
+
submit,
|
|
1452
|
+
reset,
|
|
1835
1453
|
wake,
|
|
1836
1454
|
cancel,
|
|
1837
|
-
pauseUpload
|
|
1838
|
-
|
|
1839
|
-
setUpload((current) => current ? {
|
|
1840
|
-
...current,
|
|
1841
|
-
paused: true
|
|
1842
|
-
} : current);
|
|
1843
|
-
}, []),
|
|
1844
|
-
resumeUpload: useCallback(() => {
|
|
1845
|
-
session.current?.gate.resume();
|
|
1846
|
-
setUpload((current) => current ? {
|
|
1847
|
-
...current,
|
|
1848
|
-
paused: false
|
|
1849
|
-
} : current);
|
|
1850
|
-
}, []),
|
|
1455
|
+
pauseUpload,
|
|
1456
|
+
resumeUpload,
|
|
1851
1457
|
run: tracked.run,
|
|
1852
1458
|
pending: starting || tracked.polling,
|
|
1853
1459
|
upload,
|
|
@@ -1994,208 +1600,6 @@ function humanize(name) {
|
|
|
1994
1600
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
1995
1601
|
}
|
|
1996
1602
|
//#endregion
|
|
1997
|
-
//#region use-workflow-progress.ts
|
|
1998
|
-
/**
|
|
1999
|
-
* `useWorkflowProgress` — read what a run has WRITTEN while it runs.
|
|
2000
|
-
*
|
|
2001
|
-
* The sibling of `useWorkflowRun`, and the split between them is the whole
|
|
2002
|
-
* reason this exists. That hook reports a run's STATE: the status transitions
|
|
2003
|
-
* the world records, which every run has. This reports what the run itself
|
|
2004
|
-
* wrote through `getWritable()`, which is the only thing a long run can say
|
|
2005
|
-
* before it finishes — a snapshot carries a status and, once terminal, an
|
|
2006
|
-
* output, and nothing in between. A page that shows only status shows
|
|
2007
|
-
* "Working…" for ten minutes and then a result.
|
|
2008
|
-
*
|
|
2009
|
-
* ## There is no poll fallback, and that is not an omission
|
|
2010
|
-
*
|
|
2011
|
-
* `useWorkflowRun` degrades to polling `GET /runs/:id` because a run's STATE is
|
|
2012
|
-
* readable that way. A run's written chunks are not: the stream is the only
|
|
2013
|
-
* route to them, so an agent that does not serve it has no progress to give and
|
|
2014
|
-
* `supported` says so once, rather than a poll pretending to look for something
|
|
2015
|
-
* that is not there. A page renders its status line either way.
|
|
2016
|
-
*
|
|
2017
|
-
* ## Chunks are RETAINED, so this is a replay as much as a tail
|
|
2018
|
-
*
|
|
2019
|
-
* The run's stream keeps every chunk, so a page that mounts late — a reload, a
|
|
2020
|
-
* second tab, a link opened tomorrow — reads the whole history from index 0 and
|
|
2021
|
-
* arrives at the same list as one that watched throughout. That is what makes a
|
|
2022
|
-
* durable run's progress durable too, and it is why the default `startIndex` is
|
|
2023
|
-
* 0 rather than "from now": a tail-only default would make the same page show
|
|
2024
|
-
* different things depending on when it opened.
|
|
2025
|
-
*
|
|
2026
|
-
* ## It RE-OPENS while the run is live, because a progress read is bounded
|
|
2027
|
-
*
|
|
2028
|
-
* The route answers with the chunks written when the request arrived and then
|
|
2029
|
-
* ends, reporting `complete` — whether the run itself was terminal. It has to:
|
|
2030
|
-
* a workflow stream signals its end only once CLOSED, and a progress channel
|
|
2031
|
-
* written by one step after another is never closed, so a read that waited for
|
|
2032
|
-
* the end would hang forever on a finished run. (It did: see the route's own
|
|
2033
|
-
* doc.)
|
|
2034
|
-
*
|
|
2035
|
-
* So this hook re-opens from where it left off until a read comes back
|
|
2036
|
-
* `complete`. That is a poll, and the honest description of progress is a durable
|
|
2037
|
-
* log rather than a socket — but it is a poll of a CHEAP shape: each read asks
|
|
2038
|
-
* only for chunks past the last index it saw, so a quiet run costs an empty
|
|
2039
|
-
* answer rather than the whole log again.
|
|
2040
|
-
*/
|
|
2041
|
-
/** How often a live run's progress is re-read once a bounded read has ended. */
|
|
2042
|
-
const DEFAULT_PROGRESS_POLL_MS = 1e3;
|
|
2043
|
-
/**
|
|
2044
|
-
* Drain one bounded read's frames, reporting how it ended and everything it
|
|
2045
|
-
* carried.
|
|
2046
|
-
*
|
|
2047
|
-
* The chunks are RETURNED rather than handed over one at a time, and that is
|
|
2048
|
-
* what lets the hook commit a whole read in one React update: a per-chunk
|
|
2049
|
-
* callback re-rendered the page once per progress line and rebuilt the list
|
|
2050
|
-
* each time, which for a fan-out writing a line per segment is an O(n²) copy of
|
|
2051
|
-
* the log a reader can already only see one frame at a time. A read is bounded
|
|
2052
|
-
* by construction (see the module doc), so buffering one is bounded too.
|
|
2053
|
-
*/
|
|
2054
|
-
async function consumeFrames(body, signal) {
|
|
2055
|
-
const chunks = [];
|
|
2056
|
-
let ending = "partial";
|
|
2057
|
-
for await (const frame of readEventStream(body, signal)) if (frame.event === "chunk") chunks.push(frame.data);
|
|
2058
|
-
else if (frame.event === "done") ending = frame.data?.complete ? "complete" : "partial";
|
|
2059
|
-
else if (frame.event === "missing") return {
|
|
2060
|
-
ending: "complete",
|
|
2061
|
-
chunks
|
|
2062
|
-
};
|
|
2063
|
-
return {
|
|
2064
|
-
ending,
|
|
2065
|
-
chunks
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
/**
|
|
2069
|
-
* Read one run's progress until it is complete, reporting each read's chunks.
|
|
2070
|
-
*
|
|
2071
|
-
* Module-level rather than inline in the hook, so the loop reads without React in
|
|
2072
|
-
* the way — the same split `useWorkflowRun` makes with `pollUntilTerminal`.
|
|
2073
|
-
*
|
|
2074
|
-
* Every re-open asks from an ABSOLUTE position past the last chunk read, so a
|
|
2075
|
-
* read only ever fetches what this reader has not seen. That is what keeps the
|
|
2076
|
-
* poll cheap: a quiet run answers with a bare `done` rather than the whole log
|
|
2077
|
-
* again.
|
|
2078
|
-
*
|
|
2079
|
-
* ## A negative `startIndex` is resolved on the FIRST read, not carried
|
|
2080
|
-
*
|
|
2081
|
-
* "The last N lines" names no position a later read can resume from — the tail
|
|
2082
|
-
* it counts back from moves with every line the run writes. Carrying it meant a
|
|
2083
|
-
* re-open asking for everything from 0 and dropping `seen` chunks off the
|
|
2084
|
-
* FRONT, which is a different set entirely: a reader that opened at
|
|
2085
|
-
* `startIndex: -3` on a 10-line log holds lines 7-9, and its next read handed
|
|
2086
|
-
* over lines 3 onwards — four lines it never asked for, then the three it
|
|
2087
|
-
* already had, in that order. The dedupe the old comment claimed would need the
|
|
2088
|
-
* first read's absolute tail, which the reader never learned.
|
|
2089
|
-
*
|
|
2090
|
-
* So the first read is issued from 0 instead, and only its last N chunks are
|
|
2091
|
-
* handed over. The window the caller asked for is unchanged, and the reader now
|
|
2092
|
-
* knows exactly where it is — every read after it is the ordinary absolute case.
|
|
2093
|
-
* The cost is that the first read transfers the whole log, which is what the
|
|
2094
|
-
* DEFAULT (`startIndex: 0`, "replay everything") already does.
|
|
2095
|
-
*/
|
|
2096
|
-
function readProgressUntilComplete(getClient, runId, options, intervalMs, onChunks, onEnded) {
|
|
2097
|
-
const start = options.startIndex ?? 0;
|
|
2098
|
-
const tail = start < 0 ? -start : void 0;
|
|
2099
|
-
let next = tail === void 0 ? start : 0;
|
|
2100
|
-
let firstRead = true;
|
|
2101
|
-
/** One bounded read. Resolves how it ended. */
|
|
2102
|
-
const readOnce = async (signal) => {
|
|
2103
|
-
const res = await getClient().streamOutput(runId, {
|
|
2104
|
-
...omitUndefined({
|
|
2105
|
-
namespace: options.namespace,
|
|
2106
|
-
startIndex: next === 0 ? void 0 : next
|
|
2107
|
-
}),
|
|
2108
|
-
signal
|
|
2109
|
-
});
|
|
2110
|
-
if (!(res.ok && res.body)) return "unsupported";
|
|
2111
|
-
const { ending, chunks } = await consumeFrames(res.body, signal);
|
|
2112
|
-
next += chunks.length;
|
|
2113
|
-
const fresh = firstRead && tail !== void 0 ? chunks.slice(-tail) : chunks;
|
|
2114
|
-
firstRead = false;
|
|
2115
|
-
if (fresh.length > 0 && !signal.aborted) onChunks(fresh);
|
|
2116
|
-
return ending;
|
|
2117
|
-
};
|
|
2118
|
-
return repeatUntil(intervalMs, async (signal) => {
|
|
2119
|
-
let ending;
|
|
2120
|
-
try {
|
|
2121
|
-
ending = await readOnce(signal);
|
|
2122
|
-
} catch {
|
|
2123
|
-
ending = "partial";
|
|
2124
|
-
}
|
|
2125
|
-
if (signal.aborted) return true;
|
|
2126
|
-
if (ending === "partial") return false;
|
|
2127
|
-
onEnded(ending);
|
|
2128
|
-
return true;
|
|
2129
|
-
});
|
|
2130
|
-
}
|
|
2131
|
-
/**
|
|
2132
|
-
* Follow one run's progress stream.
|
|
2133
|
-
*
|
|
2134
|
-
* Passing `undefined` (nothing started yet) costs nothing, and reading stops for
|
|
2135
|
-
* good once a read reports the run terminal — so a finished run costs one read.
|
|
2136
|
-
*
|
|
2137
|
-
* @example
|
|
2138
|
-
* ```tsx
|
|
2139
|
-
* import { useWorkflowProgress } from "@alexkroman1/aai-ui";
|
|
2140
|
-
*
|
|
2141
|
-
* function Progress({ runId }: { runId?: string }) {
|
|
2142
|
-
* const { progress, streaming, supported } = useWorkflowProgress(runId);
|
|
2143
|
-
* if (!supported) return null;
|
|
2144
|
-
* return (
|
|
2145
|
-
* <pre>
|
|
2146
|
-
* {progress.join("\n")}
|
|
2147
|
-
* {streaming && "\n…"}
|
|
2148
|
-
* </pre>
|
|
2149
|
-
* );
|
|
2150
|
-
* }
|
|
2151
|
-
* ```
|
|
2152
|
-
*
|
|
2153
|
-
* @typeParam T - What the workflow writes. Defaults to `string`, which is what
|
|
2154
|
-
* a progress channel usually carries; a workflow writing objects names its own
|
|
2155
|
-
* shape. Nothing in the browser can verify it — the route describes no type —
|
|
2156
|
-
* so this is the page's assertion about its own agent, narrowed once here
|
|
2157
|
-
* rather than at every read.
|
|
2158
|
-
*
|
|
2159
|
-
* @public
|
|
2160
|
-
*/
|
|
2161
|
-
function useWorkflowProgress(runId, opts = {}) {
|
|
2162
|
-
const { api, namespace, startIndex, intervalMs = DEFAULT_PROGRESS_POLL_MS } = opts;
|
|
2163
|
-
const [progress, setProgress] = useState([]);
|
|
2164
|
-
const [streaming, setStreaming] = useState(false);
|
|
2165
|
-
const [supported, setSupported] = useState(true);
|
|
2166
|
-
const getClient = useWorkflowApiRef(api);
|
|
2167
|
-
useEffect(() => {
|
|
2168
|
-
setProgress([]);
|
|
2169
|
-
setSupported(true);
|
|
2170
|
-
setStreaming(false);
|
|
2171
|
-
if (!runId) return;
|
|
2172
|
-
setStreaming(true);
|
|
2173
|
-
const stop = readProgressUntilComplete(getClient, runId, {
|
|
2174
|
-
namespace,
|
|
2175
|
-
startIndex
|
|
2176
|
-
}, intervalMs, (chunks) => setProgress((seen) => [...seen, ...chunks]), (ending) => {
|
|
2177
|
-
setStreaming(false);
|
|
2178
|
-
if (ending === "unsupported") setSupported(false);
|
|
2179
|
-
});
|
|
2180
|
-
return () => {
|
|
2181
|
-
stop();
|
|
2182
|
-
setStreaming(false);
|
|
2183
|
-
};
|
|
2184
|
-
}, [
|
|
2185
|
-
runId,
|
|
2186
|
-
namespace,
|
|
2187
|
-
startIndex,
|
|
2188
|
-
intervalMs,
|
|
2189
|
-
getClient
|
|
2190
|
-
]);
|
|
2191
|
-
return {
|
|
2192
|
-
progress,
|
|
2193
|
-
latest: progress.at(-1),
|
|
2194
|
-
streaming,
|
|
2195
|
-
supported
|
|
2196
|
-
};
|
|
2197
|
-
}
|
|
2198
|
-
//#endregion
|
|
2199
1603
|
//#region components/workflow-progress.tsx
|
|
2200
1604
|
/** @jsxImportSource react */
|
|
2201
1605
|
/**
|
|
@@ -2612,77 +2016,68 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
2612
2016
|
intervalMs
|
|
2613
2017
|
}));
|
|
2614
2018
|
const { wake, cancel } = useRunControls(runId, getClient);
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
setUpload(void 0);
|
|
2655
|
-
}
|
|
2019
|
+
const submit = useCallback(async (input) => {
|
|
2020
|
+
const client = getClient();
|
|
2021
|
+
setStarting(true);
|
|
2022
|
+
setStartError(void 0);
|
|
2023
|
+
setRunId(void 0);
|
|
2024
|
+
gateRef.current?.cancel();
|
|
2025
|
+
const gate = createUploadGate();
|
|
2026
|
+
gateRef.current = gate;
|
|
2027
|
+
let started;
|
|
2028
|
+
try {
|
|
2029
|
+
const id = randomUploadId();
|
|
2030
|
+
const begun = await beginRun({
|
|
2031
|
+
client,
|
|
2032
|
+
workflow,
|
|
2033
|
+
input,
|
|
2034
|
+
id,
|
|
2035
|
+
...omitUndefined({ key })
|
|
2036
|
+
});
|
|
2037
|
+
started = begun.runId;
|
|
2038
|
+
const chosen = begun.file;
|
|
2039
|
+
setRunId(started);
|
|
2040
|
+
if (!chosen) return;
|
|
2041
|
+
await streamFile({
|
|
2042
|
+
client,
|
|
2043
|
+
gate,
|
|
2044
|
+
id,
|
|
2045
|
+
file: chosen,
|
|
2046
|
+
parallel,
|
|
2047
|
+
report: setUpload
|
|
2048
|
+
});
|
|
2049
|
+
await client.wake(started).catch(() => void 0);
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
if (!gate.cancelled) setStartError(errorMessage(err));
|
|
2052
|
+
if (started) await client.cancel(started).catch(() => void 0);
|
|
2053
|
+
} finally {
|
|
2054
|
+
if (gateRef.current === gate) {
|
|
2055
|
+
gateRef.current = void 0;
|
|
2056
|
+
setStarting(false);
|
|
2057
|
+
setUpload(void 0);
|
|
2656
2058
|
}
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2059
|
+
}
|
|
2060
|
+
}, [
|
|
2061
|
+
workflow,
|
|
2062
|
+
key,
|
|
2063
|
+
parallel,
|
|
2064
|
+
getClient
|
|
2065
|
+
]);
|
|
2066
|
+
const reset = useCallback(() => {
|
|
2067
|
+
gateRef.current?.cancel();
|
|
2068
|
+
gateRef.current = void 0;
|
|
2069
|
+
setRunId(void 0);
|
|
2070
|
+
setStartError(void 0);
|
|
2071
|
+
setUpload(void 0);
|
|
2072
|
+
}, []);
|
|
2073
|
+
const { pauseUpload, resumeUpload } = useUploadPause(useCallback(() => gateRef.current, []), setUpload);
|
|
2074
|
+
return {
|
|
2075
|
+
submit,
|
|
2076
|
+
reset,
|
|
2670
2077
|
wake,
|
|
2671
2078
|
cancel,
|
|
2672
|
-
pauseUpload
|
|
2673
|
-
|
|
2674
|
-
setUpload((current) => current ? {
|
|
2675
|
-
...current,
|
|
2676
|
-
paused: true
|
|
2677
|
-
} : current);
|
|
2678
|
-
}, []),
|
|
2679
|
-
resumeUpload: useCallback(() => {
|
|
2680
|
-
gateRef.current?.resume();
|
|
2681
|
-
setUpload((current) => current ? {
|
|
2682
|
-
...current,
|
|
2683
|
-
paused: false
|
|
2684
|
-
} : current);
|
|
2685
|
-
}, []),
|
|
2079
|
+
pauseUpload,
|
|
2080
|
+
resumeUpload,
|
|
2686
2081
|
run: tracked.run,
|
|
2687
2082
|
pending: starting || tracked.polling,
|
|
2688
2083
|
upload,
|
|
@@ -2808,4 +2203,4 @@ const WORKFLOW_STATUS_LABELS = {
|
|
|
2808
2203
|
cancelled: "Cancelled"
|
|
2809
2204
|
};
|
|
2810
2205
|
//#endregion
|
|
2811
|
-
export { AutoScroll, Button, ChatView, CheckboxField, ConsoleShell, Controls,
|
|
2206
|
+
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 };
|