@alexkroman1/aai-cli 13.3.0 → 15.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 (158) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/_artifacts-BJOYGQPp.mjs +21 -0
  4. package/dist/_artifacts.d.ts +16 -0
  5. package/dist/_build-target.d.ts +172 -0
  6. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
  7. package/dist/_cli-common.d.ts +45 -6
  8. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  9. package/dist/_dev-env.d.ts +1 -1
  10. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
  11. package/dist/_dev-vite-config.d.ts +3 -3
  12. package/dist/_e2e-test-utils.d.ts +1 -1
  13. package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
  14. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  15. package/dist/_output.d.ts +10 -9
  16. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  17. package/dist/_resource-commands.d.ts +9 -9
  18. package/dist/_server-common-De0haHr9.mjs +70 -0
  19. package/dist/_server-common.d.ts +20 -1
  20. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  21. package/dist/_slug-api.d.ts +19 -0
  22. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  23. package/dist/_studio-commands.d.ts +13 -13
  24. package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
  25. package/dist/_templates.d.ts +1 -1
  26. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  27. package/dist/_typecheck-gate.d.ts +3 -1
  28. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  29. package/dist/_utils.d.ts +35 -0
  30. package/dist/_vercel-output.d.ts +63 -0
  31. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  32. package/dist/_workflow-determinism.d.ts +3 -3
  33. package/dist/build-BhEaxBPu.mjs +481 -0
  34. package/dist/build.d.ts +23 -10
  35. package/dist/cli.d.ts +1 -2
  36. package/dist/cli.mjs +155 -138
  37. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
  38. package/dist/client-bundler.d.ts +1 -1
  39. package/dist/client-bundler.mjs +1 -1
  40. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  41. package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
  42. package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
  43. package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
  44. package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
  45. package/dist/init.d.ts +14 -0
  46. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  47. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  48. package/dist/project-config.mjs +1 -1
  49. package/dist/scaffold/CLAUDE.md +155 -76
  50. package/dist/scaffold/package.json +6 -6
  51. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  52. package/dist/secret.d.ts +8 -6
  53. package/dist/start.d.ts +112 -0
  54. package/dist/start.mjs +156 -0
  55. package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
  56. package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
  57. package/dist/templates/call-audit/agent.test.ts +11 -6
  58. package/dist/templates/call-audit/agent.ts +2 -2
  59. package/dist/templates/call-audit/client.tsx +27 -42
  60. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  61. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  62. package/dist/templates/call-audit/workflows/media.ts +12 -37
  63. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  64. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  65. package/dist/templates/code-interpreter/agent.test.ts +103 -0
  66. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  67. package/dist/templates/dispatch-center/client.tsx +53 -61
  68. package/dist/templates/dispatch-center/shared.ts +0 -8
  69. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  70. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  71. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  72. package/dist/templates/infocom-adventure/client.tsx +40 -36
  73. package/dist/templates/link-digest/agent.test.ts +3 -3
  74. package/dist/templates/link-digest/agent.ts +2 -2
  75. package/dist/templates/link-digest/client.tsx +81 -19
  76. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  77. package/dist/templates/math-buddy/agent.test.ts +126 -0
  78. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  79. package/dist/templates/night-owl/client.tsx +8 -2
  80. package/dist/templates/personal-finance/agent.test.ts +127 -0
  81. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  82. package/dist/templates/pizza-ordering/client.tsx +5 -4
  83. package/dist/templates/pizza-ordering/shared.ts +3 -8
  84. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  85. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  86. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  87. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  88. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  89. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  90. package/dist/templates/plan-and-execute/client.tsx +2 -2
  91. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  92. package/dist/templates/podcast-digest/client.tsx +16 -22
  93. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  94. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  95. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  96. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  97. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  98. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  99. package/dist/templates/redline/agent.eval.test.ts +5 -8
  100. package/dist/templates/redline/agent.test.ts +1 -1
  101. package/dist/templates/redline/client.tsx +36 -34
  102. package/dist/templates/redline/workflows/redline.ts +17 -17
  103. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  104. package/dist/templates/research-workflow/agent.test.ts +10 -7
  105. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  106. package/dist/templates/retail/agent.test.ts +17 -15
  107. package/dist/templates/retail/cancel.ts +7 -6
  108. package/dist/templates/retail/client.tsx +39 -58
  109. package/dist/templates/retail/payment.ts +4 -3
  110. package/dist/templates/retail/pending.test.ts +19 -19
  111. package/dist/templates/retail/store.test.ts +5 -5
  112. package/dist/templates/retail/swap.ts +8 -11
  113. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  114. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  115. package/dist/templates/solo-rpg/client.tsx +3 -3
  116. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  117. package/dist/templates/spoken-summary/agent.ts +3 -3
  118. package/dist/templates/spoken-summary/client.tsx +23 -30
  119. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  120. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  121. package/dist/templates/support-line/agent.ts +8 -0
  122. package/dist/templates/support-line/client.tsx +4 -3
  123. package/dist/templates/support-line/prompts.ts +0 -2
  124. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  125. package/dist/templates/transcription-workflow/agent.ts +2 -2
  126. package/dist/templates/transcription-workflow/client.tsx +15 -14
  127. package/dist/templates/transcription-workflow/recover.ts +1 -1
  128. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  129. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  130. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  131. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  132. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  133. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  134. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  135. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  136. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  137. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  138. package/dist/templates/travel-concierge/client.tsx +2 -2
  139. package/dist/templates/travel-concierge/routing.ts +64 -55
  140. package/dist/templates/travel-concierge/shared.ts +10 -14
  141. package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
  142. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
  143. package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
  144. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  145. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  146. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  147. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  148. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  149. package/dist/templates/web-researcher/agent.test.ts +130 -0
  150. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  151. package/dist/typecheck.mjs +1 -1
  152. package/dist/worker-bundler.d.ts +1 -1
  153. package/dist/worker-bundler.mjs +8 -8
  154. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  155. package/package.json +9 -4
  156. package/dist/_server-common-BV4_CrP3.mjs +0 -43
  157. package/dist/build-CWyFGe-W.mjs +0 -69
  158. package/dist/scaffold/server.mjs +0 -204
