@alexkroman1/aai-cli 13.3.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 (134) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DolUCMxu.mjs} +8 -7
  4. package/dist/_cli-common.d.ts +45 -6
  5. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  6. package/dist/_dev-env.d.ts +1 -1
  7. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-CSMqF8PN.mjs} +20 -22
  8. package/dist/_dev-vite-config.d.ts +3 -3
  9. package/dist/{_init-Ci4QGjp8.mjs → _init-CQ8idAwo.mjs} +9 -23
  10. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  11. package/dist/_output.d.ts +10 -9
  12. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  13. package/dist/_resource-commands.d.ts +9 -9
  14. package/dist/{_server-common-BV4_CrP3.mjs → _server-common-vILJp3it.mjs} +1 -1
  15. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  16. package/dist/_slug-api.d.ts +19 -0
  17. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  18. package/dist/_studio-commands.d.ts +13 -13
  19. package/dist/{_templates-V4l5sZJy.mjs → _templates-CK4oKoeX.mjs} +3 -3
  20. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  21. package/dist/_typecheck-gate.d.ts +3 -1
  22. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  23. package/dist/_utils.d.ts +35 -0
  24. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  25. package/dist/_workflow-determinism.d.ts +3 -3
  26. package/dist/{build-CWyFGe-W.mjs → build-Mxk8gWvX.mjs} +46 -7
  27. package/dist/build.d.ts +5 -0
  28. package/dist/cli.d.ts +1 -2
  29. package/dist/cli.mjs +117 -135
  30. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-BJgREAh6.mjs} +1 -1
  31. package/dist/client-bundler.mjs +1 -1
  32. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  33. package/dist/{deploy-B7f3CK7v.mjs → deploy-uAJ4NukN.mjs} +11 -11
  34. package/dist/{dev-DamXieW_.mjs → dev-DApPSaE_.mjs} +3 -3
  35. package/dist/{eval-h3DTVfRw.mjs → eval-BK47A_K5.mjs} +3 -3
  36. package/dist/{init-D-vFrV8h.mjs → init-DukDxECd.mjs} +11 -19
  37. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  38. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  39. package/dist/project-config.mjs +1 -1
  40. package/dist/scaffold/CLAUDE.md +84 -56
  41. package/dist/scaffold/package.json +4 -4
  42. package/dist/scaffold/server.mjs +129 -25
  43. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  44. package/dist/secret.d.ts +8 -6
  45. package/dist/{studio-BpMCfUNx.mjs → studio-CpHlNHUZ.mjs} +13 -15
  46. package/dist/templates/call-audit/agent.test.ts +11 -6
  47. package/dist/templates/call-audit/agent.ts +2 -2
  48. package/dist/templates/call-audit/client.tsx +27 -42
  49. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  50. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  51. package/dist/templates/call-audit/workflows/media.ts +12 -37
  52. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  53. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  54. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  55. package/dist/templates/dispatch-center/client.tsx +53 -61
  56. package/dist/templates/dispatch-center/shared.ts +0 -8
  57. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  58. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  59. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  60. package/dist/templates/infocom-adventure/client.tsx +40 -36
  61. package/dist/templates/link-digest/agent.test.ts +3 -3
  62. package/dist/templates/link-digest/agent.ts +2 -2
  63. package/dist/templates/link-digest/client.tsx +27 -17
  64. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  65. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  66. package/dist/templates/night-owl/client.tsx +8 -2
  67. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  68. package/dist/templates/pizza-ordering/client.tsx +5 -4
  69. package/dist/templates/pizza-ordering/shared.ts +3 -8
  70. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  71. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  72. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  73. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  74. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  75. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  76. package/dist/templates/plan-and-execute/client.tsx +2 -2
  77. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  78. package/dist/templates/podcast-digest/client.tsx +16 -22
  79. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  80. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  81. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  82. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  83. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  84. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  85. package/dist/templates/redline/agent.eval.test.ts +5 -8
  86. package/dist/templates/redline/agent.test.ts +1 -1
  87. package/dist/templates/redline/client.tsx +36 -34
  88. package/dist/templates/redline/workflows/redline.ts +17 -17
  89. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  90. package/dist/templates/research-workflow/agent.test.ts +10 -7
  91. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  92. package/dist/templates/retail/agent.test.ts +17 -15
  93. package/dist/templates/retail/cancel.ts +7 -6
  94. package/dist/templates/retail/client.tsx +39 -58
  95. package/dist/templates/retail/payment.ts +4 -3
  96. package/dist/templates/retail/pending.test.ts +19 -19
  97. package/dist/templates/retail/store.test.ts +5 -5
  98. package/dist/templates/retail/swap.ts +8 -11
  99. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  100. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  101. package/dist/templates/solo-rpg/client.tsx +3 -3
  102. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  103. package/dist/templates/spoken-summary/agent.ts +3 -3
  104. package/dist/templates/spoken-summary/client.tsx +23 -30
  105. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  106. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  107. package/dist/templates/support-line/client.tsx +4 -3
  108. package/dist/templates/support-line/prompts.ts +0 -2
  109. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  110. package/dist/templates/transcription-workflow/agent.ts +2 -2
  111. package/dist/templates/transcription-workflow/client.tsx +15 -14
  112. package/dist/templates/transcription-workflow/recover.ts +1 -1
  113. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  114. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  115. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  116. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  117. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  118. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  119. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  120. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  121. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  122. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  123. package/dist/templates/travel-concierge/client.tsx +2 -2
  124. package/dist/templates/travel-concierge/shared.ts +10 -14
  125. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  126. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  127. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  128. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  129. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  130. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  131. package/dist/typecheck.mjs +1 -1
  132. package/dist/worker-bundler.mjs +1 -1
  133. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  134. package/package.json +4 -4
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
7
+ import { formatMoney } from "@alexkroman1/aai/utils";
7
8
  import { resolveOrder } from "./resolve.ts";
