@alexkroman1/aai-cli 6.10.1 → 7.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 (162) hide show
  1. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  2. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
  3. package/dist/_dev-env.d.ts +22 -2
  4. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
  5. package/dist/_dev-server.d.ts +0 -17
  6. package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
  7. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  8. package/dist/_preflight.d.ts +1 -1
  9. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
  10. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  11. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
  12. package/dist/_workflow-scan.d.ts +108 -0
  13. package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
  14. package/dist/cli.mjs +30 -25
  15. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  16. package/dist/client-bundler.mjs +1 -1
  17. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  18. package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
  19. package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
  20. package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
  21. package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
  22. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  23. package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
  24. package/dist/project-config.mjs +1 -1
  25. package/dist/scaffold/CLAUDE.md +93 -14
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +13 -4
  28. package/dist/scaffold/vite.config.ts +1 -1
  29. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  30. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  31. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  32. package/dist/templates/call-audit/agent.test.ts +818 -0
  33. package/dist/templates/call-audit/agent.ts +171 -0
  34. package/dist/templates/call-audit/client.tsx +198 -0
  35. package/dist/templates/call-audit/workflows/audit.ts +298 -0
  36. package/dist/templates/call-audit/workflows/ingest.ts +251 -0
  37. package/dist/templates/call-audit/workflows/media.ts +637 -0
  38. package/dist/templates/call-audit/workflows/summarize.ts +201 -0
  39. package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
  40. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  41. package/dist/templates/dispatch-center/client.tsx +239 -129
  42. package/dist/templates/dispatch-center/shared.ts +99 -1
  43. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  44. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  45. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  46. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  47. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  48. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  49. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  50. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  51. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  52. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  53. package/dist/templates/health-assistant/agent.test.ts +22 -4
  54. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  55. package/dist/templates/infocom-adventure/client.tsx +246 -164
  56. package/dist/templates/link-digest/agent.test.ts +24 -19
  57. package/dist/templates/link-digest/client.tsx +47 -61
  58. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  59. package/dist/templates/night-owl/agent.test.ts +70 -19
  60. package/dist/templates/night-owl/agent.ts +5 -0
  61. package/dist/templates/night-owl/client.tsx +56 -56
  62. package/dist/templates/night-owl/shared.ts +24 -0
  63. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  64. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  65. package/dist/templates/pizza-ordering/client.tsx +9 -26
  66. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  67. package/dist/templates/plan-and-execute/agent.ts +1 -1
  68. package/dist/templates/plan-and-execute/client.tsx +12 -15
  69. package/dist/templates/plan-and-execute/shared.ts +71 -2
  70. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  71. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  72. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  73. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  74. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  75. package/dist/templates/podcast-digest/agent.ts +139 -0
  76. package/dist/templates/podcast-digest/client.tsx +154 -0
  77. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  78. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  79. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  80. package/dist/templates/recap-workflow/agent.test.ts +107 -96
  81. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  82. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  83. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  84. package/dist/templates/recap-workflow/workflows/recap.ts +48 -41
  85. package/dist/templates/redline/agent.test.ts +17 -15
  86. package/dist/templates/redline/client.tsx +12 -12
  87. package/dist/templates/redline/workflows/redline.ts +19 -31
  88. package/dist/templates/research-workflow/agent.test.ts +60 -59
  89. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  90. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  91. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  92. package/dist/templates/retail/agent.test.ts +26 -23
  93. package/dist/templates/retail/client.tsx +226 -117
  94. package/dist/templates/retail/registry.test.ts +38 -6
  95. package/dist/templates/retail/store.test.ts +82 -15
  96. package/dist/templates/retail/store.ts +174 -47
  97. package/dist/templates/retail/system-prompt.md +11 -2
  98. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  99. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  100. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  101. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  102. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  105. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  106. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  107. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  108. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  109. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  110. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  111. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  112. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  113. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  114. package/dist/templates/solo-rpg/client.tsx +38 -37
  115. package/dist/templates/solo-rpg/shared.ts +145 -19
  116. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  117. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  118. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  119. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  120. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  121. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  122. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  123. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  124. package/dist/templates/spoken-summary/agent.test.ts +314 -0
  125. package/dist/templates/spoken-summary/agent.ts +155 -0
  126. package/dist/templates/spoken-summary/client.tsx +185 -0
  127. package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
  128. package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
  129. package/dist/templates/support-line/agent.test.ts +11 -16
  130. package/dist/templates/support-line/agent.ts +1 -1
  131. package/dist/templates/support-line/client.tsx +9 -9
  132. package/dist/templates/support-line/nodes.ts +100 -0
  133. package/dist/templates/support-line/procedure.ts +407 -0
  134. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  135. package/dist/templates/transcription-workflow/agent.test.ts +246 -123
  136. package/dist/templates/transcription-workflow/agent.ts +21 -7
  137. package/dist/templates/transcription-workflow/client.tsx +17 -42
  138. package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
  139. package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
  140. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  141. package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
  142. package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
  143. package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
  144. package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
  145. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  146. package/dist/templates/travel-concierge/client.tsx +11 -23
  147. package/dist/templates/travel-concierge/routing.ts +34 -15
  148. package/dist/templates/travel-concierge/shared.ts +70 -3
  149. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  153. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  154. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  155. package/dist/worker-bundler.mjs +1 -1
  156. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  157. package/dist/workflow-bundler.d.ts +6 -1
  158. package/dist/workflow.d.ts +1 -1
  159. package/package.json +5 -4
  160. package/dist/templates/support-line/graph.ts +0 -224
  161. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  162. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -1,11 +1,12 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