@@ -10,10 +10,11 @@
10
10
  */
11
11
 
12
12
  import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
13
+ import { formatMoney } from "@alexkroman1/aai/utils";
13
14
  import { creditRefund, REFUND_DELAY_NOTE, REFUND_IMMEDIATE_NOTE } from "./refund.ts";
14
15
  import { resolveOrder } from "./resolve.ts";
15
16
  import type { RetailState } from "./shared.ts";
16
- import { authenticatedUser, money } from "./store.ts";
17
+ import { money } from "./store.ts";
17
18
 
18
19
  /** The only two reasons the store records. */
19
20
  export const CANCEL_REASONS = ["no longer needed", "ordered by mistake"] as const;
@@ -42,9 +43,9 @@ export function planCancel(
42
43
  spokenOrderId: string,
43
44
  reason: CancelReason,
44
45
  ): CancelPlan | ToolFailure {
45
- const user = authenticatedUser(state);
46
- if (isToolFailure(user)) return user;
47
-
46
+ // No `authenticatedUser` gate here: `resolveOrder` opens with the identical
47
+ // call and returns the identical failure, so a second one is a second store
48
+ // lookup for a value this planner never reads.
48
49
  const order = resolveOrder(state, spokenOrderId);
49
50
  if (isToolFailure(order)) return order;
50
51
 
@@ -75,7 +76,7 @@ export function planCancel(
75
76
  return {
76
77
  readBack:
77
78
  `cancel order ${order.order_id} (${items}) as '${reason}' and refund ` +
78
- `$${total.toFixed(2)} to ${destinations}`,
79
+ `${formatMoney(total)} to ${destinations}`,
79
80
  orderId: order.order_id,
80
81
  reason,
81
82
  refunds,
@@ -109,7 +110,7 @@ export function applyCancel(state: RetailState, plan: CancelPlan) {
109
110
  cancel_reason: plan.reason,
110
111
  refunded: plan.total,
111
112
  refund_immediate: immediate,
112
- message: `Order ${plan.orderId} is cancelled and $${plan.total.toFixed(2)} is being refunded. ${
113
+ message: `Order ${plan.orderId} is cancelled and ${formatMoney(plan.total)} is being refunded. ${
113
114
  immediate ? REFUND_IMMEDIATE_NOTE : REFUND_DELAY_NOTE
114
115
  }`,
115
116
  };
@@ -1,12 +1,15 @@
1
+ import { formatMoney, plural } from "@alexkroman1/aai/utils";
1
2
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { AgentState, ConversationItem, Session } from "@alexkroman1/aai-ui";
3
+ import type { AgentState, ConversationItem } from "@alexkroman1/aai-ui";
3
4
  import {
4
5
  AutoScroll,
5
- client,
6
+ mountClient,
7
+ SessionErrorBanner,
6
8
  useAgentState,
7
9
  useConversation,
8
- useSession,
10
+ useSessionActions,
9
11
  useSessionSelector,
12
+ useSessionStatus,
10
13
  } from "@alexkroman1/aai-ui";
11
14
  import type { ReactNode } from "react";
12
15
  import type {
@@ -100,7 +103,7 @@ function MethodRow({ method }: { method: PaymentMethodView }) {
100
103
  </span>
101
104
  ) : (
102
105
  <span className="font-semibold tabular-nums" style={{ color: "#15803d" }}>
103
- ${method.balance.toFixed(2)}
106
+ {formatMoney(method.balance)}
104
107
  </span>
105
108
  )}
106
109
  </div>
@@ -132,8 +135,7 @@ function OrderCard({ order, focused }: { order: OrderView; focused: boolean }) {
132
135
  </span>
133
136
  </div>
134
137
  <div className="text-[11px] mb-1 tabular-nums" style={{ color: "#71717a" }}>
135
- ${order.total.toFixed(2)} · {order.items.length} item
136
- {order.items.length === 1 ? "" : "s"}
138
+ {formatMoney(order.total)} · {order.items.length} {plural(order.items.length, "item")}
137
139
  </div>
138
140
  {order.items.slice(0, 3).map((item) => (
139
141
  <div
@@ -246,7 +248,7 @@ function SwapOptions({ option }: { option: SwapOptionView }) {
246
248
  >
247
249
  <span>{Object.values(alternative.options).join(", ")}</span>
248
250
  <span className="tabular-nums whitespace-nowrap" style={{ color: "#71717a" }}>
249
- ${alternative.price.toFixed(2)}
251
+ {formatMoney(alternative.price)}
250
252
  </span>
251
253
  </div>
252
254
  ))
@@ -344,8 +346,16 @@ function Conversation() {
344
346
  {streaming}
345
347
  </div>
346
348
  )}
349
+ {/* Same contract as the shipped `MessageList`'s indicator: three pulsing
350
+ dots are the only sign the agent is working, and to a screen reader
351
+ they are punctuation. */}
347
352
  {thinking && (
348
- <div className="self-start text-[11px] px-3.5" style={{ color: "#a1a1aa" }}>
353
+ <div
354
+ role="status"
355
+ aria-label="Agent is thinking"
356
+ className="self-start text-[11px] px-3.5"
357
+ style={{ color: "#a1a1aa" }}
358
+ >
349
359
  <span style={{ animation: "rt-pulse 1.2s ease-in-out infinite" }}>· · ·</span>
350
360
  </div>
351
361
  )}
@@ -370,7 +380,7 @@ function Conversation() {
370
380
  /** The live status dot, on its own subscription rather than a field off a
371
381
  * whole-page read — the header is the only thing here that wants it. */
372
382
  function StatusReadout() {
373
- const state = useSessionSelector((s) => s.state);
383
+ const state = useSessionStatus();
374
384
  return (
375
385
  <>
376
386
  <span
@@ -393,58 +403,29 @@ function StatusReadout() {
393
403
  );
394
404
  }
395
405
 
396
- /**
397
- * The fatal-error banner.
406
+ /*
407
+ * The call controls.
398
408
  *
399
- * `role="alert"` is the part a hand-rolled banner leaves out and the part that
400
- * matters: per the `fatalError` latch in the session core this is the only
401
- * remaining signal the status readout beside it goes back to reading like a
402
- * live call and a screen reader is never told an unannounced one appeared.
403
- * `ConsoleShell` carries it, and this template's two-pane page cannot use that
404
- * shell as its frame; the attribute is what it is here for.
409
+ * The methods come off `useSessionActions()`, which does not subscribe, and the
410
+ * two flags off one-field selectors. The whole-snapshot `useSession()` this
411
+ * replaced re-rendered these four buttons on every STT partial to read two
412
+ * booleans that change once a call.
405
413
  */
406
- function ErrorBanner() {
407
- const error = useSessionSelector((s) => s.error);
408
- if (!error) return null;
409
- return (
410
- <div
411
- role="alert"
412
- className="px-4 py-2 text-xs"
413
- style={{ background: "#fef2f2", color: "#b91c1c", borderTop: "1px solid #fecaca" }}
414
- >
415
- {error.message} ({error.code})
416
- </div>
417
- );
418
- }
419
-
420
- /**
421
- * Start a fresh conversation without leaving the console.
422
- *
423
- * Written out here rather than reached for on the session, because there is no
424
- * one method that does it: `reset()` clears the CONVERSATION and keeps the
425
- * session, which is wrong for any agent that also keeps session-scoped state —
426
- * this one's store would come back with the next tool call.
427
- */
428
- function newConversation(session: Session): void {
429
- session.end();
430
- session.start();
431
- }
432
-
433
- /** The call controls: the one place a whole-session read is what is wanted —
434
- * `started`, `running` and four methods, in four buttons. */
435
414
  function CallControls({ productCount }: { productCount: number }) {
436
- const session = useSession();
415
+ const { start, toggle, restart, end } = useSessionActions();
416
+ const started = useSessionSelector((s) => s.started);
417
+ const running = useSessionSelector((s) => s.running);
437
418
  return (
438
419
  <div
439
420
  className="flex items-center gap-2 px-4 py-3"
440
421
  style={{ background: "#ffffff", borderTop: "1px solid #e4e4e7" }}
441
422
  >
442
- {!session.started ? (
423
+ {!started ? (
443
424
  <button
444
425
  type="button"
445
426
  className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
446
427
  style={{ background: "#2563eb" }}
447
- onClick={() => session.start()}
428
+ onClick={() => start()}
448
429
  >
449
430
  Start call
450
431
  </button>
@@ -454,12 +435,12 @@ function CallControls({ productCount }: { productCount: number }) {
454
435
  type="button"
455
436
  className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer"
456
437
  style={{
457
- background: session.running ? "#e4e4e7" : "#2563eb",
458
- color: session.running ? "#18181b" : "#ffffff",
438
+ background: running ? "#e4e4e7" : "#2563eb",
439
+ color: running ? "#18181b" : "#ffffff",
459
440
  }}
460
- onClick={() => session.toggle()}
441
+ onClick={() => toggle()}
461
442
  >
462
- {session.running ? "Hold" : "Resume"}
443
+ {running ? "Hold" : "Resume"}
463
444
  </button>
464
445
  {/* The one-click new conversation the default shell's
465
446
  `<Controls>` gives every other template — a custom
@@ -472,7 +453,7 @@ function CallControls({ productCount }: { productCount: number }) {
472
453
  type="button"
473
454
  className="px-4 py-2 rounded-md text-xs font-semibold cursor-pointer"
474
455
  style={{ background: "#ffffff", color: "#18181b", border: "1px solid #e4e4e7" }}
475
- onClick={() => newConversation(session)}
456
+ onClick={restart}
476
457
  >
477
458
  New Conversation
478
459
  </button>
@@ -484,7 +465,7 @@ function CallControls({ productCount }: { productCount: number }) {
484
465
  type="button"
485
466
  className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
486
467
  style={{ background: "#dc2626" }}
487
- onClick={() => session.end()}
468
+ onClick={() => end()}
488
469
  >
489
470
  End
490
471
  </button>
@@ -545,7 +526,7 @@ function App() {
545
526
  style={{ borderRight: "1px solid #e4e4e7" }}
546
527
  >
547
528
  <Conversation />
548
- <ErrorBanner />
529
+ <SessionErrorBanner className="rounded-none border-x-0 border-b-0" />
549
530
  <CallControls productCount={view.productCount} />
550
531
  </div>
551
532
 
@@ -642,7 +623,7 @@ function App() {
642
623
  )}
643
624
  </span>
644
625
  <span className="tabular-nums whitespace-nowrap ml-3">
645
- {view.callSeq} call{view.callSeq === 1 ? "" : "s"}
626
+ {view.callSeq} {plural(view.callSeq, "call")}
646
627
  </span>
647
628
  </div>
648
629
  </div>
@@ -650,7 +631,7 @@ function App() {
650
631
  );
651
632
  }
652
633
 
653
- client({
634
+ mountClient({
654
635
  component: App,
655
636
  theme: {
656
637
  bg: "#f4f4f5",
@@ -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