@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,7 @@
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
1
2
  import "@alexkroman1/aai-ui/styles.css";
2
- import { client, useAgentState } from "@alexkroman1/aai-ui";
3
- import { formatPrice, orderProjection, pizzaPrice } from "./shared.ts";
3
+ import { mountClient, useAgentState } from "@alexkroman1/aai-ui";
4
+ import { orderProjection, pizzaPrice } from "./shared.ts";
4
5
 
5
6
  function PizzaIcon({ size }: { size: string }) {
6
7
  const dim = size === "small" ? 36 : size === "large" ? 52 : 44;
@@ -67,7 +68,7 @@ function OrderSidebar() {
67
68
  </p>
68
69
  </div>
69
70
  <p className="text-sm font-bold whitespace-nowrap text-aai-primary">
70
- {formatPrice(pizzaPrice(p))}
71
+ {formatMoney(pizzaPrice(p))}
71
72
  </p>
72
73
  </div>
73
74
  ))}
@@ -79,7 +80,7 @@ function OrderSidebar() {
79
80
  );
80
81
  }
81
82
 
82
- client({
83
+ mountClient({
83
84
  name: "Pizza Palace",
84
85
  sidebar: OrderSidebar,
85
86
  theme: {
@@ -1,4 +1,5 @@
1
1
  import { type DeepReadonly, sessionSlot } from "@alexkroman1/aai";
2
+ import { formatMoney } from "@alexkroman1/aai/utils";
2
3
 
3
4
  export const SIZES = ["small", "medium", "large"] as const;
4
5
  export const CRUSTS = ["thin", "regular", "thick", "stuffed"] as const;
@@ -37,19 +38,13 @@ export function calculateTotal(pizzas: readonly ReadonlyPizza[]): number {
37
38
  return pizzas.reduce((total, pizza) => total + pizzaPrice(pizza), 0);
38
39
  }
39
40
 
40
- /** The one money format. Tool results, the projection, and the sidebar all
41
- * show prices through this, so they can never disagree on rounding. */
42
- export function formatPrice(amount: number): string {
43
- return `$${amount.toFixed(2)}`;
44
- }
45
-
46
41
  /**
47
42
  * The menu as prompt prose, generated from `MENU` so the agent can never
48
43
  * quote a price the pricing code doesn't charge.
49
44
  */
50
45
  export function menuText(): string {
51
46
  const price = (amount: number, upcharge = false) =>
52
- amount === 0 ? "free" : `${upcharge ? "+" : ""}${formatPrice(amount)}`;
47
+ amount === 0 ? "free" : `${upcharge ? "+" : ""}${formatMoney(amount)}`;
53
48
  const list = (items: Record<string, number>, upcharge = false) =>
54
49
  Object.entries(items)
55
50
  .map(([name, amount]) => `${name.replaceAll("_", " ")} (${price(amount, upcharge)})`)
@@ -169,7 +164,7 @@ export function orderView(order: FrozenOrderState): OrderView {
169
164
  const placed = order.placed;
170
165
  return {
171
166
  pizzas: order.pizzas,
172
- total: placed?.total ?? formatPrice(calculateTotal(order.pizzas)),
167
+ total: placed?.total ?? formatMoney(calculateTotal(order.pizzas)),
173
168
  orderPlaced: Boolean(placed),
174
169
  ...(placed
175
170
  ? { orderNumber: placed.orderNumber, estimatedMinutes: placed.estimatedMinutes }
@@ -1,5 +1,6 @@
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
1
2
  import { z } from "zod";
2
- import { CRUSTS, calculateTotal, formatPrice, orderSlot, type Pizza, SIZES } from "../shared.ts";
3
+ import { CRUSTS, calculateTotal, orderSlot, type Pizza, SIZES } from "../shared.ts";
3
4
 
4
5
  export default orderSlot.updateTool({
5
6
  description: "Add a pizza to the order. Use when the customer has decided on a pizza.",
@@ -24,7 +25,7 @@ export default orderSlot.updateTool({
24
25
 
25
26
  return {
26
27
  added: pizza,
27
- orderTotal: formatPrice(calculateTotal(order.pizzas)),
28
+ orderTotal: formatMoney(calculateTotal(order.pizzas)),
28
29
  itemCount: order.pizzas.length,
29
30
  };
30
31
  },
@@ -1,5 +1,6 @@
1
1
  import { toolFailure } from "@alexkroman1/aai";
2
- import { calculateTotal, formatPrice, orderSlot, resetOrder } from "../shared.ts";
2
+ import { formatMoney } from "@alexkroman1/aai/utils";
3
+ import { calculateTotal, orderSlot, resetOrder } from "../shared.ts";
3
4
 
4
5
  export default orderSlot.updateTool({
5
6
  description:
@@ -9,7 +10,7 @@ export default orderSlot.updateTool({
9
10
  if (pizzas.length === 0) return toolFailure("Cannot place an empty order.");
10
11
 
11
12
  const customerName = order.customerName ?? "Guest";
12
- const total = formatPrice(calculateTotal(pizzas));
13
+ const total = formatMoney(calculateTotal(pizzas));
13
14
  const orderNumber = Math.floor(1000 + Math.random() * 9000);
14
15
 
15
16
  const estimatedMinutes = 15 + pizzas.length * 5;
@@ -1,6 +1,7 @@
1
1
  import { toolFailure } from "@alexkroman1/aai";
2
+ import { formatMoney } from "@alexkroman1/aai/utils";
2
3
  import { z } from "zod";
3
- import { calculateTotal, formatPrice, orderSlot } from "../shared.ts";
4
+ import { calculateTotal, orderSlot } from "../shared.ts";
4
5
 
5
6
  export default orderSlot.updateTool({
6
7
  description: "Remove a pizza from the order by its ID.",
@@ -15,7 +16,7 @@ export default orderSlot.updateTool({
15
16
 
16
17
  return {
17
18
  removed,
18
- orderTotal: formatPrice(calculateTotal(order.pizzas)),
19
+ orderTotal: formatMoney(calculateTotal(order.pizzas)),
19
20
  itemCount: order.pizzas.length,
20
21
  };
21
22
  },
@@ -1,6 +1,7 @@
1
1
  import { toolFailure } from "@alexkroman1/aai";
2
+ import { formatMoney } from "@alexkroman1/aai/utils";
2
3
  import { z } from "zod";
3
- import { CRUSTS, calculateTotal, formatPrice, orderSlot, type Pizza, SIZES } from "../shared.ts";
4
+ import { CRUSTS, calculateTotal, orderSlot, type Pizza, SIZES } from "../shared.ts";
4
5
 
5
6
  export default orderSlot.updateTool({
6
7
  description: "Update an existing pizza in the order. Only provided fields are changed.",
@@ -23,6 +24,6 @@ export default orderSlot.updateTool({
23
24
 
24
25
  order.pizzas[idx] = pizza;
25
26
 
26
- return { updated: pizza, orderTotal: formatPrice(calculateTotal(order.pizzas)) };
27
+ return { updated: pizza, orderTotal: formatMoney(calculateTotal(order.pizzas)) };
27
28
  },
28
29
  });
@@ -1,4 +1,5 @@
1
- import { calculateTotal, formatPrice, orderSlot } from "../shared.ts";
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
2
+ import { calculateTotal, orderSlot } from "../shared.ts";
2
3
 
3
4
  export default orderSlot.tool({
4
5
  description: "View the current order summary with all pizzas and total price.",
@@ -15,7 +16,7 @@ export default orderSlot.tool({
15
16
  toppings: p.toppings,
16
17
  quantity: p.quantity,
17
18
  })),
18
- orderTotal: formatPrice(calculateTotal(pizzas)),
19
+ orderTotal: formatMoney(calculateTotal(pizzas)),
19
20
  };
20
21
  },
21
22
  });
@@ -1,7 +1,12 @@
1
1
  /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
2
2
  import agentDef from "virtual:aai/agent";
3
3
  import type { ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, okPosition, stubGenerate, toolRunner } from "@alexkroman1/aai/testing";
4
+ import {
5
+ createToolContext,
6
+ expectDialogOk,
7
+ stubGenerate,
8
+ toolRunner,
9
+ } from "@alexkroman1/aai/testing";
5
10
  import { describe, expect, test } from "vitest";
6
11
 
7
12
  import { executeStep, MAX_STEP_SEARCHES, normalizeAct, planNode } from "./procedure.ts";
@@ -216,7 +221,7 @@ describe("work_next_step", () => {
216
221
  const ctx = createToolContext({ generate });
217
222
  await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
218
223
 
219
- const first = okPosition<{
224
+ const first = expectDialogOk<{
220
225
  finished: boolean;
221
226
  step: string;
222
227
  result: string;
@@ -243,7 +248,7 @@ describe("work_next_step", () => {
243
248
  });
244
249
  const ctx = createToolContext({ generate });
245
250
  await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
246
- const answered = okPosition<{ finished: boolean; response: string }>(
251
+ const answered = expectDialogOk<{ finished: boolean; response: string }>(
247
252
  await run("work_next_step", ctx),
248
253
  );
249
254
 
@@ -327,7 +332,7 @@ describe("revise_plan", () => {
327
332
  await run("work_next_step", ctx);
328
333
  expect(stateOf(ctx).response).not.toBeNull();
329
334
 
330
- const revised = okPosition<{ finished: boolean; remaining: string[] }>(
335
+ const revised = expectDialogOk<{ finished: boolean; remaining: string[] }>(
331
336
  await run("revise_plan", { instruction: "make it Porto instead" }, ctx),
332
337
  );
333
338
  expect(revised.result.finished).toBe(false);
@@ -362,7 +367,7 @@ describe("revise_plan", () => {
362
367
  });
363
368
  const ctx = createToolContext({ generate });
364
369
  await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
365
- const revised = okPosition<{ finished: boolean }>(
370
+ const revised = expectDialogOk<{ finished: boolean }>(
366
371
  await run("revise_plan", { instruction: "never mind, it is booked" }, ctx),
367
372
  );
368
373
  expect(revised.result.finished).toBe(true);
@@ -1,5 +1,5 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
2
+ import { AutoScroll, mountClient, useAgentState } from "@alexkroman1/aai-ui";
3
3
  import { planProjection } from "./shared.ts";
4
4
 
5
5
  /**
@@ -90,7 +90,7 @@ function PlanSidebar() {
90
90
  );
91
91
  }
92
92
 
93
- client({
93
+ mountClient({
94
94
  name: "Planning Desk",
95
95
  sidebar: PlanSidebar,
96
96
  theme: {
@@ -11,7 +11,7 @@
11
11
  * HTTP handling, its partial-failure policy and its `FatalError` guards are
12
12
  * all reachable — `installStubStepFetch` answers the network and
13
13
  * `stubGateway` answers the model.
14
- * - The BODY, twice. `createWorkflowCtx` records what it asked for (the digest
14
+ * - The BODY, twice. `createWorkflowContext` records what it asked for (the digest
15
15
  * loop, the shrinking pending set, the sleep BETWEEN digests and never after
16
16
  * the last), and `runWorkflow` runs it on the real replay engine — which is
17
17
  * what this file used to say a unit test could not do, on the ground that a
@@ -26,8 +26,9 @@
26
26
 
27
27
  import { type FeedItem, parseFeed } from "@alexkroman1/aai/html";
28
28
  import {
29
- createWorkflowCtx,
29
+ createWorkflowContext,
30
30
  parseSchemaInput,
31
+ routeStepFetch,
31
32
  schemaInputIssues,
32
33
  stubGatewayRoute,
33
34
  } from "@alexkroman1/aai/testing";
@@ -103,15 +104,25 @@ const slackInput = (slackWebhookUrl: string) => ({
103
104
 
104
105
  describe("the declaration", () => {
105
106
  test("is a workflow app with one workflow and a static page", () => {
106
- expect(agentDef.name).toBe("Podcast Digest");
107
- expect(Object.keys(agentDef.workflows ?? {})).toEqual(["dailyDigest"]);
107
+ // Not the agent's name: renaming it is an invited edit, and pinning the
108
+ // literal here would fail a build in somebody else's project.
109
+ expect(agentDef.name).toBeTruthy();
110
+ // `toContain` rather than an exact key list: adding a second workflow is an
111
+ // invited edit and must not redden a test the author did not write. The
112
+ // NAME is still pinned, deliberately — the page starts a run by this
113
+ // string, so renaming the key is a runtime 400 rather than a compile
114
+ // error, and this pin is the only thing that says so. Rename it here and
115
+ // in `client.tsx` together.
116
+ expect(Object.keys(agentDef.workflows ?? {})).toContain("dailyDigest");
108
117
  expect(agentDef.workflows?.dailyDigest).toBe(dailyDigest);
109
118
  });
110
119
 
111
120
  test("names the one credential its steps read", () => {
112
121
  // A workflow app declares no providers, so nothing else in the config can
113
122
  // name this — which is what makes a deploy able to check for it.
114
- expect(agentDef.requiredEnv).toEqual(["ASSEMBLYAI_API_KEY"]);
123
+ // `toContain`, so a step of your own that reads a second credential can be
124
+ // declared beside this one without failing a test you did not write.
125
+ expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
115
126
  });
116
127
  });
117
128
 
@@ -412,12 +423,20 @@ const THREE_EPISODES = feedXml(
412
423
  * the thing that forgets, and a step fetch left published reaches the next file.
413
424
  */
414
425
  function stubRoutes(routes: Record<string, { status?: number; body?: unknown }>) {
415
- return installStubStepFetch((request) => {
416
- for (const [fragment, answer] of Object.entries(routes)) {
417
- if (request.url.includes(fragment)) return answer;
418
- }
419
- return { status: 404, body: `no route for ${request.url}` };
420
- });
426
+ return installStubStepFetch(
427
+ routeStepFetch(
428
+ Object.entries(routes).map(
429
+ ([fragment, answer]) =>
430
+ (request) =>
431
+ request.url.includes(fragment) ? answer : undefined,
432
+ ),
433
+ // A 404 rather than the default throw, and this one is deliberate: several
434
+ // cases below are ABOUT a feed the run cannot reach, and they express that
435
+ // by leaving it out of `routes`. Where an unrecognised request would be a
436
+ // finding instead, the default is what says so at the call.
437
+ { unmatched: "notFound" },
438
+ ),
439
+ );
421
440
  }
422
441
 
423
442
  describe("discoverEpisodes", () => {
@@ -827,7 +846,7 @@ describe("posting the digest", () => {
827
846
  });
828
847
 
829
848
  /**
830
- * The 4xx/5xx split is the reason this is not a one-line `stepFetchOk`: a
849
+ * The 4xx/5xx split is the reason this is not a one-line `stepFetchOrFail`: a
831
850
  * revoked webhook answers 4xx identically on every retry, so retrying it
832
851
  * burns the DevKit's attempts and delays the real error by minutes.
833
852
  */
@@ -866,7 +885,7 @@ describe("the body — the run that IS the schedule", () => {
866
885
  * per-step spec can see, and all of which is the template's actual subject.
867
886
  */
868
887
  function driveTwoDigests(pollResults: unknown) {
869
- const ctx = createWorkflowCtx({
888
+ const ctx = createWorkflowContext({
870
889
  runSteps: false,
871
890
  results: {
872
891
  discoverEpisodes: [EPISODE],
@@ -963,7 +982,7 @@ describe("the body — the run that IS the schedule", () => {
963
982
  /**
964
983
  * The schedule, on the real replay engine.
965
984
  *
966
- * The block above drives the same body through `createWorkflowCtx`, which
985
+ * The block above drives the same body through `createWorkflowContext`, which
967
986
  * RECORDS a sleep rather than taking one and replays nothing — right for the
968
987
  * loop's logic, and silent about the property this template is: a run that
969
988
  * sleeps for a day between digests and comes back. `runWorkflow`
@@ -3,7 +3,7 @@
3
3
  * The browser half — a form that starts a run, and a panel that watches one
4
4
  * that may outlive the browser by a week.
5
5
  *
6
- * Mounted with `page()` rather than `client()`: there is no session to build, so
6
+ * Mounted with `mountPage()` rather than `mountClient()`: there is no session to build, so
7
7
  * no socket, no audio graph, no microphone request. `useWorkflowSubmit()` starts
8
8
  * the run, follows its STATUS, and hands back the controls bound to it.
9
9
  *
@@ -17,12 +17,13 @@
17
17
  * minutes, this one loses the only Stop button a thirty-digest commitment has,
18
18
  * and the run is invisible from then on to everything but `curl`.
19
19
  *
20
- * `key` is the handle that survives and `recover: true` is what reads it back,
21
- * so a later load lands on the same count, the same newest line, and the same
22
- * Wake and Cancel buttons bound to the same run.
20
+ * A correlation KEY is the handle that survives, and `useWorkflowSubmit` looks
21
+ * that key's newest run up as it mounts, so a later load lands on the same
22
+ * count, the same newest line, and the same Wake and Cancel buttons bound to
23
+ * the same run.
23
24
  *
24
- * **The key is `useRunKey({ storage: "local" })`, and this is the one workflow
25
- * app that passes that option.** Its siblings take the default,
25
+ * **This is the one workflow app that passes a `key` of its own, and it is
26
+ * `useRunKey({ storage: "local" })`.** Its siblings let the hook mint one into
26
27
  * `sessionStorage`, which dies with the tab and covers exactly the interruption
27
28
  * they have — a reload, a same-tab navigation, a crashed tab. A schedule
28
29
  * outlives all of that by design: closing the browser on Tuesday and coming
@@ -42,9 +43,9 @@
42
43
  * URL would carry a credential into a lookup token — which is why the
43
44
  * platform stopped writing keys to the operator's log.
44
45
  *
45
- * A real app with accounts passes the account's own id instead, and then the
46
- * schedule follows the person to a new device — a promise only a login can
47
- * keep.
46
+ * A real app with accounts passes the account's own id as `key` instead, and
47
+ * then the schedule follows the person to a new device — a promise only a login
48
+ * can keep.
48
49
  *
49
50
  * ## The form is DECLARED, not written
50
51
  *
@@ -85,8 +86,9 @@
85
86
  */
86
87
 
87
88
  import {
89
+ BulletList,
88
90
  Form,
89
- page,
91
+ mountPage,
90
92
  SubmitButton,
91
93
  useRunKey,
92
94
  useWorkflowSubmit,
@@ -97,7 +99,6 @@ import "@alexkroman1/aai-ui/styles.css";
97
99
  // ERASED at build time, so naming the agent's own type costs the browser bundle
98
100
  // nothing — and it is what stops this file restating a shape `workflows/
99
101
  // digest.ts` already declares.
100
- import { useState } from "react";
101
102
  import type { dailyDigest } from "./agent.ts";
102
103
 
103
104
  /** The workflow this page drives. Matches the key in `workflowApp({ workflows })`. */
@@ -130,21 +131,15 @@ export function App() {
130
131
  // Did THIS load start the schedule? A later load cannot have, and that is the
131
132
  // only way the page can tell "scheduled just now" from "still running from
132
133
  // Tuesday" — the hook reports the run, not who asked for it.
133
- const [startedHere, setStartedHere] = useState(false);
134
134
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
135
135
  // `run.output` instead of `unknown`. `error` is the agent's own sentence for a
136
136
  // rejected input — better copy than anything this page could write, and the
137
137
  // reason there is no `try`/`catch` here.
138
- const { submitForm, run, pending, error, wake, cancel } = useWorkflowSubmit<typeof dailyDigest>(
139
- WORKFLOW,
140
- {
141
- // Recorded with the run, and read back on the next load. Neither half is
142
- // useful alone: without the key there is nothing to find the run by, and
143
- // without `recover` the key is only ever written.
144
- key,
145
- recover: true,
146
- },
147
- );
138
+ // The key REPLACES the tab-scoped one the hook would mint; the lookup that
139
+ // reads it back on the next load happens either way.
140
+ const { submitForm, run, pending, error, wake, cancel, startedHere } = useWorkflowSubmit<
141
+ typeof dailyDigest
142
+ >(WORKFLOW, { key });
148
143
 
149
144
  return (
150
145
  <main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
@@ -152,13 +147,7 @@ export function App() {
152
147
 
153
148
  {/* `submit()` resolves as soon as the run EXISTS — deliberately not when it
154
149
  finishes, which here could be a month away. */}
155
- <Form
156
- onSubmit={(values) => {
157
- setStartedHere(true);
158
- return submitForm(values);
159
- }}
160
- error={error}
161
- >
150
+ <Form onSubmit={(values) => submitForm(values)} error={error}>
162
151
  <WorkflowFields workflow={WORKFLOW} />
163
152
  <SubmitButton pending={pending}>
164
153
  {pending ? "Digest scheduled" : "Start digest"}
@@ -198,7 +187,13 @@ export function App() {
198
187
  </div>
199
188
  )}
200
189
 
201
- {run?.status === "failed" && <p className="text-red-600">That run failed: {run.error}</p>}
190
+ {/* `role="alert"`, the same contract `<Form>` gives the submit error
191
+ above: a digest that fails does so days later, with nobody watching. */}
192
+ {run?.status === "failed" && (
193
+ <p role="alert" className="text-red-600">
194
+ That run failed: {run.error}
195
+ </p>
196
+ )}
202
197
  {run?.status === "cancelled" && <p>Cancelled — no further digests will be posted.</p>}
203
198
 
204
199
  {run?.status === "completed" && (
@@ -216,11 +211,7 @@ export function App() {
216
211
  </a>
217
212
  </h2>
218
213
  <p>{episode.summary}</p>
219
- <ul className="flex list-disc flex-col gap-1 pl-5 text-sm">
220
- {episode.keyPoints.map((point) => (
221
- <li key={point}>{point}</li>
222
- ))}
223
- </ul>
214
+ <BulletList items={episode.keyPoints} size="sm" />
224
215
  </section>
225
216
  ))}
226
217
  </article>
@@ -229,4 +220,4 @@ export function App() {
229
220
  );
230
221
  }
231
222
 
232
- page({ name: "Podcast Digest", component: App });
223
+ mountPage({ name: "Podcast Digest", component: App });
@@ -49,13 +49,13 @@
49
49
  * @module digest
50
50
  */
51
51
 
52
- import type { WorkflowCtx } from "@alexkroman1/aai";
53
- import { mapConcurrent, report, TRANSCRIBE_API } from "@alexkroman1/aai/step";
52
+ import type { WorkflowContext } from "@alexkroman1/aai";
53
+ import { mapConcurrent, stepReport, TRANSCRIBE_API } from "@alexkroman1/aai/step";
54
54
  import {
55
55
  FatalError,
56
- stepGenerateJsonClassified,
57
- stepTranscribePollClassified,
58
- stepTranscribeSubmitClassified,
56
+ stepGenerateJsonOrFail,
57
+ stepTranscribePollOrFail,
58
+ stepTranscribeSubmitOrFail,
59
59
  } from "@alexkroman1/aai/step-errors";
60
60
  import { errorMessage } from "@alexkroman1/aai/utils";
61
61
  import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
@@ -160,7 +160,7 @@ export type DailyDigestOutput = {
160
160
  } | null;
161
161
  };
162
162
 
163
- /** What the model must answer with, and what `stepGenerateJsonClassified` enforces. */
163
+ /** What the model must answer with, and what `stepGenerateJsonOrFail` enforces. */
164
164
  const SummaryReply = z.object({
165
165
  summary: z.string().trim().min(1),
166
166
  keyPoints: z.array(z.string().trim().min(1)).min(1).max(5),
@@ -178,7 +178,7 @@ const SummaryReply = z.object({
178
178
  */
179
179
  export async function dailyDigestFlow(
180
180
  input: DigestInput,
181
- ctx: WorkflowCtx,
181
+ ctx: WorkflowContext,
182
182
  ): Promise<DailyDigestOutput> {
183
183
  // No `??` fallbacks: {@link DigestInput} is the schema's OUTPUT, so every
184
184
  // `.default()` has already run by the time a run reaches this line. The
@@ -269,7 +269,7 @@ export async function dailyDigestFlow(
269
269
  */
270
270
  async function waitForTranscripts(
271
271
  jobs: TranscriptJob[],
272
- ctx: WorkflowCtx,
272
+ ctx: WorkflowContext,
273
273
  ): Promise<TranscriptState[]> {
274
274
  let pending = jobs;
275
275
  // Keyed by episode id rather than appended, and that is what keeps the digest
@@ -287,17 +287,19 @@ async function waitForTranscripts(
287
287
  if (pending.length > 0) await ctx.sleep("poll", POLL_DELAY_MS);
288
288
  }
289
289
 
290
- for (const job of pending) settled.set(job.id, gaveUpOn(job));
291
290
  // The list `jobs` arrived in, which `discoverEpisodes` sorted newest first.
291
+ // A job still pending is absent from `settled`, so the `??` IS the
292
+ // give-up path — `pollTranscript` returns the job unchanged while it is
293
+ // submitted, so the value given up on is the one that went in.
292
294
  return jobs.map((job) => settled.get(job.id) ?? gaveUpOn(job));
293
295
  }
294
296
 
295
297
  /**
296
298
  * One episode the run is done waiting for.
297
299
  *
298
- * Its own function because {@link waitForTranscripts} needs it in two places —
299
- * the jobs still pending when the budget ran out, and the unreachable fallback
300
- * the ordered rebuild above needs for a lookup that cannot miss.
300
+ * Its own function because {@link waitForTranscripts} reaches it from the
301
+ * ordered rebuild, for every job whose budget ran out while it was still
302
+ * submitted and which is therefore absent from `settled`.
301
303
  */
302
304
  function gaveUpOn(job: TranscriptJob): TranscriptState {
303
305
  return {
@@ -322,7 +324,7 @@ function gaveUpOn(job: TranscriptJob): TranscriptState {
322
324
  * an `unavailable` VALUE rather than a throw, because one bad episode must not
323
325
  * sink a digest of five.
324
326
  *
325
- * The verdict itself is the SDK's: `stepTranscribeSubmitClassified` reads
327
+ * The verdict itself is the SDK's: `stepTranscribeSubmitOrFail` reads
326
328
  * `TranscribeError`'s own `retryable` AND its `retryAfter`, and throws a
327
329
  * `FatalError` or a `RetryableError` accordingly. The hand-written
328
330
  * `err instanceof TranscribeError && err.retryable` this replaces read only the
@@ -330,9 +332,9 @@ function gaveUpOn(job: TranscriptJob): TranscriptState {
330
332
  * one-second default instead.
331
333
  */
332
334
  export async function submitTranscript(episode: Episode): Promise<TranscriptJob> {
333
- await report(`Submitting ${episode.title} for transcription.`);
335
+ await stepReport(`Submitting ${episode.title} for transcription.`);
334
336
  try {
335
- const { id } = await stepTranscribeSubmitClassified(episode.audioUrl, {
337
+ const { id } = await stepTranscribeSubmitOrFail(episode.audioUrl, {
336
338
  // A digest quotes nobody, so who spoke costs time for nothing.
337
339
  params: { speaker_labels: false },
338
340
  });
@@ -356,12 +358,12 @@ export async function pollTranscript(job: TranscriptJob): Promise<TranscriptStat
356
358
  if (job.transcriptStatus === "unavailable") return job;
357
359
 
358
360
  try {
359
- const progress = await stepTranscribePollClassified(job.transcriptId);
361
+ const progress = await stepTranscribePollOrFail(job.transcriptId);
360
362
  // Branch on `done`, never on a status string: a vocabulary this body does
361
363
  // not own would otherwise read as "not finished yet" forever.
362
364
  if (!progress.done) return job;
363
365
 
364
- await report(`Transcribed ${job.title}.`);
366
+ await stepReport(`Transcribed ${job.title}.`);
365
367
  return {
366
368
  ...job,
367
369
  transcriptStatus: "done",
@@ -391,8 +393,8 @@ export async function summarizeTranscript(state: TranscriptState): Promise<Episo
391
393
  };
392
394
  }
393
395
 
394
- await report(`Summarizing ${state.title}.`);
395
- const parsed = await stepGenerateJsonClassified(
396
+ await stepReport(`Summarizing ${state.title}.`);
397
+ const parsed = await stepGenerateJsonOrFail(
396
398
  [
397
399
  `Podcast: ${state.podcastTitle}`,
398
400
  `Episode: ${state.title}`,