2
+ import type { AgentState, ConversationItem } from "@alexkroman1/aai-ui";
3
3
  import {
4
4
  AutoScroll,
5
5
  client,
6
6
  useAgentState,
7
+ useConversation,
7
8
  useSession,
8
- useUserTranscript,
9
+ useSessionSelector,
9
10
  } from "@alexkroman1/aai-ui";
10
11
  import type { DispatchState, IncidentSummary, Severity, Status } from "./shared.ts";
11
12
  import { dashboardProjection } from "./shared.ts";
@@ -139,15 +140,242 @@ function IncidentCard({ inc }: { inc: IncidentSummary }) {
139
140
  );
140
141
  }
141
142
 
142
- function App() {
143
+ /** One radio line: a message bubble, or the tool call that came after it. */
144
+ function Row({ item }: { item: ConversationItem }) {
145
+ if (item.kind === "tool") {
146
+ const { name, status } = item.toolCall;
147
+ return (
148
+ <div
149
+ className="self-start rounded-md px-2.5 py-1.5 text-[11px] font-mono flex items-center gap-2"
150
+ style={{
151
+ background: "#111827",
152
+ border: "1px solid #1e293b",
153
+ color: status === "pending" ? "#eab308" : "#64748b",
154
+ }}
155
+ >
156
+ <span
157
+ className="w-1.5 h-1.5 rounded-full inline-block"
158
+ style={{
159
+ background: status === "pending" ? "#eab308" : "#22c55e",
160
+ animation: status === "pending" ? "dc-pulse 1s ease-in-out infinite" : "none",
161
+ }}
162
+ />
163
+ {name}
164
+ </div>
165
+ );
166
+ }
167
+ const { role, content } = item.message;
168
+ return (
169
+ <div
170
+ className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5"
171
+ style={{
172
+ lineHeight: 1.6,
173
+ alignSelf: role === "assistant" ? "flex-start" : "flex-end",
174
+ background: role === "assistant" ? "#1e293b" : "#172554",
175
+ animation: "dc-slide-in 0.2s ease-out",
176
+ borderLeft: role === "assistant" ? "3px solid #3b82f6" : "none",
177
+ borderRight: role !== "assistant" ? "3px solid #22d3ee" : "none",
178
+ }}
179
+ >
180
+ <div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: "#64748b" }}>
181
+ {role === "assistant" ? "DISPATCH" : "OPERATOR"}
182
+ </div>
183
+ {content}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ /**
189
+ * The radio log.
190
+ *
191
+ * `useConversation()` rather than a `session.messages.map(...)`: the messages
192
+ * were only half of it. This board runs eight tools and rendered NONE of them,
193
+ * because tool calls live in a second array this page never read — and it also
194
+ * dropped the streaming reply and the thinking indicator. The hook owns the
195
+ * interleave (a tool row follows its anchor message), the `null`-vs-`""`
196
+ * transcript distinction and the thinking-suppression rule; what stays here is
197
+ * the markup, which is the part this template exists to show.
198
+ *
199
+ * It also subscribes per FIELD, so the conversation re-renders at the
200
+ * conversation's rate. The whole-page `useSession()` this replaced re-rendered
201
+ * the incident board on every STT partial.
202
+ */
203
+ function Conversation() {
204
+ const { items, streaming, transcript, thinking } = useConversation();
205
+ return (
206
+ <>
207
+ <AutoScroll
208
+ scrollClassName="dc-messages overflow-y-auto"
209
+ contentClassName="p-4 flex flex-col gap-2"
210
+ >
211
+ {items.length === 0 && !streaming && (
212
+ <div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
213
+ Dispatch Command Center standing by. Click START to begin operations.
214
+ </div>
215
+ )}
216
+ {items.map((item) => (
217
+ <Row
218
+ key={item.kind === "message" ? `m${item.message.id}` : item.toolCall.callId}
219
+ item={item}
220
+ />
221
+ ))}
222
+ {streaming !== null && (
223
+ <div
224
+ className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5 self-start"
225
+ style={{ lineHeight: 1.6, background: "#1e293b", borderLeft: "3px solid #3b82f6" }}
226
+ >
227
+ <div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: "#64748b" }}>
228
+ DISPATCH
229
+ </div>
230
+ {streaming}
231
+ </div>
232
+ )}
233
+ {thinking && (
234
+ <div className="self-start text-[11px] px-3.5" style={{ color: "#64748b" }}>
235
+ <span style={{ animation: "dc-pulse 1.2s ease-in-out infinite" }}>· · ·</span>
236
+ </div>
237
+ )}
238
+ </AutoScroll>
239
+
240
+ {transcript.speaking && (
241
+ <div
242
+ className="flex items-center px-4 py-2 text-xs italic min-h-8"
243
+ style={{ background: "#111827", borderTop: "1px solid #1e293b", color: "#64748b" }}
244
+ >
245
+ <span
246
+ className="w-2.5 h-2.5 rounded-full inline-block mr-2"
247
+ style={{ background: "#22c55e", animation: "dc-pulse 1.5s ease-in-out infinite" }}
248
+ />
249
+ {transcript.text}
250
+ </div>
251
+ )}
252
+ </>
253
+ );
254
+ }
255
+
256
+ /**
257
+ * The live status readout, on its own subscription.
258
+ *
259
+ * `useSessionSelector` rather than a field off a whole-page `useSession()`: the
260
+ * header is the only thing here that cares about the session state, and reading
261
+ * it one level up dragged the incident board through every snapshot change.
262
+ */
263
+ function StatusReadout() {
264
+ const state = useSessionSelector((s) => s.state);
265
+ return (
266
+ <>
267
+ <span
268
+ className="w-2.5 h-2.5 rounded-full inline-block"
269
+ style={{
270
+ background: STATE_COLORS[state],
271
+ animation:
272
+ state === "listening"
273
+ ? "dc-pulse 1.5s ease-in-out infinite"
274
+ : state === "thinking"
275
+ ? "dc-pulse 0.8s ease-in-out infinite"
276
+ : "none",
277
+ }}
278
+ title={state}
279
+ />
280
+ <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()}
288
+ </span>
289
+ </>
290
+ );
291
+ }
292
+
293
+ /**
294
+ * The fatal-error banner.
295
+ *
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.
302
+ */
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
+ /** The shift controls. The one place a whole-session read is what is wanted:
318
+ * it needs `started`, `running` and three methods, and it is three buttons. */
319
+ function ShiftControls({ logged }: { logged: number }) {
143
320
  const session = useSession();
144
- // `speaking` and `text` in place of a `userTranscript !== null` check: `null`
145
- // is silence and `""` is speech detected with no words back yet, and reading
146
- // them as one falsy value hides the indicator at the start of every turn.
147
- const { speaking, text: userTranscript } = useUserTranscript();
321
+ return (
322
+ <div
323
+ className="flex items-center gap-2.5 px-4 py-3"
324
+ style={{ background: "#111827", borderTop: "1px solid #1e293b" }}
325
+ >
326
+ {!session.started ? (
327
+ <button
328
+ type="button"
329
+ className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
330
+ style={{ background: "#2563eb" }}
331
+ onClick={() => session.start()}
332
+ >
333
+ Start Dispatch
334
+ </button>
335
+ ) : (
336
+ <>
337
+ <button
338
+ type="button"
339
+ className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
340
+ style={{
341
+ background: session.running ? "#334155" : "#2563eb",
342
+ color: session.running ? "#e2e8f0" : "white",
343
+ }}
344
+ onClick={() => session.toggle()}
345
+ >
346
+ {session.running ? "Pause" : "Resume"}
347
+ </button>
348
+ {/* end() hangs up and flips `started` back, so the UI
349
+ returns to "Start Dispatch" and the next start is a
350
+ brand-new shift (fresh incident board, greeting
351
+ included). reset() would keep the call live — the
352
+ buttons never toggle back. */}
353
+ <button
354
+ type="button"
355
+ className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
356
+ style={{ background: "#dc2626" }}
357
+ onClick={() => session.end()}
358
+ >
359
+ End Shift
360
+ </button>
361
+ </>
362
+ )}
363
+ <div className="flex-1" />
364
+ <span className="text-[10px]" style={{ color: "#475569" }}>
365
+ {logged} incident{logged !== 1 ? "s" : ""} logged
366
+ </span>
367
+ </div>
368
+ );
369
+ }
370
+
371
+ function App() {
148
372
  // The agent's own board, projected by `syncState`. This replaced a
149
373
  // useState mirror that merged incident deltas out of tool events — the
150
374
  // projection is already the complete list, so there is nothing to merge.
375
+ //
376
+ // It is the ONLY subscription at this level: the session reads that used to
377
+ // sit beside it moved into the four components above, so a partial transcript
378
+ // no longer re-renders the incident cards.
151
379
  const dash = useAgentState(dashboardProjection);
152
380
 
153
381
  const incidentList = [...dash.incidents].reverse();
@@ -179,28 +407,7 @@ function App() {
179
407
  >
180
408
  <span style={{ color: "#3b82f6" }}>&#9670;</span>
181
409
  Dispatch Command Center
182
- <span
183
- className="w-2.5 h-2.5 rounded-full inline-block"
184
- style={{
185
- background: STATE_COLORS[session.state],
186
- animation:
187
- session.state === "listening"
188
- ? "dc-pulse 1.5s ease-in-out infinite"
189
- : session.state === "thinking"
190
- ? "dc-pulse 0.8s ease-in-out infinite"
191
- : "none",
192
- }}
193
- title={session.state}
194
- />
195
- <span className="text-[11px] font-normal normal-case" style={{ color: "#64748b" }}>
196
- {session.state === "listening"
197
- ? "LISTENING"
198
- : session.state === "thinking"
199
- ? "PROCESSING"
200
- : session.state === "speaking"
201
- ? "TRANSMITTING"
202
- : session.state.toUpperCase()}
203
- </span>
410
+ <StatusReadout />
204
411
  </div>
205
412
  <div className="flex gap-2 items-center">
206
413
  <span className="text-[10px] tracking-wider" style={{ color: "#64748b" }}>
@@ -229,106 +436,9 @@ function App() {
229
436
  className="flex flex-col overflow-hidden"
230
437
  style={{ borderRight: "1px solid #1e293b" }}
231
438
  >
232
- <AutoScroll
233
- scrollClassName="dc-messages overflow-y-auto"
234
- contentClassName="p-4 flex flex-col gap-2"
235
- >
236
- {session.messages.length === 0 && (
237
- <div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
238
- Dispatch Command Center standing by. Click START to begin operations.
239
- </div>
240
- )}
241
- {session.messages.map((m: ChatMessage, i: number) => (
242
- <div
243
- key={i}
244
- className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5"
245
- style={{
246
- lineHeight: 1.6,
247
- alignSelf: m.role === "assistant" ? "flex-start" : "flex-end",
248
- background: m.role === "assistant" ? "#1e293b" : "#172554",
249
- animation: "dc-slide-in 0.2s ease-out",
250
- borderLeft: m.role === "assistant" ? "3px solid #3b82f6" : "none",
251
- borderRight: m.role !== "assistant" ? "3px solid #22d3ee" : "none",
252
- }}
253
- >
254
- <div
255
- className="text-[10px] uppercase tracking-wider mb-1"
256
- style={{ color: "#64748b" }}
257
- >
258
- {m.role === "assistant" ? "DISPATCH" : "OPERATOR"}
259
- </div>
260
- {m.content}
261
- </div>
262
- ))}
263
- </AutoScroll>
264
-
265
- {speaking && (
266
- <div
267
- className="flex items-center px-4 py-2 text-xs italic min-h-8"
268
- style={{ background: "#111827", borderTop: "1px solid #1e293b", color: "#64748b" }}
269
- >
270
- <span
271
- className="w-2.5 h-2.5 rounded-full inline-block mr-2"
272
- style={{ background: "#22c55e", animation: "dc-pulse 1.5s ease-in-out infinite" }}
273
- />
274
- {userTranscript}
275
- </div>
276
- )}
277
- {session.error && (
278
- <div
279
- className="px-4 py-2 text-xs"
280
- style={{ background: "#450a0a", color: "#fca5a5", borderTop: "1px solid #991b1b" }}
281
- >
282
- ERROR: {session.error.message} ({session.error.code})
283
- </div>
284
- )}
285
-
286
- <div
287
- className="flex items-center gap-2.5 px-4 py-3"
288
- style={{ background: "#111827", borderTop: "1px solid #1e293b" }}
289
- >
290
- {!session.started ? (
291
- <button
292
- type="button"
293
- className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
294
- style={{ background: "#2563eb" }}
295
- onClick={() => session.start()}
296
- >
297
- Start Dispatch
298
- </button>
299
- ) : (
300
- <>
301
- <button
302
- type="button"
303
- className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
304
- style={{
305
- background: session.running ? "#334155" : "#2563eb",
306
- color: session.running ? "#e2e8f0" : "white",
307
- }}
308
- onClick={() => session.toggle()}
309
- >
310
- {session.running ? "Pause" : "Resume"}
311
- </button>
312
- {/* end() hangs up and flips `started` back, so the UI
313
- returns to "Start Dispatch" and the next start is a
314
- brand-new shift (fresh incident board, greeting
315
- included). reset() would keep the call live — the
316
- buttons never toggle back. */}
317
- <button
318
- type="button"
319
- className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
320
- style={{ background: "#dc2626" }}
321
- onClick={() => session.end()}
322
- >
323
- End Shift
324
- </button>
325
- </>
326
- )}
327
- <div className="flex-1" />
328
- <span className="text-[10px]" style={{ color: "#475569" }}>
329
- {incidentList.length} incident{incidentList.length !== 1 ? "s" : ""} logged
330
- </span>
331
- </div>
439
+ <Conversation />
440
+ <ErrorBanner />
441
+ <ShiftControls logged={incidentList.length} />
332
442
  </div>
333
443
 
334
444
  {/* Right: sidebar dashboard */}
@@ -1,7 +1,7 @@
1
1
  // ─── Types ───────────────────────────────────────────────────────────────────
2
2
 
3
3
  import type { DeepReadonly, ToolFailure } from "@alexkroman1/aai";
4
- import { pushCapped, sessionSlot } from "@alexkroman1/aai";
4
+ import { dialog, pushCapped, sessionSlot } from "@alexkroman1/aai";
5
5
 
6
6
  export const SEVERITIES = ["critical", "urgent", "moderate", "minor"] as const;
7
7
  export type Severity = (typeof SEVERITIES)[number];
@@ -218,6 +218,104 @@ export const dispatchSlot = sessionSlot("dispatch", createDefaultState, {
218
218
  /** The projection BOTH ends use: `syncState` on the agent, `useAgentState` in the client. */
219
219
  export const dashboardProjection = dispatchSlot.projection(dashboardView);
220
220
 
221
+ // ─── The call in hand ────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Where the dispatcher is on the call in hand, as a plain state map.
225
+ *
226
+ * Declared as a {@link DialogSpec} — `{ initial, states }`, each state carrying
227
+ * its own `instruction` and `on` map — rather than an XState machine. This
228
+ * dialog used exactly the four things a spec can say, and the `setup({ types:
229
+ * {} as { events: … } })` block it used to carry restated the four event names
230
+ * already written in the `on` maps below. The instruction is a TYPED field now
231
+ * instead of an untyped `meta` bag, which matters because a misspelling there
232
+ * produced a refusal with no recovery text and no error anywhere.
233
+ *
234
+ * The system prompt used to carry this as prose — "incident_triage: After
235
+ * creating, assess severity", "Critical incidents get immediate dispatch",
236
+ * "Never leave a critical incident without at least one resource dispatched" —
237
+ * which is advice to a model rather than a property of the agent. Two things
238
+ * change by declaring it.
239
+ *
240
+ * **The gate is real.** Six mutating tools take an incident id and did their own
241
+ * "does this exist" check through {@link findIncident}, whose answer on an empty
242
+ * board is `Incident INC-0001 not found` — a data answer to a positional
243
+ * question, and the wrong sentence for a dispatcher who has logged nothing yet.
244
+ * `when: "working"` is that check now, and the SDK's refusal names where the
245
+ * shift actually is and quotes the state's instruction.
246
+ *
247
+ * **The instruction rides every result.** A flow tool answers with the position
248
+ * it landed in, so `incident_create`'s result now ends with "confirm the
249
+ * severity and type with incident_triage" and `incident_triage`'s with "assign
250
+ * units" — the tool-by-tool sequencing the prompt was spending paragraphs on,
251
+ * arriving in the last thing the model reads before it speaks.
252
+ *
253
+ * **What this position is NOT is per-incident.** A flow is bound to a session
254
+ * and holds one position, and this board holds many incidents at once — so
255
+ * `working.monitoring` means "the incident this dispatcher last touched has
256
+ * units on it", not "every incident does". Per-incident status stays where it
257
+ * belongs, on {@link Incident.status}, and the tools stay addressed by id. That
258
+ * is also why `working` is a PARENT state: what every gated tool actually needs
259
+ * is "something has been logged on this shift", and the three children exist to
260
+ * carry the instruction for the step in front of the dispatcher rather than to
261
+ * gate anything.
262
+ *
263
+ * `LOGGED` is accepted from every state because a new 911 call is always legal —
264
+ * the one transition that is not a progression.
265
+ *
266
+ * `as const` is what keeps the `on` keys literal, so `DialogEvent` synthesizes
267
+ * the event union from them and a misspelled `send` is a compile error.
268
+ */
269
+ const callSpec = {
270
+ initial: "standby",
271
+ states: {
272
+ standby: {
273
+ instruction:
274
+ "Nothing is logged on this shift. Take the call — location first, then the " +
275
+ "nature of the emergency, then the caller's name and callback — and log it " +
276
+ "with incident_create.",
277
+ on: { LOGGED: "working" },
278
+ },
279
+ working: {
280
+ initial: "triaging",
281
+ // Declared on the PARENT so all three children inherit it, which is what
282
+ // makes a new call legal mid-incident without repeating the transition
283
+ // three times. The children re-target each other below.
284
+ on: { LOGGED: ".triaging" },
285
+ states: {
286
+ triaging: {
287
+ instruction:
288
+ "Logged, not yet triaged. Confirm or override the severity and type with " +
289
+ "incident_triage. For a critical call, dispatch first and triage alongside it.",
290
+ on: { TRIAGED: "dispatching", DISPATCHED: "monitoring", ESCALATED: "dispatching" },
291
+ },
292
+ dispatching: {
293
+ instruction:
294
+ "Triaged, nothing rolling. Assign units with resources_dispatch — never " +
295
+ "leave a critical incident without at least one.",
296
+ on: { DISPATCHED: "monitoring", TRIAGED: "dispatching", ESCALATED: "dispatching" },
297
+ },
298
+ monitoring: {
299
+ instruction:
300
+ "Units are assigned. Work it — status updates as they radio in, notes as " +
301
+ "the picture changes, escalate if it outgrows the response, and close it " +
302
+ "with incident_update_status.",
303
+ on: { TRIAGED: "dispatching", DISPATCHED: "monitoring", ESCALATED: "dispatching" },
304
+ },
305
+ },
306
+ },
307
+ },
308
+ } as const;
309
+
310
+ /**
311
+ * The flow. Its own slot key beside {@link dispatchSlot}: the flow holds the
312
+ * POSITION and the board holds the incidents, because the position is one fact
313
+ * about the conversation and the board is many facts about the world. One tool
314
+ * call moves both — every converted tool opens `dispatchSlot.update` inside its
315
+ * `execute` and lets the flow's own `send` follow it.
316
+ */
317
+ export const callFlow = dialog("call", callSpec);
318
+
221
319
  /**
222
320
  * The board as a READ hands it out: deep-frozen, and typed to say so.
223
321
  *
@@ -2,11 +2,13 @@ You are the AI-powered Emergency Dispatch Command Center. You coordinate emergen
2
2
 
3
3
  Your role combines call-taker, dispatcher, and incident commander. You speak like an experienced dispatcher: calm, precise, and authoritative. Never panic. Use brevity codes and dispatch terminology naturally.
4
4
 
5
+ WHERE THE CALL IS: every incident and resource tool answers with the position the call is in and what that position expects next — read it. Those tools refuse outright until something has been logged, and the refusal tells you what to do first. Nothing here has to be remembered between turns.
6
+
5
7
  Your tools:
6
8
 
7
9
  INCIDENT MANAGEMENT:
8
10
  - incident_create: Log a new incident. Ask for location first, then nature of emergency, then caller info. Speed matters for critical calls.
9
- - incident_triage: After creating, assess severity. The system recommends severity, type, and protocols. Review and confirm or override.
11
+ - incident_triage: Assess severity. The system recommends severity, type, and protocols. Review and confirm or override.
10
12
  - incident_update_status: Move incidents through the workflow (en_route, on_scene, resolved, escalated).
11
13
  - incident_get: Get details on a specific incident.
12
14
  - incident_escalate: Escalate when an incident exceeds current capacity or severity increases.
@@ -1,25 +1,28 @@
1
1
  import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { dispatchSlot, findIncident, logEvent } from "../shared.ts";
3
+ import { callFlow, dispatchSlot, findIncident, logEvent } from "../shared.ts";
4
4
 
5
- export default dispatchSlot.updateTool({
5
+ /** Gated on `working` — there is nothing to annotate before anything is logged. */
6
+ export default callFlow.tool({
6
7
  description: "Add a situational update note to an incident's timeline.",
8
+ when: "working",
7
9
  inputSchema: z.object({
8
10
  incidentId: z.string().max(20).describe("The incident ID"),
9
11
  note: z.string().max(1000).describe("The note to add"),
10
12
  source: z.string().max(100).describe("Who reported this — unit callsign or caller").optional(),
11
13
  }),
12
- execute(args, state) {
13
- const inc = findIncident(state, args.incidentId);
14
- if (isToolFailure(inc)) return inc;
14
+ execute: (args, ctx) =>
15
+ dispatchSlot.update(ctx, (state) => {
16
+ const inc = findIncident(state, args.incidentId);
17
+ if (isToolFailure(inc)) return inc;
15
18
 
16
- const entry = args.source ? `[${args.source}] ${args.note}` : args.note;
17
- logEvent(inc, entry);
19
+ const entry = args.source ? `[${args.source}] ${args.note}` : args.note;
20
+ logEvent(inc, entry);
18
21
 
19
- return {
20
- incidentId: args.incidentId,
21
- noteAdded: entry,
22
- timelineEntries: inc.timeline.length,
23
- };
24
- },
22
+ return {
23
+ incidentId: args.incidentId,
24
+ noteAdded: entry,
25
+ timelineEntries: inc.timeline.length,
26
+ };
27
+ }),
25
28
  });
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import {
3
3
  calculateTriageScore,
4
+ callFlow,
4
5
  createIncident,
5
6
  dispatchSlot,
6
7
  getApplicableProtocols,
@@ -10,6 +11,18 @@ import {
10
11
  resourceBrief,
11
12
  } from "../shared.ts";
12
13
 
14
+ /**
15
+ * **Not a `callFlow.tool`, deliberately.** A new 911 call is legal in every
16
+ * state, so a `when` listing all of them would be a gate that gates nothing —
17
+ * the same call `plan-and-execute`'s `start_plan` makes. It sends `LOGGED`
18
+ * itself, which is what `dialog.send` is public for, and reports the position it
19
+ * landed in so the model reads "confirm the severity and type" as part of this
20
+ * result rather than having to remember it from the prompt.
21
+ *
22
+ * The `send` sits INSIDE the board's mutation window, which is safe because it
23
+ * is a different slot: the re-entrancy guard is per slot, and neither window
24
+ * spans an await, so the whole call commits atomically.
25
+ */
13
26
  export default dispatchSlot.updateTool({
14
27
  description: "Create a new incident from an incoming emergency call.",
15
28
  inputSchema: z.object({
@@ -29,7 +42,7 @@ export default dispatchSlot.updateTool({
29
42
  .describe("Known hazards: fire, chemical, electrical, structural, weapons")
30
43
  .optional(),
31
44
  }),
32
- execute(args, state) {
45
+ execute(args, state, ctx) {
33
46
  const recSeverity = recommendSeverity(args.description);
34
47
  const recType = recommendType(args.description);
35
48
  const triageScore = calculateTriageScore(
@@ -62,6 +75,11 @@ export default dispatchSlot.updateTool({
62
75
 
63
76
  return {
64
77
  incidentId: id,
78
+ // Spread the position rather than renaming its fields: this tool is
79
+ // ungated, so the `{ state, instruction }` a gated tool's result carries
80
+ // has to come from here — and under two different key sets the model
81
+ // reads its own position two different ways within one agent.
82
+ ...callFlow.send(ctx, { type: "LOGGED" }),
65
83
  recommendedSeverity: recSeverity,
66
84
  recommendedType: recType,
67
85
  triageScore,