8
9
  import type { RetailState } from "./shared.ts";
9
10
  import { authenticatedUser, findPaymentMethod, isGiftCard, money } from "./store.ts";
@@ -53,13 +54,13 @@ export function planPayment(
53
54
  const amount = original.amount;
54
55
  if (isGiftCard(newMethod) && newMethod.balance < amount) {
55
56
  return {
56
- error: `Gift card ${newMethodId}'s balance ($${newMethod.balance.toFixed(2)}) does not cover the $${amount.toFixed(2)} order total.`,
57
+ error: `Gift card ${newMethodId}'s balance (${formatMoney(newMethod.balance)}) does not cover the ${formatMoney(amount)} order total.`,
57
58
  };
58
59
  }
59
60
 
60
61
  return {
61
62
  readBack:
62
- `charge order ${order.order_id} — $${amount.toFixed(2)} — to ${newMethodId} instead, ` +
63
+ `charge order ${order.order_id} — ${formatMoney(amount)} — to ${newMethodId} instead, ` +
63
64
  `refunding ${original.payment_method_id}`,
64
65
  orderId: order.order_id,
65
66
  newMethodId,
@@ -102,6 +103,6 @@ export function applyPayment(state: RetailState, plan: PaymentPlan) {
102
103
  amount: plan.amount,
103
104
  paid_with: plan.newMethodId,
104
105
  refunded_to: plan.oldMethodId,
105
- message: `Order ${plan.orderId} is now charged to ${plan.newMethodId}, and $${plan.amount.toFixed(2)} is going back to ${plan.oldMethodId}.`,
106
+ message: `Order ${plan.orderId} is now charged to ${plan.newMethodId}, and ${formatMoney(plan.amount)} is going back to ${plan.oldMethodId}.`,
106
107
  };
107
108
  }
@@ -1,6 +1,6 @@
1
1
  import type { ToolContext } from "@alexkroman1/aai";
2
2
  import { isToolFailure } from "@alexkroman1/aai";
3
- import { createToolContext, ok, okPosition } from "@alexkroman1/aai/testing";
3
+ import { createToolContext, expectDialogOk, expectToolOk } from "@alexkroman1/aai/testing";
4
4
  import { describe, expect, test } from "vitest";
5
5
  import type { StagedResult } from "./pending.ts";
6
6
  import { retailSlot } from "./store.ts";
@@ -17,7 +17,7 @@ import transferToHumanAgents from "./tools/transfer_to_human_agents.ts";
17
17
  * one delivered order (#W4316152), which is every shape these cases need. */
18
18
  async function aaravCtx(): Promise<ToolContext> {
19
19
  const ctx = createToolContext();
20
- ok(await findUserIdByEmail.execute({ email: "aarav.anderson9752@example.com" }, ctx));
20
+ expectToolOk(await findUserIdByEmail.execute({ email: "aarav.anderson9752@example.com" }, ctx));
21
21
  return ctx;
22
22
  }
23
23
 
@@ -29,7 +29,7 @@ describe("staging", () => {
29
29
  const ctx = await aaravCtx();
30
30
  const before = structuredClone(retailSlot.get(ctx).store.orders["#W9300146"]);
31
31
 
32
- const staged = okPosition<StagedResult>(await stageCancel(ctx));
32
+ const staged = expectDialogOk<StagedResult>(await stageCancel(ctx));
33
33
 
34
34
  expect(staged.state).toBe("serving.awaitingConfirmation");
35
35
  expect(staged.result.staged).toBe("cancel_pending_order");
@@ -43,7 +43,7 @@ describe("staging", () => {
43
43
 
44
44
  test("the readback names the order, the items, the amount and where the money goes", async () => {
45
45
  const ctx = await aaravCtx();
46
- const staged = ok<StagedResult>(await stageCancel(ctx));
46
+ const staged = expectToolOk<StagedResult>(await stageCancel(ctx));
47
47
  expect(staged.read_back).toContain("#W9300146");
48
48
  expect(staged.read_back).toContain("153.23");
49
49
  expect(staged.read_back).toContain("gift_card_7245904");
@@ -52,14 +52,14 @@ describe("staging", () => {
52
52
 
53
53
  test("the result says out loud that nothing has happened", async () => {
54
54
  const ctx = await aaravCtx();
55
- const staged = ok<StagedResult>(await stageCancel(ctx));
55
+ const staged = expectToolOk<StagedResult>(await stageCancel(ctx));
56
56
  expect(staged.message).toContain("NOTHING HAS CHANGED YET");
57
57
  expect(staged.message).toContain("confirm_change");
58
58
  });
59
59
 
60
60
  test("a second stage is refused, and the refusal names the one already waiting", async () => {
61
61
  const ctx = await aaravCtx();
62
- ok(await stageCancel(ctx));
62
+ expectToolOk(await stageCancel(ctx));
63
63
 
64
64
  // A DIFFERENT change, so this is not a repeat of the first call.
65
65
  const second = await modifyUserAddress.execute(
@@ -89,9 +89,9 @@ describe("staging", () => {
89
89
  describe("confirming", () => {
90
90
  test("confirm_change applies the staged change and returns to helping", async () => {
91
91
  const ctx = await aaravCtx();
92
- ok(await stageCancel(ctx));
92
+ expectToolOk(await stageCancel(ctx));
93
93
 
94
- const done = okPosition<{ confirmed: string; status: string }>(
94
+ const done = expectDialogOk<{ confirmed: string; status: string }>(
95
95
  await confirmChange.execute({}, ctx),
96
96
  );
97
97
  expect(done.state).toBe("serving.helping");
@@ -114,8 +114,8 @@ describe("confirming", () => {
114
114
 
115
115
  test("confirming twice is refused — the second call has nothing staged", async () => {
116
116
  const ctx = await aaravCtx();
117
- ok(await stageCancel(ctx));
118
- ok(await confirmChange.execute({}, ctx));
117
+ expectToolOk(await stageCancel(ctx));
118
+ expectToolOk(await confirmChange.execute({}, ctx));
119
119
 
120
120
  const again = await confirmChange.execute({}, ctx);
121
121
  expect(isToolFailure(again)).toBe(true);
@@ -130,9 +130,9 @@ describe("confirming", () => {
130
130
  describe("cancelling a staged change", () => {
131
131
  test("cancel_change drops it, changes nothing, and frees the call", async () => {
132
132
  const ctx = await aaravCtx();
133
- ok(await stageCancel(ctx));
133
+ expectToolOk(await stageCancel(ctx));
134
134
 
135
- const dropped = okPosition<{ dropped: string | null; message: string }>(
135
+ const dropped = expectDialogOk<{ dropped: string | null; message: string }>(
136
136
  await cancelChange.execute({}, ctx),
137
137
  );
138
138
  expect(dropped.state).toBe("serving.helping");
@@ -144,12 +144,12 @@ describe("cancelling a staged change", () => {
144
144
 
145
145
  test("a corrected change can be staged straight after", async () => {
146
146
  const ctx = await aaravCtx();
147
- ok(await stageCancel(ctx));
148
- ok(await cancelChange.execute({}, ctx));
147
+ expectToolOk(await stageCancel(ctx));
148
+ expectToolOk(await cancelChange.execute({}, ctx));
149
149
 
150
150
  // The caller changed their mind about which order — the very thing
151
151
  // `cancel_change` exists for.
152
- const restaged = ok<StagedResult>(
152
+ const restaged = expectToolOk<StagedResult>(
153
153
  await returnDeliveredOrderItems.execute(
154
154
  {
155
155
  order_id: "#W4316152",
@@ -166,9 +166,9 @@ describe("cancelling a staged change", () => {
166
166
  describe("what stays legal while a change waits", () => {
167
167
  test("a read is still answerable — 'what was the total again?'", async () => {
168
168
  const ctx = await aaravCtx();
169
- ok(await stageCancel(ctx));
169
+ expectToolOk(await stageCancel(ctx));
170
170
  // `when: "serving"` matches both children, which is what makes this work.
171
- const read = okPosition<{ order_id: string }>(
171
+ const read = expectDialogOk<{ order_id: string }>(
172
172
  await getOrderDetails.execute({ order_id: "#W9300146" }, ctx),
173
173
  );
174
174
  expect(read.result.order_id).toBe("#W9300146");
@@ -178,9 +178,9 @@ describe("what stays legal while a change waits", () => {
178
178
 
179
179
  test("a caller can still ask for a human, and everything refuses afterwards", async () => {
180
180
  const ctx = await aaravCtx();
181
- ok(await stageCancel(ctx));
181
+ expectToolOk(await stageCancel(ctx));
182
182
 
183
- const transferred = okPosition(
183
+ const transferred = expectDialogOk(
184
184
  await transferToHumanAgents.execute({ summary: "wants a human" }, ctx),
185
185
  );
186
186
  // `TRANSFERRED` is declared on the `serving` PARENT, which is what lets it
@@ -1,5 +1,5 @@
1
1
  import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
2
- import { createToolContext, ok, okPosition } from "@alexkroman1/aai/testing";
2
+ import { createToolContext, expectDialogOk, expectToolOk } from "@alexkroman1/aai/testing";
3
3
  import { describe, expect, test } from "vitest";
4
4
  import { z } from "zod";
5
5
  import {
@@ -237,16 +237,16 @@ describe("retailTool", () => {
237
237
  test("runs once the flow says serving", async () => {
238
238
  const ctx = makeCtx();
239
239
  serve(ctx);
240
- expect(ok(await gated.execute({}, ctx))).toEqual({ ok: true });
240
+ expect(expectToolOk(await gated.execute({}, ctx))).toEqual({ ok: true });
241
241
  });
242
242
 
243
243
  test("the result carries the position the call landed in", async () => {
244
244
  const ctx = makeCtx();
245
245
  serve(ctx);
246
- // `okPosition` rather than a cast to `{ instruction?: string }`: it keeps
246
+ // `expectDialogOk` rather than a cast to `{ instruction?: string }`: it keeps
247
247
  // the envelope this test is about, and a refusal fails here naming what the
248
248
  // flow said instead of reading `undefined` off a field nobody assigned.
249
- const answered = okPosition<{ ok: boolean }>(await gated.execute({}, ctx));
249
+ const answered = expectDialogOk<{ ok: boolean }>(await gated.execute({}, ctx));
250
250
  expect(answered).toMatchObject({ state: "serving.helping", done: false });
251
251
  expect(answered.instruction).toMatch(/one identified customer/);
252
252
  });
@@ -307,7 +307,7 @@ describe("the call flow", () => {
307
307
  summary: () => "ran",
308
308
  execute: () => ({ ok: true }),
309
309
  });
310
- expect(ok(await anywhere.execute({}, ctx))).toEqual({ ok: true });
310
+ expect(expectToolOk(await anywhere.execute({}, ctx))).toEqual({ ok: true });
311
311
 
312
312
  const at = callFlow.send(ctx, { type: "TRANSFERRED" });
313
313
  expect(at.state).toBe("transferred");
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
10
+ import { formatMoney } from "@alexkroman1/aai/utils";
10
11
  import { resolveOrder } from "./resolve.ts";
11
12
  import type { Order, OrderItem, RetailState, User, Variant } from "./shared.ts";
12
13
  import {
@@ -48,7 +49,6 @@ export interface SwapLine {
48
49
  fromOptions: Record<string, string>;
49
50
  fromPrice: number;
50
51
  toItemId: string;
51
- toName: string;
52
52
  toOptions: Record<string, string>;
53
53
  toPrice: number;
54
54
  }
@@ -146,9 +146,6 @@ export function toSwapLines(plan: SwapPlan): SwapLine[] {
146
146
  fromOptions: { ...pair.item.options },
147
147
  fromPrice: pair.item.price,
148
148
  toItemId: pair.newVariant.item_id,
149
- // Same product by construction — a swap can only reach another option of
150
- // it — so the name is the item's own and the OPTIONS are what differ.
151
- toName: pair.item.name,
152
149
  toOptions: { ...pair.newVariant.options },
153
150
  toPrice: pair.newVariant.price,
154
151
  }));
@@ -163,7 +160,7 @@ export function assertCanCoverDiff(user: User, methodId: string, diff: number):
163
160
  if (isToolFailure(method)) return method;
164
161
  if (isGiftCard(method) && method.balance < diff) {
165
162
  return {
166
- error: `Gift card ${methodId}'s balance ($${method.balance.toFixed(2)}) does not cover the $${diff.toFixed(2)} difference. Ask for another payment method.`,
163
+ error: `Gift card ${methodId}'s balance (${formatMoney(method.balance)}) does not cover the ${formatMoney(diff)} difference. Ask for another payment method.`,
167
164
  };
168
165
  }
169
166
  return null;
@@ -190,8 +187,8 @@ function describeLine(line: SwapLine): string {
190
187
  }
191
188
 
192
189
  function describeDiff(diff: number, methodId: string): string {
193
- if (diff > 0) return `$${diff.toFixed(2)} charged to ${methodId}`;
194
- if (diff < 0) return `$${Math.abs(diff).toFixed(2)} refunded to ${methodId}`;
190
+ if (diff > 0) return `${formatMoney(diff)} charged to ${methodId}`;
191
+ if (diff < 0) return `${formatMoney(Math.abs(diff))} refunded to ${methodId}`;
195
192
  return "no price difference";
196
193
  }
197
194
 
@@ -275,8 +272,8 @@ export function applyModifyItems(state: RetailState, plan: ModifyItemsPlan) {
275
272
  })),
276
273
  message:
277
274
  plan.diff > 0
278
- ? `Done. $${plan.diff.toFixed(2)} was charged to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`
279
- : `Done. $${Math.abs(plan.diff).toFixed(2)} is being refunded to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`,
275
+ ? `Done. ${formatMoney(plan.diff)} was charged to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`
276
+ : `Done. ${formatMoney(Math.abs(plan.diff))} is being refunded to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`,
280
277
  };
281
278
  }
282
279
 
@@ -358,7 +355,7 @@ export function applyExchange(state: RetailState, plan: ExchangePlan) {
358
355
  })),
359
356
  message:
360
357
  plan.diff > 0
361
- ? `Exchange requested on ${plan.orderId}. $${plan.diff.toFixed(2)} will be charged to ${plan.paymentMethodId}. An email with return instructions is on its way.`
362
- : `Exchange requested on ${plan.orderId}. $${Math.abs(plan.diff).toFixed(2)} will be refunded to ${plan.paymentMethodId}. An email with return instructions is on its way.`,
358
+ ? `Exchange requested on ${plan.orderId}. ${formatMoney(plan.diff)} will be charged to ${plan.paymentMethodId}. An email with return instructions is on its way.`
359
+ : `Exchange requested on ${plan.orderId}. ${formatMoney(Math.abs(plan.diff))} will be refunded to ${plan.paymentMethodId}. An email with return instructions is on its way.`,
363
360
  };
364
361
  }
@@ -33,7 +33,7 @@
33
33
  * deployed.
34
34
  */
35
35
  import agentDef from "virtual:aai/agent";
36
- import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
36
+ import { type EvalTurn, toolResultIn, toolResultsIn } from "@alexkroman1/aai-runtime/eval";
37
37
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
38
38
  import { expect } from "vitest";
39
39
  import { z } from "zod";
@@ -82,20 +82,6 @@ const Refusal = z.object({ error: z.string() });
82
82
  const answerOf = <T>(turn: EvalTurn, name: string, schema: z.ZodType<T>): T =>
83
83
  toolResultIn(turn.toolCalls, name, schema);
84
84
 
85
- /**
86
- * What EVERY call to `name` answered on this turn, in call order.
87
- *
88
- * The plural form is for a claim about calls that may legitimately not have
89
- * happened — a refusal the narrator may have pre-empted by not calling at all.
90
- * `toolResultIn` over a ONE-CALL list per call: the name is that call's own, so
91
- * the reader's "no such call" and "two calls" throws are unreachable and what
92
- * is left is the parse, the schema, and the "never completed" failure.
93
- */
94
- const answersOf = <T>(turn: EvalTurn, name: string, schema: z.ZodType<T>): T[] =>
95
- turn.toolCalls
96
- .filter((c) => c.name === name)
97
- .map((call) => toolResultIn([call], call.name, schema));
98
-
99
85
  /**
100
86
  * Every field `setup_character` requires, for the SCRIPTED runs.
101
87
  *
@@ -267,7 +253,7 @@ describeEval(agentDef, (test) => {
267
253
  expect(answerOf(down, "update_state", Settled).state).toBe("gameOver");
268
254
 
269
255
  const after = await session.say("I refuse to die. Roll to fight on.");
270
- const attempts = answersOf(after, "action_roll", Refusal);
256
+ const attempts = toolResultsIn(after.toolCalls, "action_roll", Refusal);
271
257
  // The gate is only OBSERVABLE if something calls the gated tool, and the
272
258
  // script is what guarantees that — hence the exact count. `gameOver` was
273
259
  // once a flag nothing acted on, so a player with both tracks empty could
@@ -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,
@@ -215,14 +215,14 @@ describe("speaking", () => {
215
215
 
216
216
  const { audio } = await speak("Hello.");
217
217
 
218
- await expect(uploadInfo(audio)).resolves.toMatchObject({
218
+ await expect(stepUploadInfo(audio)).resolves.toMatchObject({
219
219
  name: "summary.wav",
220
220
  // The byte route serves this as `Content-Type`, and a browser will not
221
221
  // play inline a file it was handed as octet-stream.
222
222
  type: "audio/wav",
223
223
  size: 44 + 4000,
224
224
  });
225
- const { bytes } = await readUpload(audio, { end: 12 });
225
+ const { bytes } = await stepReadUpload(audio, { end: 12 });
226
226
  expect(String.fromCharCode(...bytes.subarray(0, 4))).toBe("RIFF");
227
227
  expect(String.fromCharCode(...bytes.subarray(8, 12))).toBe("WAVE");
228
228
  });
@@ -292,7 +292,7 @@ describe("the whole run", () => {
292
292
  installStubReporter();
293
293
  installStubSpeech();
294
294
 
295
- const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx());
295
+ const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext());
296
296
 
297
297
  expect(summary).toEqual({
298
298
  source: "standup.wav",
@@ -314,7 +314,7 @@ describe("the whole run", () => {
314
314
  installStubReporter();
315
315
  const speech = installStubSpeech();
316
316
 
317
- await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowCtx());
317
+ await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowContext());
318
318
 
319
319
  expect(speech.calls[0]).toMatchObject({ text: "Spoken.", voice: "michael" });
320
320
  });
@@ -324,9 +324,9 @@ describe("the whole run", () => {
324
324
  installStubReporter();
325
325
  installStubSpeech();
326
326
 
327
- await expect(spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx())).rejects.toThrow(
328
- "corrupt audio",
329
- );
327
+ await expect(
328
+ spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext()),
329
+ ).rejects.toThrow("corrupt audio");
330
330
  });
331
331
  });
332
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
  });
@@ -69,9 +69,11 @@ import "@alexkroman1/aai-ui/styles.css";
69
69
  // `workflows/summarize.ts` already declares.
70
70
  import { formatDuration } from "@alexkroman1/aai/utils";
71
71
  import {
72
+ BulletList,
72
73
  createWorkflowApi,
74
+ Facts,
73
75
  Form,
74
- page,
76
+ mountPage,
75
77
  SubmitButton,
76
78
  UploadProgressBar,
77
79
  useDownloadUrl,
@@ -79,7 +81,6 @@ import {
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,16 +129,11 @@ function captionsUrl(text: string, durationMs: number): string {
128
129
  }
129
130
 
130
131
  export function App() {
131
- // Did THIS load start the run? A reload cannot have, and that is the only way
132
- // the page can tell "working on what you just sent" from "picking up where
133
- // you left off" — the hook reports the run, not who asked for it.
134
- const [startedHere, setStartedHere] = useState(false);
135
132
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
136
133
  // `run.output` instead of `unknown`. The reload is the hook's own doing — see
137
134
  // the module doc for why the key it mints is the right one for this page.
138
- const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
139
- typeof spokenSummary
140
- >(WORKFLOW, { api });
135
+ const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
136
+ useWorkflowSubmit<typeof spokenSummary>(WORKFLOW, { api });
141
137
  const output = run?.status === "completed" ? run.output : undefined;
142
138
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
143
139
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -153,14 +149,7 @@ export function App() {
153
149
  </p>
154
150
  </header>
155
151
 
156
- <Form
157
- onSubmit={(values) => {
158
- setStartedHere(true);
159
- return submitForm(values);
160
- }}
161
- error={error}
162
- className="flex flex-col gap-4"
163
- >
152
+ <Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
164
153
  {/* Every control, from the workflow's own input schema. See the module doc. */}
165
154
  <WorkflowFields workflow={WORKFLOW} />
166
155
  <SubmitButton pending={pending} pendingLabel="Working…">
@@ -180,25 +169,27 @@ export function App() {
180
169
  nothing for `<WorkflowProgress>` to read. */}
181
170
  <UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
182
171
 
183
- {/* 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. */}
184
173
  <WorkflowProgress runId={run?.runId} api={api} />
185
174
 
186
- {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
+ )}
187
182
 
188
183
  {output !== undefined && (
189
184
  <article className="flex flex-col gap-5">
190
185
  <div className="flex flex-col gap-1">
191
186
  <h2 className="text-xl">{output.headline}</h2>
192
- <p className="text-sm opacity-70">
193
- {output.source} · {formatDuration(output.durationMs)} · {output.words} words
194
- </p>
187
+ <Facts
188
+ items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
189
+ />
195
190
  </div>
196
191
 
197
- <ul className="flex list-disc flex-col gap-1 pl-5">
198
- {output.points.map((point) => (
199
- <li key={point}>{point}</li>
200
- ))}
201
- </ul>
192
+ <BulletList items={output.points} />
202
193
 
203
194
  <section className="flex flex-col gap-2">
204
195
  <h3 className="text-sm font-medium opacity-70">
@@ -206,11 +197,13 @@ export function App() {
206
197
  </h3>
207
198
  {audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
208
199
  {audio.error !== undefined && (
209
- <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>
210
203
  )}
211
204
  {audio.url !== undefined && (
212
205
  <>
213
- <audio controls src={audio.url} className="w-full">
206
+ <audio aria-label="Summary read aloud" controls src={audio.url} className="w-full">
214
207
  {/* A real caption track, not a suppression: the summary was
215
208
  written before it was spoken, so the words are already
216
209
  here and one cue spanning the clip is an honest
@@ -244,4 +237,4 @@ export function App() {
244
237
  );
245
238
  }
246
239
 
247
- page({ name: "Spoken Summary", component: App });
240
+ mountPage({ name: "Spoken Summary", component: App });