@alexkroman1/aai-cli 13.2.0 → 14.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.
Files changed (152) hide show
  1. package/bin.mjs +1 -1
  2. package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
  3. package/dist/_agent.d.ts +23 -0
  4. package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
  5. package/dist/_cli-common.d.ts +45 -6
  6. package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
  7. package/dist/_config.d.ts +25 -1
  8. package/dist/_dev-env.d.ts +1 -1
  9. package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
  10. package/dist/_dev-vite-config.d.ts +3 -3
  11. package/dist/_e2e-test-utils.d.ts +15 -7
  12. package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
  13. package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
  14. package/dist/_output.d.ts +24 -7
  15. package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
  16. package/dist/_resource-commands.d.ts +9 -9
  17. package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
  18. package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
  19. package/dist/_slug-api.d.ts +19 -0
  20. package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
  21. package/dist/_studio-commands.d.ts +13 -13
  22. package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
  23. package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
  24. package/dist/_typecheck-gate.d.ts +3 -1
  25. package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
  26. package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
  27. package/dist/_utils.d.ts +35 -0
  28. package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
  29. package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
  30. package/dist/_workflow-determinism.d.ts +3 -3
  31. package/dist/build-Mxk8gWvX.mjs +108 -0
  32. package/dist/build.d.ts +5 -0
  33. package/dist/cli.mjs +172 -116
  34. package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
  35. package/dist/client-bundler.mjs +1 -1
  36. package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
  37. package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
  38. package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
  39. package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
  40. package/dist/eval.d.ts +2 -0
  41. package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
  42. package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
  43. package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
  44. package/dist/project-config.mjs +1 -1
  45. package/dist/scaffold/.env.example +17 -1
  46. package/dist/scaffold/CLAUDE.md +84 -56
  47. package/dist/scaffold/package.json +6 -5
  48. package/dist/scaffold/server.mjs +129 -25
  49. package/dist/secret-Cjp912yF.mjs +162 -0
  50. package/dist/secret.d.ts +48 -13
  51. package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
  52. package/dist/templates/briefing-desk/agent.test.ts +7 -3
  53. package/dist/templates/call-audit/agent.test.ts +25 -9
  54. package/dist/templates/call-audit/agent.ts +2 -2
  55. package/dist/templates/call-audit/client.tsx +40 -58
  56. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  57. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  58. package/dist/templates/call-audit/workflows/media.ts +12 -37
  59. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  60. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  61. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  62. package/dist/templates/dispatch-center/client.tsx +53 -61
  63. package/dist/templates/dispatch-center/shared.ts +0 -8
  64. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  65. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  66. package/dist/templates/embedded-assets/agent.test.ts +7 -1
  67. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  68. package/dist/templates/health-assistant/agent.test.ts +8 -4
  69. package/dist/templates/infocom-adventure/client.tsx +40 -36
  70. package/dist/templates/link-digest/agent.test.ts +10 -4
  71. package/dist/templates/link-digest/agent.ts +2 -2
  72. package/dist/templates/link-digest/client.tsx +42 -36
  73. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  74. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  75. package/dist/templates/night-owl/agent.test.ts +6 -1
  76. package/dist/templates/night-owl/client.tsx +8 -2
  77. package/dist/templates/pipeline-simple/agent.test.ts +33 -16
  78. package/dist/templates/pizza-ordering/client.tsx +5 -4
  79. package/dist/templates/pizza-ordering/shared.ts +3 -8
  80. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  81. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  82. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  83. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  84. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  85. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  86. package/dist/templates/plan-and-execute/client.tsx +2 -2
  87. package/dist/templates/podcast-digest/agent.test.ts +33 -14
  88. package/dist/templates/podcast-digest/client.tsx +27 -36
  89. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  90. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  91. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  92. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  93. package/dist/templates/recap-workflow/agent.test.ts +49 -32
  94. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  95. package/dist/templates/redline/agent.eval.test.ts +5 -8
  96. package/dist/templates/redline/agent.test.ts +8 -2
  97. package/dist/templates/redline/client.tsx +47 -48
  98. package/dist/templates/redline/workflows/redline.ts +17 -17
  99. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  100. package/dist/templates/research-workflow/agent.test.ts +17 -8
  101. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  102. package/dist/templates/retail/agent.test.ts +17 -15
  103. package/dist/templates/retail/cancel.ts +7 -6
  104. package/dist/templates/retail/client.tsx +39 -58
  105. package/dist/templates/retail/payment.ts +4 -3
  106. package/dist/templates/retail/pending.test.ts +19 -19
  107. package/dist/templates/retail/registry.test.ts +99 -58
  108. package/dist/templates/retail/store.test.ts +5 -5
  109. package/dist/templates/retail/swap.ts +8 -11
  110. package/dist/templates/simple/agent.test.ts +39 -6
  111. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  112. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  113. package/dist/templates/solo-rpg/client.tsx +3 -3
  114. package/dist/templates/spoken-summary/agent.test.ts +21 -12
  115. package/dist/templates/spoken-summary/agent.ts +3 -3
  116. package/dist/templates/spoken-summary/client.tsx +36 -47
  117. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  118. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  119. package/dist/templates/support-line/client.tsx +4 -3
  120. package/dist/templates/support-line/prompts.ts +0 -2
  121. package/dist/templates/transcription-workflow/agent.test.ts +36 -20
  122. package/dist/templates/transcription-workflow/agent.ts +2 -2
  123. package/dist/templates/transcription-workflow/client.tsx +34 -40
  124. package/dist/templates/transcription-workflow/recover.ts +1 -1
  125. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  126. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  127. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  128. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  129. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  130. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  131. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  132. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  133. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  134. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  135. package/dist/templates/travel-concierge/client.tsx +2 -2
  136. package/dist/templates/travel-concierge/shared.ts +10 -14
  137. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  138. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  139. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  140. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  141. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  142. package/dist/test-D032V2cA.mjs +242 -0
  143. package/dist/test.d.ts +97 -31
  144. package/dist/typecheck.mjs +2 -2
  145. package/dist/worker-bundler.mjs +3 -3
  146. package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
  147. package/package.json +10 -10
  148. package/dist/bin.d.mts +0 -2
  149. package/dist/build-BLlLOrng.mjs +0 -66
  150. package/dist/secret-B-3T5tvr.mjs +0 -61
  151. package/dist/test-CiLab-AA.mjs +0 -182
  152. package/dist/tsdown.config.d.ts +0 -2
