@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
@@ -1,12 +1,16 @@
1
+ import { 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 {
5
+ AGENT_STATE_LABELS,
4
6
  AutoScroll,
5
- client,
7
+ mountClient,
8
+ SessionErrorBanner,
6
9
  useAgentState,
7
10
  useConversation,
8
- useSession,
11
+ useSessionActions,
9
12
  useSessionSelector,
13
+ useSessionStatus,
10
14
  } from "@alexkroman1/aai-ui";
11
15
  import type { DispatchState, IncidentSummary, Severity, Status } from "./shared.ts";
12
16
  import { dashboardProjection } from "./shared.ts";
@@ -77,6 +81,20 @@ const STATE_COLORS = {
77
81
  error: "#6b7280",
78
82
  } satisfies Record<AgentState, string>;
79
83
 
84
+ /*
85
+ * The board's own vocabulary, spread over the package's record rather than
86
+ * written as a ternary chain. Only three of the seven states get a dispatch
87
+ * word; the rest come from `AGENT_STATE_LABELS`, so a state added upstream
88
+ * reads as something rather than falling through to whichever arm the chain
89
+ * ended on.
90
+ */
91
+ const STATE_LABELS: Record<AgentState, string> = {
92
+ ...AGENT_STATE_LABELS,
93
+ listening: "LISTENING",
94
+ thinking: "PROCESSING",
95
+ speaking: "TRANSMITTING",
96
+ };
97
+
80
98
  function Panel({ title, children }: { title: string; children: React.ReactNode }) {
81
99
  return (
82
100
  <div className="rounded-lg p-3" style={{ background: "#1a1a2e", border: "1px solid #1e293b" }}>
@@ -230,8 +248,16 @@ function Conversation() {
230
248
  {streaming}
231
249
  </div>
232
250
  )}
251
+ {/* Same contract as the shipped `MessageList`'s indicator: three pulsing
252
+ dots are the only sign the desk is working, and to a screen reader
253
+ they are punctuation. */}
233
254
  {thinking && (
234
- <div className="self-start text-[11px] px-3.5" style={{ color: "#64748b" }}>
255
+ <div
256
+ role="status"
257
+ aria-label="Dispatch is thinking"
258
+ className="self-start text-[11px] px-3.5"
259
+ style={{ color: "#64748b" }}
260
+ >
235
261
  <span style={{ animation: "dc-pulse 1.2s ease-in-out infinite" }}>· · ·</span>
236
262
  </div>
237
263
  )}
@@ -261,7 +287,7 @@ function Conversation() {
261
287
  * it one level up dragged the incident board through every snapshot change.
262
288
  */
263
289
  function StatusReadout() {
264
- const state = useSessionSelector((s) => s.state);
290
+ const state = useSessionStatus();
265
291
  return (
266
292
  <>
267
293
  <span
@@ -278,70 +304,36 @@ function StatusReadout() {
278
304
  title={state}
279
305
  />
280
306
  <span className="text-[11px] font-normal normal-case" style={{ color: "#64748b" }}>
281
- {state === "listening"
282
- ? "LISTENING"
283
- : state === "thinking"
284
- ? "PROCESSING"
285
- : state === "speaking"
286
- ? "TRANSMITTING"
287
- : state.toUpperCase()}
307
+ {STATE_LABELS[state]}
288
308
  </span>
289
309
  </>
290
310
  );
291
311
  }
292
312
 
293
- /**
294
- * The fatal-error banner.
313
+ /*
314
+ * The shift controls.
295
315
  *
296
- * `role="alert"` is the part that is easy to leave out and is load-bearing: per
297
- * the `fatalError` latch in the session core this banner is the only remaining
298
- * signal the status eyebrow beside it goes back to reading like a live
299
- * session and a screen reader is never told an unannounced one appeared. It
300
- * is what `ConsoleShell` carries, which this template's full-bleed two-pane
301
- * board cannot use as its frame.
316
+ * `useSessionActions()` for the methods and two one-field selectors for the
317
+ * flags, rather than the whole-snapshot `useSession()` this used to hold. The
318
+ * methods are what the row is really after, and `useSession()` re-renders on
319
+ * every snapshot change so four buttons re-rendered at STT-partial rate to
320
+ * read two booleans that flip once a shift.
302
321
  */
303
- function ErrorBanner() {
304
- const error = useSessionSelector((s) => s.error);
305
- if (!error) return null;
306
- return (
307
- <div
308
- role="alert"
309
- className="px-4 py-2 text-xs"
310
- style={{ background: "#450a0a", color: "#fca5a5", borderTop: "1px solid #991b1b" }}
311
- >
312
- ERROR: {error.message} ({error.code})
313
- </div>
314
- );
315
- }
316
-
317
- /**
318
- * Start a fresh conversation without leaving the console.
319
- *
320
- * Written out here rather than reached for on the session, because there is no
321
- * one method that does it: `reset()` clears the CONVERSATION and keeps the
322
- * session, which is wrong for any agent that also keeps session-scoped state —
323
- * this one's incident board would come back with the next tool call.
324
- */
325
- function newConversation(session: Session): void {
326
- session.end();
327
- session.start();
328
- }
329
-
330
- /** The shift controls. The one place a whole-session read is what is wanted:
331
- * it needs `started`, `running` and four methods, and it is four buttons. */
332
322
  function ShiftControls({ logged }: { logged: number }) {
333
- const session = useSession();
323
+ const { start, toggle, restart, end } = useSessionActions();
324
+ const started = useSessionSelector((s) => s.started);
325
+ const running = useSessionSelector((s) => s.running);
334
326
  return (
335
327
  <div
336
328
  className="flex items-center gap-2.5 px-4 py-3"
337
329
  style={{ background: "#111827", borderTop: "1px solid #1e293b" }}
338
330
  >
339
- {!session.started ? (
331
+ {!started ? (
340
332
  <button
341
333
  type="button"
342
334
  className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
343
335
  style={{ background: "#2563eb" }}
344
- onClick={() => session.start()}
336
+ onClick={() => start()}
345
337
  >
346
338
  Start Dispatch
347
339
  </button>
@@ -351,12 +343,12 @@ function ShiftControls({ logged }: { logged: number }) {
351
343
  type="button"
352
344
  className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
353
345
  style={{
354
- background: session.running ? "#334155" : "#2563eb",
355
- color: session.running ? "#e2e8f0" : "white",
346
+ background: running ? "#334155" : "#2563eb",
347
+ color: running ? "#e2e8f0" : "white",
356
348
  }}
357
- onClick={() => session.toggle()}
349
+ onClick={() => toggle()}
358
350
  >
359
- {session.running ? "Pause" : "Resume"}
351
+ {running ? "Pause" : "Resume"}
360
352
  </button>
361
353
  {/* The one-click new conversation the default shell's
362
354
  `<Controls>` gives every other template — a custom
@@ -375,7 +367,7 @@ function ShiftControls({ logged }: { logged: number }) {
375
367
  type="button"
376
368
  className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
377
369
  style={{ background: "#1e293b", color: "#e2e8f0" }}
378
- onClick={() => newConversation(session)}
370
+ onClick={restart}
379
371
  >
380
372
  New Conversation
381
373
  </button>
@@ -388,7 +380,7 @@ function ShiftControls({ logged }: { logged: number }) {
388
380
  type="button"
389
381
  className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
390
382
  style={{ background: "#dc2626" }}
391
- onClick={() => session.end()}
383
+ onClick={() => end()}
392
384
  >
393
385
  End Shift
394
386
  </button>
@@ -396,7 +388,7 @@ function ShiftControls({ logged }: { logged: number }) {
396
388
  )}
397
389
  <div className="flex-1" />
398
390
  <span className="text-[10px]" style={{ color: "#475569" }}>
399
- {logged} incident{logged !== 1 ? "s" : ""} logged
391
+ {logged} {plural(logged, "incident")} logged
400
392
  </span>
401
393
  </div>
402
394
  );
@@ -471,7 +463,7 @@ function App() {
471
463
  style={{ borderRight: "1px solid #1e293b" }}
472
464
  >
473
465
  <Conversation />
474
- <ErrorBanner />
466
+ <SessionErrorBanner className="rounded-none border-x-0 border-b-0" />
475
467
  <ShiftControls logged={incidentList.length} />
476
468
  </div>
477
469
 
@@ -524,7 +516,7 @@ function App() {
524
516
  );
525
517
  }
526
518
 
527
- client({
519
+ mountClient({
528
520
  component: App,
529
521
  theme: {
530
522
  bg: "#0a0a0f",
@@ -71,7 +71,6 @@ export interface Incident {
71
71
  createdAt: number;
72
72
  updatedAt: number;
73
73
  escalationLevel: number;
74
- protocolsActivated: string[];
75
74
  casualties: { confirmed: number; estimated: number; treated: number };
76
75
  hazards: string[];
77
76
  }
@@ -82,7 +81,6 @@ export interface DispatchState {
82
81
  incidentCounter: number;
83
82
  mutualAidCounter: number;
84
83
  alertLevel: "green" | "yellow" | "orange" | "red";
85
- mutualAidRequested: boolean;
86
84
  }
87
85
 
88
86
  /**
@@ -157,7 +155,6 @@ export function createDefaultState(): DispatchState {
157
155
  incidentCounter: 0,
158
156
  mutualAidCounter: 0,
159
157
  alertLevel: "green",
160
- mutualAidRequested: false,
161
158
  };
162
159
  }
163
160
 
@@ -348,7 +345,6 @@ export function createIncident(state: DispatchState, overrides: Partial<Incident
348
345
  createdAt: time,
349
346
  updatedAt: time,
350
347
  escalationLevel: 0,
351
- protocolsActivated: [],
352
348
  casualties: { confirmed: 0, estimated: 0, treated: 0 },
353
349
  hazards: [],
354
350
  ...overrides,
@@ -730,8 +726,4 @@ export function recalculateAlertLevel(state: DispatchState): void {
730
726
  } else {
731
727
  state.alertLevel = "green";
732
728
  }
733
-
734
- // Mutual aid tracks system posture: requested at red alert, stood down
735
- // when the alert level drops back below red.
736
- state.mutualAidRequested = state.alertLevel === "red";
737
729
  }
@@ -25,7 +25,10 @@ export default dispatchSlot.tool({
25
25
 
26
26
  return {
27
27
  systemAlertLevel: state.alertLevel,
28
- mutualAidActive: state.mutualAidRequested,
28
+ // Derived, not stored: mutual aid is requested at red alert and stood
29
+ // down when the level drops back below it, so the alert level IS the
30
+ // fact and the two can never disagree.
31
+ mutualAidActive: state.alertLevel === "red",
29
32
  resourceUtilization: `${utilization}%`,
30
33
  resourceSummary,
31
34
  activeIncidentCount: activeIncidents.length,
@@ -13,8 +13,10 @@
13
13
  /**
14
14
  * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
15
15
  *
16
- * The glob is written HERE rather than reached for from a shared helper because
17
- * this file SHIPS a scaffolded project has no repo helper to import. Without
16
+ * Taken from `virtual:aai/agent` rather than a hand-written glob: the plugin
17
+ * expands it against THIS file's own directory, so the spec needs no glob and
18
+ * no shared helper — which matters because this file SHIPS, and a scaffolded
19
+ * project has no repo helper to import. Without
18
20
  * it this agent would have no tools and would answer every question from the
19
21
  * model's own memory, which is exactly what the cases below forbid.
20
22
  *
@@ -22,8 +22,10 @@
22
22
  /**
23
23
  * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
24
24
  *
25
- * The glob is written HERE rather than reached for from a shared helper because
26
- * this file SHIPS a scaffolded project has no repo helper to import. Without
25
+ * Taken from `virtual:aai/agent` rather than a hand-written glob: the plugin
26
+ * expands it against THIS file's own directory, so the spec needs no glob and
27
+ * no shared helper — which matters because this file SHIPS, and a scaffolded
28
+ * project has no repo helper to import. Without
27
29
  * it the desk would have no tools and would answer every one of these from
28
30
  * memory, which is the failure these cases exist to catch.
29
31
  *
@@ -1,10 +1,11 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { AgentState, Session } from "@alexkroman1/aai-ui";
2
+ import type { AgentState } from "@alexkroman1/aai-ui";
3
3
  import {
4
4
  AutoScroll,
5
- client,
5
+ mountClient,
6
6
  useConversation,
7
- useSession,
7
+ useSessionActions,
8
+ useSessionError,
8
9
  useSessionSelector,
9
10
  useTheme,
10
11
  } from "@alexkroman1/aai-ui";
@@ -98,7 +99,7 @@ function Transcript() {
98
99
  // The one theme read left in this file, and the case `useTheme()` is still
99
100
  // for: `scrollbar-color` takes TWO values and has no utility class, so it has
100
101
  // to be a style — and reading it off the theme beats re-pinning the two hex
101
- // codes the `client({ theme })` block below already declares.
102
+ // codes the `mountClient({ theme })` block below already declares.
102
103
  const theme = useTheme();
103
104
 
104
105
  return (
@@ -146,8 +147,16 @@ function Transcript() {
146
147
  {transcript.text}
147
148
  </div>
148
149
  )}
150
+ {/* Same contract as the shipped `MessageList`'s indicator: the blinking
151
+ block is the only sign the parser is working, and to a screen reader it
152
+ is one unpronounceable glyph. */}
149
153
  {thinking && (
150
- <div className="animate-pulse" style={{ color: GREEN_DIM }}>
154
+ <div
155
+ role="status"
156
+ aria-label="The parser is thinking"
157
+ className="animate-pulse"
158
+ style={{ color: GREEN_DIM }}
159
+ >
151
160
  &#9612;
152
161
  </div>
153
162
  )}
@@ -155,12 +164,18 @@ function Transcript() {
155
164
  );
156
165
  }
157
166
 
167
+ /** Module scope, so the selector has a STABLE identity: an inline arrow makes
168
+ * `useSyncExternalStoreWithSelector` rebuild its selection memo every render,
169
+ * and this runs on every snapshot push — each STT partial and streaming
170
+ * delta. Counting with `reduce` rather than `filter().length` for the same
171
+ * reason: the array copy is thrown away, and it grows for the whole adventure. */
172
+ const userTurns = (snapshot: { messages: { role: string }[] }): number =>
173
+ snapshot.messages.reduce((n, message) => (message.role === "user" ? n + 1 : n), 0);
174
+
158
175
  /** The turn counter — a NUMBER out of the selector, so a new message array with
159
176
  * the same user-message count re-renders nothing. */
160
177
  function TurnCount() {
161
- const turns = useSessionSelector(
162
- (snapshot) => snapshot.messages.filter((message) => message.role === "user").length,
163
- );
178
+ const turns = useSessionSelector(userTurns);
164
179
  return <span>Turns: {turns}</span>;
165
180
  }
166
181
 
@@ -207,7 +222,7 @@ function StatusDot() {
207
222
  * never told an unannounced one appeared.
208
223
  */
209
224
  function ErrorBanner() {
210
- const error = useSessionSelector((snapshot) => snapshot.error);
225
+ const error = useSessionError();
211
226
  if (!error) return null;
212
227
  return (
213
228
  <div
@@ -215,33 +230,22 @@ function ErrorBanner() {
215
230
  className="px-5 py-2 text-xs"
216
231
  style={{ background: "#3a0000", color: "#ff4141" }}
217
232
  >
218
- ERROR: {error.message}
233
+ ERROR: {error.message} ({error.code})
219
234
  </div>
220
235
  );
221
236
  }
222
237
 
223
- /**
224
- * Start a fresh conversation without leaving the game screen.
238
+ /*
239
+ * Pause/resume, new game and hang-up.
225
240
  *
226
- * Written out here rather than reached for on the session, because there is no
227
- * one method that does it: `reset()` clears the CONVERSATION and keeps the
228
- * session, which is wrong for any agent that also keeps session-scoped state —
229
- * this game's world would come back with the next tool call. `end()` drops the
230
- * resume identity, so the redial is a brand-new session (fresh world, opening
231
- * scene included), and `start()` puts the player straight into it rather than
232
- * back at the title screen.
241
+ * `useSessionActions()` is the narrow way `<Controls>` reaches the methods, and
242
+ * it is published now so this row takes the three it presses and one
243
+ * selector for the flag it reads, instead of a whole-snapshot `useSession()`
244
+ * that re-rendered the footer on every partial transcript.
233
245
  */
234
- function newConversation(session: Session): void {
235
- session.end();
236
- session.start();
237
- }
238
-
239
- /** Pause/resume, new game and hang-up. The only place a whole-session
240
- * subscription is still needed: the ACTIONS live on `useSession()`, and
241
- * `useSessionCore` — the narrow way `<Controls>` reaches them — is not on the
242
- * public surface. */
243
246
  function Footer() {
244
- const session = useSession();
247
+ const { toggle, restart, end } = useSessionActions();
248
+ const running = useSessionSelector((snapshot) => snapshot.running);
245
249
  return (
246
250
  <div
247
251
  className="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
@@ -253,9 +257,9 @@ function Footer() {
253
257
  type="button"
254
258
  className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
255
259
  style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
256
- onClick={session.toggle}
260
+ onClick={toggle}
257
261
  >
258
- {session.running ? "[P]ause" : "[R]esume"}
262
+ {running ? "[P]ause" : "[R]esume"}
259
263
  </button>
260
264
  {/* The one-click new conversation the default shell's `<Controls>`
261
265
  gives every other template — a custom `component:` renders no
@@ -266,7 +270,7 @@ function Footer() {
266
270
  type="button"
267
271
  className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
268
272
  style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
269
- onClick={() => newConversation(session)}
273
+ onClick={restart}
270
274
  >
271
275
  [N]ew Game
272
276
  </button>
@@ -276,7 +280,7 @@ function Footer() {
276
280
  type="button"
277
281
  className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
278
282
  style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
279
- onClick={() => session.end()}
283
+ onClick={() => end()}
280
284
  >
281
285
  [Q]uit
282
286
  </button>
@@ -315,7 +319,7 @@ function Crt({ children }: { children: ReactNode }) {
315
319
  }
316
320
 
317
321
  function TitleScreen() {
318
- const session = useSession();
322
+ const { start } = useSessionActions();
319
323
  return (
320
324
  <Crt>
321
325
  <div
@@ -345,7 +349,7 @@ function TitleScreen() {
345
349
  border: `1px solid ${GREEN}`,
346
350
  animation: "ic-pulse 2s ease-in-out infinite",
347
351
  }}
348
- onClick={session.start}
352
+ onClick={start}
349
353
  >
350
354
  Begin Adventure
351
355
  </button>
@@ -381,7 +385,7 @@ function InfocomAdventure() {
381
385
  );
382
386
  }
383
387
 
384
- client({
388
+ mountClient({
385
389
  component: InfocomAdventure,
386
390
  theme: {
387
391
  bg: CRT_BG,
@@ -26,7 +26,7 @@
26
26
  */
27
27
 
28
28
  import {
29
- createWorkflowCtx,
29
+ createWorkflowContext,
30
30
  schemaInputIssues,
31
31
  stubGatewayRoute,
32
32
  stubStepInfo,
@@ -170,7 +170,7 @@ describe("fetchArticle", () => {
170
170
  });
171
171
 
172
172
  test("fails FATALLY on a 404 and plainly on a 503", async () => {
173
- // `stepFetchOk` builds the message, so it is the SDK's one spelling for a
173
+ // `stepFetchOrFail` builds the message, so it is the SDK's one spelling for a
174
174
  // bad response — the REQUEST, the status, and a preview of any body.
175
175
  stubPage("", 404);
176
176
  await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(
@@ -264,7 +264,7 @@ describe("summarize", () => {
264
264
  // which is the only place the policy is observable at all. `runSteps: false`
265
265
  // because the subject is the declared policy rather than the work: the steps
266
266
  // would otherwise need a page and a model.
267
- const ctx = createWorkflowCtx({ runSteps: false });
267
+ const ctx = createWorkflowContext({ runSteps: false });
268
268
  await digestFlow({ url: "https://example.com/a" }, ctx);
269
269
 
270
270
  const summarizeStep = ctx.steps.find((step) => step.name === "summarize");
@@ -13,8 +13,8 @@
13
13
  * the entire product, and the declaration says so:
14
14
  *
15
15
  * - `workflowApp()` is `agent({ …, page: "static" })` with the discriminant
16
- * already set. The declaration is not decoration: `createServer` declines
17
- * `/websocket` with a reason (so a page mounted with `client()` by mistake
16
+ * already set. The declaration is not decoration: `createRuntimeServer` declines
17
+ * `/websocket` with a reason (so a page mounted with `mountClient()` by mistake
18
18
  * fails the same way in `aai dev` and in production rather than only after a
19
19
  * deploy) and telephony defaults off.
20
20
  * - There is no `stt`/`llm`/`tts`, no `tools` and no `systemPrompt` — and they