@@ -1,6 +1,6 @@
1
1
  import type { ToolContext, ToolDef, ToolInputSchema } from "@alexkroman1/aai";
2
2
  import { isToolFailure } from "@alexkroman1/aai";
3
- import { createToolContext, ok } from "@alexkroman1/aai/testing";
3
+ import { createToolContext, expectToolOk } from "@alexkroman1/aai/testing";
4
4
  import { describe, expect, test, vi } from "vitest";
5
5
  import {
6
6
  applyConsequences,
@@ -24,11 +24,11 @@ import updateState from "./tools/update_state.ts";
24
24
 
25
25
  // ── Test doubles ─────────────────────────────────────────────────────────────
26
26
 
27
- /** `send` is a spy rather than the recorder `createToolContext` installs,
28
- * because this suite asserts call counts on it. Each call gets its own slot
29
- * store, which is what makes two contexts two games. */
27
+ /** Each call gets its own slot store, which is what makes two contexts two
28
+ * games. `send` is the recorder `createToolContext` installs no test here
29
+ * asserts on it, and overriding it would only throw the recording away. */
30
30
  function makeCtx(): ToolContext {
31
- return createToolContext({ send: vi.fn() });
31
+ return createToolContext();
32
32
  }
33
33
 
34
34
  const SETUP_ARGS = {
@@ -288,7 +288,7 @@ describe("burn_momentum", () => {
288
288
  const ctx = makeCtx();
289
289
  seedRolledState(8, ctx); // 8 beats both dice (3, 5)
290
290
 
291
- const result = ok<Record<string, unknown>>(await callNoArgs(burnMomentum, ctx));
291
+ const result = expectToolOk<Record<string, unknown>>(await callNoArgs(burnMomentum, ctx));
292
292
  expect(result.burned).toBe(true);
293
293
  expect(result.newResultCode).toBe("STRONG_HIT");
294
294
 
@@ -302,7 +302,7 @@ describe("burn_momentum", () => {
302
302
  test("momentum beating only one die upgrades a MISS to WEAK_HIT", async () => {
303
303
  const ctx = makeCtx();
304
304
  seedRolledState(4, ctx); // beats 3, not 5
305
- const result = ok<{ newResultCode: string }>(await callNoArgs(burnMomentum, ctx));
305
+ const result = expectToolOk<{ newResultCode: string }>(await callNoArgs(burnMomentum, ctx));
306
306
  expect(result.newResultCode).toBe("WEAK_HIT");
307
307
  });
308
308
 
@@ -528,7 +528,7 @@ describe("update_state", () => {
528
528
  state.clocks[0]!.filled = 3; // 3 of 4
529
529
  seedPlaying(ctx, state);
530
530
 
531
- const result = ok<{ clockEvents: { clock: string; trigger: string }[] }>(
531
+ const result = expectToolOk<{ clockEvents: { clock: string; trigger: string }[] }>(
532
532
  await updateState.execute({ advanceClockName: "Doom" }, ctx),
533
533
  );
534
534
  expect(result.clockEvents).toEqual([{ clock: "Doom", trigger: "The doom arrives" }]);
@@ -542,7 +542,7 @@ describe("update_state", () => {
542
542
  }
543
543
  seedPlaying(ctx, state);
544
544
 
545
- const result = ok<{ warnings?: string[] }>(
545
+ const result = expectToolOk<{ warnings?: string[] }>(
546
546
  await updateState.execute({ addNpcName: "One Too Many" }, ctx),
547
547
  );
548
548
  expect(result.warnings?.[0]).toMatch(/NPC limit/);
@@ -609,7 +609,7 @@ describe("the story flow", () => {
609
609
  expect(created.state).toBe("playing.awaitingRoll");
610
610
  expect(created.instruction).toMatch(/action_roll/);
611
611
 
612
- ok(
612
+ expectToolOk(
613
613
  await actionRoll.execute(
614
614
  { move: "clash", stat: "iron", position: "risky", effect: "standard", purpose: "swing" },
615
615
  ctx,
@@ -618,7 +618,7 @@ describe("the story flow", () => {
618
618
  expect(storyFlow.position(ctx).state).toBe("playing.rollResolved");
619
619
 
620
620
  // Moving the scene on SPENDS the roll: the burn window is closed.
621
- ok(await updateState.execute({ location: "The Bridge" }, ctx));
621
+ expectToolOk(await updateState.execute({ location: "The Bridge" }, ctx));
622
622
  expect(storyFlow.position(ctx).state).toBe("playing.awaitingRoll");
623
623
  expect(isToolFailure(await callNoArgs(burnMomentum, ctx))).toBe(true);
624
624
  });
@@ -644,7 +644,7 @@ describe("the story flow", () => {
644
644
  // act on it, so a player could keep rolling after both tracks emptied. The
645
645
  // WRITE is `gameSlot`'s `after` hook; this tool no longer calls it, which
646
646
  // is the point of moving it there.
647
- ok(await updateState.execute({ health: 0, spirit: 0 }, ctx));
647
+ expectToolOk(await updateState.execute({ health: 0, spirit: 0 }, ctx));
648
648
  const at = storyFlow.position(ctx);
649
649
  expect(at.state).toBe("gameOver");
650
650
  expect(at.done).toBe(true);
@@ -1,6 +1,6 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
2
  import type { DeepReadonly } from "@alexkroman1/aai";
3
- import { client, useAgentState } from "@alexkroman1/aai-ui";
3
+ import { mountClient, useAgentState } from "@alexkroman1/aai-ui";
4
4
  import type { ReactNode } from "react";
5
5
  import {
6
6
  type Clock,
@@ -782,7 +782,7 @@ function Sidebar() {
782
782
 
783
783
  // ── App ──────────────────────────────────────────────────────────────────────
784
784
 
785
- client({
785
+ mountClient({
786
786
  name: "Solo RPG",
787
787
  icon: (
788
788
  <span style={{ fontSize: "28px", color: C.accent }}>
@@ -805,7 +805,7 @@ client({
805
805
  });
806
806
 
807
807
  // No `onSessionId`/`resumeSessionId` here any more, and their removal is a FIX
808
- // rather than a simplification. `createSessionCore` remembers the id in
808
+ // rather than a simplification. `createBrowserSession` remembers the id in
809
809
  // `sessionStorage` by default now — see `session-resume-store.ts`, whose doc
810
810
  // names this template as the one client of fourteen that had wired it by hand.
811
811
  // What it had wired was `localStorage`, which is the wrong store for a pointer
@@ -17,9 +17,9 @@
17
17
  * its return value, and audio in one is megabytes replayed on every resume.
18
18
  */
19
19
 
20
- import { readUpload, uploadInfo } from "@alexkroman1/aai/step";
20
+ import { stepReadUpload, stepUploadInfo } from "@alexkroman1/aai/step";
21
21
  import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
22
- import { createWorkflowCtx, stubGatewayRoute } from "@alexkroman1/aai/testing";
22
+ import { createWorkflowContext, stubGatewayRoute } from "@alexkroman1/aai/testing";
23
23
  import {
24
24
  installStubGateway,
25
25
  installStubReporter,
@@ -55,13 +55,22 @@ describe("the declaration", () => {
55
55
  test("is a workflow app with the one workflow the page starts by name", () => {
56
56
  // The page calls `api.start("spokenSummary", …)`, so a rename here is a
57
57
  // runtime 400 rather than a compile error. This is what pins it.
58
- expect(Object.keys(agentDef.workflows ?? {})).toEqual(["spokenSummary"]);
58
+ // `toContain` rather than an exact key list: adding a second workflow is an
59
+ // invited edit and must not redden a test the author did not write. The
60
+ // NAME is still pinned, deliberately — the page starts a run by this
61
+ // string, so renaming the key is a runtime 400 rather than a compile
62
+ // error, and this pin is the only thing that says so. Rename it here and
63
+ // in `client.tsx` together.
64
+ expect(Object.keys(agentDef.workflows ?? {})).toContain("spokenSummary");
65
+ expect(agentDef.workflows?.spokenSummary).toBe(spokenSummary);
59
66
  });
60
67
 
61
- test("declares no providers and exactly the one credential its steps read", () => {
68
+ test("declares the credential its steps read, so a deploy checks for it", () => {
62
69
  // A workflow app has no session, so nothing else in its config could name
63
70
  // one — and one AssemblyAI key covers transcription, the model and the voice.
64
- expect(agentDef.requiredEnv).toEqual(["ASSEMBLYAI_API_KEY"]);
71
+ // `toContain`, so adding a step that reads a second credential and
72
+ // declaring it here does not fail this test.
73
+ expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
65
74
  });
66
75
 
67
76
  test("takes the recording as an UPLOAD, which is what makes the form a file picker", () => {
@@ -206,14 +215,14 @@ describe("speaking", () => {
206
215
 
207
216
  const { audio } = await speak("Hello.");
208
217
 
209
- await expect(uploadInfo(audio)).resolves.toMatchObject({
218
+ await expect(stepUploadInfo(audio)).resolves.toMatchObject({
210
219
  name: "summary.wav",
211
220
  // The byte route serves this as `Content-Type`, and a browser will not
212
221
  // play inline a file it was handed as octet-stream.
213
222
  type: "audio/wav",
214
223
  size: 44 + 4000,
215
224
  });
216
- const { bytes } = await readUpload(audio, { end: 12 });
225
+ const { bytes } = await stepReadUpload(audio, { end: 12 });
217
226
  expect(String.fromCharCode(...bytes.subarray(0, 4))).toBe("RIFF");
218
227
  expect(String.fromCharCode(...bytes.subarray(8, 12))).toBe("WAVE");
219
228
  });
@@ -283,7 +292,7 @@ describe("the whole run", () => {
283
292
  installStubReporter();
284
293
  installStubSpeech();
285
294
 
286
- const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx());
295
+ const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext());
287
296
 
288
297
  expect(summary).toEqual({
289
298
  source: "standup.wav",
@@ -305,7 +314,7 @@ describe("the whole run", () => {
305
314
  installStubReporter();
306
315
  const speech = installStubSpeech();
307
316
 
308
- await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowCtx());
317
+ await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowContext());
309
318
 
310
319
  expect(speech.calls[0]).toMatchObject({ text: "Spoken.", voice: "michael" });
311
320
  });
@@ -315,9 +324,9 @@ describe("the whole run", () => {
315
324
  installStubReporter();
316
325
  installStubSpeech();
317
326
 
318
- await expect(spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx())).rejects.toThrow(
319
- "corrupt audio",
320
- );
327
+ await expect(
328
+ spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext()),
329
+ ).rejects.toThrow("corrupt audio");
321
330
  });
322
331
  });
323
332
 
@@ -24,7 +24,7 @@
24
24
  * event stream wired into a live pipeline's playback, with a turn tracker and
25
25
  * barge-in behind it, and a step has no turn to be part of and has to return
26
26
  * a VALUE.
27
- * - **`writeUpload`** (same subpath) puts that value where a browser can reach
27
+ * - **`stepWriteUpload`** (same subpath) puts that value where a browser can reach
28
28
  * it. A run's OUTPUT is read back as JSON, so audio cannot travel in one —
29
29
  * the same rule that keeps a recording's bytes out of a run's INPUT, arriving
30
30
  * at the other end of the run.
@@ -54,7 +54,7 @@
54
54
  * `POST /workflows/uploads` (the browser does this for you: `uploads` below is
55
55
  * what makes `<WorkflowFields>` render a file picker, and `useWorkflowSubmit`
56
56
  * stores the file before starting the run), the input carries the returned id,
57
- * and the step that needs the bytes streams them out with `readUpload`.
57
+ * and the step that needs the bytes streams them out with `stepReadUpload`.
58
58
  *
59
59
  * ## It is scriptable, which is the other half of having an API
60
60
  *
@@ -140,7 +140,7 @@ export const spokenSummary: WorkflowDef<typeof spokenSummaryInput, SpokenSummary
140
140
  input: spokenSummaryInput,
141
141
  // The one line that makes the form take a file: `<WorkflowFields>` renders a
142
142
  // picker for this property, `useWorkflowSubmit` stores the chosen file, and
143
- // the step that transcribes it reads it back with `readUpload`.
143
+ // the step that transcribes it reads it back with `stepReadUpload`.
144
144
  uploads: ["recording"],
145
145
  run: spokenSummaryFlow,
146
146
  });
@@ -37,14 +37,15 @@
37
37
  * in React state — so a refresh lost it while the recording carried on being
38
38
  * transcribed, summarized and spoken. The bytes were already stored, so the
39
39
  * expensive half had happened; what an empty form invites is somebody uploading
40
- * the recording again and paying for all four legs twice. `key` is the handle
41
- * that survives a reload, `recover: true` is what reads it back, and the upload
42
- * half is already the SDK's picking the same file again sends only the windows
43
- * that did not land.
40
+ * the recording again and paying for all four legs twice. Both halves are the
41
+ * SDK's now and this page writes neither: `useWorkflowSubmit` records the run
42
+ * under a correlation KEY it keeps for this page and asks for it again on the
43
+ * next load, and picking the same file again sends only the windows that did
44
+ * not land.
44
45
  *
45
- * **The key is `useRunKey()`** — opaque, and in `sessionStorage`, which is the
46
- * same lifetime as that upload recall, so both halves of a reload make the same
47
- * promise. What that rules out is a `?key=` parameter, and it is worth being
46
+ * **That key is `useRunKey()`'s** — opaque, and in `sessionStorage`, which is
47
+ * the same lifetime as the upload recall, so both halves of a reload make the
48
+ * same promise. What it rules out is a `?key=` parameter, and it is worth being
48
49
  * plain about the trade because this template exists to produce something
49
50
  * sendable: a summary you can listen to is exactly the sort of thing somebody
50
51
  * forwards, and a URL is how people forward things. But there is no per-user
@@ -57,9 +58,9 @@
57
58
  * the recording would be worse again: two people summarizing one file would
58
59
  * recover each other's runs.
59
60
  *
60
- * A real app with accounts passes the account's own id instead, and then a
61
- * summary follows the person to another device — a promise only a login can
62
- * keep.
61
+ * A real app with accounts passes the account's own id as `key` instead, and
62
+ * then a summary follows the person to another device — a promise only a login
63
+ * can keep.
63
64
  */
64
65
 
65
66
  import "@alexkroman1/aai-ui/styles.css";
@@ -68,18 +69,18 @@ import "@alexkroman1/aai-ui/styles.css";
68
69
  // `workflows/summarize.ts` already declares.
69
70
  import { formatDuration } from "@alexkroman1/aai/utils";
70
71
  import {
72
+ BulletList,
71
73
  createWorkflowApi,
74
+ Facts,
72
75
  Form,
73
- page,
76
+ mountPage,
74
77
  SubmitButton,
75
78
  UploadProgressBar,
76
79
  useDownloadUrl,
77
- useRunKey,
78
80
  useWorkflowSubmit,
79
81
  WorkflowFields,
80
82
  WorkflowProgress,
81
83
  } from "@alexkroman1/aai-ui";
82
- import { useState } from "react";
83
84
  import type { spokenSummary } from "./agent.ts";
84
85
 
85
86
  /**
@@ -128,20 +129,11 @@ function captionsUrl(text: string, durationMs: number): string {
128
129
  }
129
130
 
130
131
  export function App() {
131
- // This tab's handle on its own summaries, in the store the upload recall uses
132
- // — see the module doc for why a `?key=` is the wrong trade here.
133
- const key = useRunKey();
134
- // Did THIS load start the run? A reload cannot have, and that is the only way
135
- // the page can tell "working on what you just sent" from "picking up where
136
- // you left off" — the hook reports the run, not who asked for it.
137
- const [startedHere, setStartedHere] = useState(false);
138
132
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
139
- // `run.output` instead of `unknown`. Neither half of the recovery is useful
140
- // alone: without `key` there is nothing to find the run by, and without
141
- // `recover` the key is only ever written.
142
- const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
143
- typeof spokenSummary
144
- >(WORKFLOW, { api, key, recover: true });
133
+ // `run.output` instead of `unknown`. The reload is the hook's own doing — see
134
+ // the module doc for why the key it mints is the right one for this page.
135
+ const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
136
+ useWorkflowSubmit<typeof spokenSummary>(WORKFLOW, { api });
145
137
  const output = run?.status === "completed" ? run.output : undefined;
146
138
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
147
139
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -157,14 +149,7 @@ export function App() {
157
149
  </p>
158
150
  </header>
159
151
 
160
- <Form
161
- onSubmit={(values) => {
162
- setStartedHere(true);
163
- return submitForm(values);
164
- }}
165
- error={error}
166
- className="flex flex-col gap-4"
167
- >
152
+ <Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
168
153
  {/* Every control, from the workflow's own input schema. See the module doc. */}
169
154
  <WorkflowFields workflow={WORKFLOW} />
170
155
  <SubmitButton pending={pending} pendingLabel="Working…">
@@ -184,25 +169,27 @@ export function App() {
184
169
  nothing for `<WorkflowProgress>` to read. */}
185
170
  <UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
186
171
 
187
- {/* What the run itself says, from `report()` in the workflow's steps. */}
172
+ {/* What the run itself says, from `stepReport()` in the workflow's steps. */}
188
173
  <WorkflowProgress runId={run?.runId} api={api} />
189
174
 
190
- {run?.status === "failed" && <p className="text-red-600">That one failed: {run.error}</p>}
175
+ {/* `role="alert"`, the same contract `<Form>` gives the submit error: this
176
+ is the outcome the reader waited minutes for. */}
177
+ {run?.status === "failed" && (
178
+ <p role="alert" className="text-red-600">
179
+ That one failed: {run.error}
180
+ </p>
181
+ )}
191
182
 
192
183
  {output !== undefined && (
193
184
  <article className="flex flex-col gap-5">
194
185
  <div className="flex flex-col gap-1">
195
186
  <h2 className="text-xl">{output.headline}</h2>
196
- <p className="text-sm opacity-70">
197
- {output.source} · {formatDuration(output.durationMs)} · {output.words} words
198
- </p>
187
+ <Facts
188
+ items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
189
+ />
199
190
  </div>
200
191
 
201
- <ul className="flex list-disc flex-col gap-1 pl-5">
202
- {output.points.map((point) => (
203
- <li key={point}>{point}</li>
204
- ))}
205
- </ul>
192
+ <BulletList items={output.points} />
206
193
 
207
194
  <section className="flex flex-col gap-2">
208
195
  <h3 className="text-sm font-medium opacity-70">
@@ -210,11 +197,13 @@ export function App() {
210
197
  </h3>
211
198
  {audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
212
199
  {audio.error !== undefined && (
213
- <p className="text-red-600">Could not load the audio: {audio.error}</p>
200
+ <p role="alert" className="text-red-600">
201
+ Could not load the audio: {audio.error}
202
+ </p>
214
203
  )}
215
204
  {audio.url !== undefined && (
216
205
  <>
217
- <audio controls src={audio.url} className="w-full">
206
+ <audio aria-label="Summary read aloud" controls src={audio.url} className="w-full">
218
207
  {/* A real caption track, not a suppression: the summary was
219
208
  written before it was spoken, so the words are already
220
209
  here and one cue spanning the clip is an honest
@@ -248,4 +237,4 @@ export function App() {
248
237
  );
249
238
  }
250
239
 
251
- page({ name: "Spoken Summary", component: App });
240
+ mountPage({ name: "Spoken Summary", component: App });
@@ -18,7 +18,7 @@
18
18
  * - **`stepSpeak`** synthesizes from inside a step. The session TTS surface
19
19
  * cannot: a `TtsSession` is an event stream wired into a live pipeline's
20
20
  * playback, and a step has no turn to be part of and has to return a VALUE.
21
- * - **`writeUpload`** puts that value somewhere. A run's OUTPUT is read back as
21
+ * - **`stepWriteUpload`** puts that value somewhere. A run's OUTPUT is read back as
22
22
  * JSON, so audio cannot travel in one — the same rule that keeps a
23
23
  * recording's bytes out of a run's INPUT, arriving at the other end of the
24
24
  * run. The bytes go to the store, the output carries the id, and the page
@@ -49,9 +49,9 @@
49
49
  * expensive one.
50
50
  */
51
51
 
52
- import type { WorkflowCtx } from "@alexkroman1/aai";
53
- import { report, stepSpeak, TRANSCRIBE_API, writeUpload } from "@alexkroman1/aai/step";
54
- import { stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
52
+ import type { WorkflowContext } from "@alexkroman1/aai";
53
+ import { stepReport, stepSpeak, stepWriteUpload, TRANSCRIBE_API } from "@alexkroman1/aai/step";
54
+ import { stepGenerateJsonOrFail } from "@alexkroman1/aai/step-errors";
55
55
  import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
56
56
  // ERASED at build time, so the body can name the schema's own output type without
57
57
  // a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
@@ -128,7 +128,7 @@ export type SpokenSummary = {
128
128
  /** Transcribe a recording, summarize it, and read the summary back. */
129
129
  export async function spokenSummaryFlow(
130
130
  input: WorkflowInputOf<typeof spokenSummary>,
131
- ctx: WorkflowCtx,
131
+ ctx: WorkflowContext,
132
132
  ): Promise<SpokenSummary> {
133
133
  const transcript = await transcribe(input.recording, ctx);
134
134
  const summary = await ctx.step("summarize", () => summarize(transcript.text));
@@ -157,7 +157,7 @@ export async function spokenSummaryFlow(
157
157
  * re-derives exactly the same sequence. It takes the `ctx` for that reason: a
158
158
  * helper that reaches the journal has to be handed the handle.
159
159
  */
160
- async function transcribe(recording: string, ctx: WorkflowCtx): Promise<Transcript> {
160
+ async function transcribe(recording: string, ctx: WorkflowContext): Promise<Transcript> {
161
161
  // `maxAttempts: 6` was `uploadToProvider.maxRetries = 5` — five retries after
162
162
  // the first attempt. It is the one step here worth extra patience: it streams
163
163
  // the whole recording, so a transient failure is expensive to reach again.
@@ -187,8 +187,8 @@ async function transcribe(recording: string, ctx: WorkflowCtx): Promise<Transcri
187
187
  export async function summarize(
188
188
  text: string,
189
189
  ): Promise<{ headline: string; points: string[]; spoken: string }> {
190
- await report("Summarizing the transcript.");
191
- const reply = await stepGenerateJsonClassified(
190
+ await stepReport("Summarizing the transcript.");
191
+ const reply = await stepGenerateJsonOrFail(
192
192
  "Summarize this transcript of a recording.\n\n" +
193
193
  "Answer with JSON only, in this shape:\n" +
194
194
  `{"headline": "...", "points": ["..."], "spoken": "..."}\n\n` +
@@ -204,7 +204,7 @@ export async function summarize(
204
204
  system: "You summarize recordings. You answer with JSON and nothing else.",
205
205
  schema: SummaryReply,
206
206
  },
207
- // The `Classified` caller is `stepGenerateJson` plus `throwStepError`, which
207
+ // The `OrFail` caller is `stepGenerateJson` plus `throwStepError`, which
208
208
  // reads the gateway's own status: a 429 is worth another attempt and a 400
209
209
  // is not, and that is what tells the DevKit which.
210
210
  );
@@ -226,7 +226,7 @@ export async function speak(
226
226
  voice?: string,
227
227
  ): Promise<{ audio: string; durationMs: number }> {
228
228
  const spoken = await stepSpeak(script, omitUndefined({ voice }));
229
- const stored = await writeUpload(spoken.audio, {
229
+ const stored = await stepWriteUpload(spoken.audio, {
230
230
  // Named, because this is what a person sees on the download link rather
231
231
  // than an opaque id — and typed, because the byte route serves the type it
232
232
  // was given and a browser will not play a file it was handed as bytes.
@@ -234,7 +234,7 @@ export async function speak(
234
234
  type: "audio/wav",
235
235
  });
236
236
 
237
- await report(
237
+ await stepReport(
238
238
  `Recorded a ${Math.round(spoken.durationMs / 1000)}s summary in ${spoken.voice}'s voice.`,
239
239
  );
240
240
  return { audio: stored.id, durationMs: spoken.durationMs };
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * **Every one of them is four lines, because the SDK owns the endpoint.**
14
14
  * `stepTranscribeUpload` / `stepTranscribeSubmit` / `stepTranscribePoll` on
15
- * `@alexkroman1/aai/step` — reached here through their `*Classified` callers on
15
+ * `@alexkroman1/aai/step` — reached here through their `*OrFail` callers on
16
16
  * `@alexkroman1/aai/step-errors` — carry the URL, the raw-key auth, the windowed
17
17
  * streaming upload, the PLURAL `speech_models` field and the failure
18
18
  * classification — all of which this file used to spell out, and all of which
@@ -40,11 +40,11 @@
40
40
  * the one leg that should be as boring as possible.
41
41
  */
42
42
 
43
- import { report, uploadInfo } from "@alexkroman1/aai/step";
43
+ import { stepReport, stepUploadInfo } from "@alexkroman1/aai/step";
44
44
  import {
45
- stepTranscribePollClassified,
46
- stepTranscribeSubmitClassified,
47
- stepTranscribeUploadClassified,
45
+ stepTranscribePollOrFail,
46
+ stepTranscribeSubmitOrFail,
47
+ stepTranscribeUploadOrFail,
48
48
  } from "@alexkroman1/aai/step-errors";
49
49
  import { countWords, formatBytes } from "@alexkroman1/aai/utils";
50
50
 
@@ -85,24 +85,24 @@ export type Transcript = {
85
85
  * expires before the next step runs; that costs one fresh upload, once, instead
86
86
  * of five.
87
87
  *
88
- * The `Classified` callers on `@alexkroman1/aai/step-errors` are the SDK's own
88
+ * The `OrFail` callers on `@alexkroman1/aai/step-errors` are the SDK's own
89
89
  * `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns
90
90
  * the SDK's `TranscribeError` into the DevKit's verdict — a missing key and a
91
91
  * 400 stop, a 429 waits as long as the service asked. Every step here ends the
92
92
  * same way for the same reason.
93
93
  */
94
94
  export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
95
- const stored = await uploadInfo(uploadId);
96
- await report(
95
+ const stored = await stepUploadInfo(uploadId);
96
+ await stepReport(
97
97
  `Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
98
98
  );
99
- return await stepTranscribeUploadClassified(uploadId);
99
+ return await stepTranscribeUploadOrFail(uploadId);
100
100
  }
101
101
 
102
102
  /** Create the transcription job, and answer with the id that outlives this run. */
103
103
  export async function createJob(audioUrl: string): Promise<{ id: string }> {
104
- const job = await stepTranscribeSubmitClassified(audioUrl);
105
- await report(`Transcribing — job ${job.id}.`);
104
+ const job = await stepTranscribeSubmitOrFail(audioUrl);
105
+ await stepReport(`Transcribing — job ${job.id}.`);
106
106
  return job;
107
107
  }
108
108
 
@@ -119,11 +119,11 @@ export async function pollTranscript(
119
119
  uploadId: string,
120
120
  id: string,
121
121
  ): Promise<{ done: false } | { done: true; transcript: Transcript }> {
122
- const progress = await stepTranscribePollClassified(id);
122
+ const progress = await stepTranscribePollOrFail(id);
123
123
  if (!progress.done) return { done: false };
124
124
 
125
- const stored = await uploadInfo(uploadId);
126
- await report(`Transcribed ${countWords(progress.transcript.text)} words.`);
125
+ const stored = await stepUploadInfo(uploadId);
126
+ await stepReport(`Transcribed ${countWords(progress.transcript.text)} words.`);
127
127
  return {
128
128
  done: true,
129
129
  transcript: {
@@ -1,5 +1,6 @@
1
+ import { plural } from "@alexkroman1/aai/utils";
1
2
  import "@alexkroman1/aai-ui/styles.css";
2
- import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
3
+ import { AutoScroll, mountClient, useAgentState } from "@alexkroman1/aai-ui";
3
4
  import { PRODUCT, supportProjection } from "./shared.ts";
4
5
 
5
6
  /**
@@ -19,7 +20,7 @@ function TraceSidebar() {
19
20
  <div>
20
21
  <h3 className="text-sm font-bold uppercase tracking-wide opacity-60">{support.product}</h3>
21
22
  <p className="text-xs opacity-50">
22
- {support.asked.length} question{support.asked.length === 1 ? "" : "s"} this call
23
+ {support.asked.length} {plural(support.asked.length, "question")} this call
23
24
  {support.ticket ? ` · ticket ${support.ticket}` : ""}
24
25
  </p>
25
26
  </div>
@@ -97,7 +98,7 @@ function TraceSidebar() {
97
98
  );
98
99
  }
99
100
 
100
- client({
101
+ mountClient({
101
102
  // Derived, not typed twice: `PRODUCT` comes off `knowledge.json`, which is
102
103
  // what `agent.ts` names the agent and greets with. A knowledge base swapped
103
104
  // for another product otherwise leaves the browser tab advertising the old
@@ -40,8 +40,6 @@ export const binaryScore = z.object({
40
40
  reason: z.string().max(200).describe("One short sentence of justification"),
41
41
  });
42
42
 
43
- export type BinaryScore = z.infer<typeof binaryScore>;
44
-
45
43
  /** Their `retrieval_grader`: deliberately NOT a stringent test. */
46
44
  export const DOC_GRADER_SYSTEM = [
47
45
  "You are a grader assessing whether a retrieved support document is relevant",