@alexkroman1/aai-cli 5.13.2 → 6.1.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 (236) hide show
  1. package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
  2. package/dist/_agent.d.ts +18 -0
  3. package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
  4. package/dist/_api-client.d.ts +39 -0
  5. package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
  6. package/dist/_bundler.d.ts +43 -5
  7. package/dist/_cli-common.d.ts +67 -3
  8. package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
  9. package/dist/_deploy.d.ts +2 -5
  10. package/dist/_dev-restart.d.ts +6 -1
  11. package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
  12. package/dist/_dev-server.d.ts +30 -2
  13. package/dist/_e2e-test-utils.d.ts +80 -0
  14. package/dist/_fault-mode.d.ts +175 -0
  15. package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
  16. package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
  17. package/dist/_output.d.ts +9 -2
  18. package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
  19. package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
  20. package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
  21. package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
  22. package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
  23. package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
  24. package/dist/_utils.d.ts +18 -2
  25. package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
  26. package/dist/_vite-env.d.ts +30 -0
  27. package/dist/build-BO1Ni6oJ.mjs +62 -0
  28. package/dist/build.d.ts +19 -0
  29. package/dist/cli-workflow.d.ts +10 -0
  30. package/dist/cli.d.ts +1 -2
  31. package/dist/cli.mjs +406 -224
  32. package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
  33. package/dist/client-bundler.mjs +1 -1
  34. package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
  35. package/dist/delete.d.ts +2 -8
  36. package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
  37. package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
  38. package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
  39. package/dist/eject.d.ts +30 -3
  40. package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
  41. package/dist/init.d.ts +11 -0
  42. package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
  43. package/dist/project-config.mjs +1 -1
  44. package/dist/scaffold/CLAUDE.md +498 -91
  45. package/dist/scaffold/package.json +11 -8
  46. package/dist/scaffold/pnpm-workspace.yaml +32 -0
  47. package/dist/scaffold/server.mjs +53 -57
  48. package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
  49. package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
  50. package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
  51. package/dist/templates/code-interpreter/agent.ts +0 -2
  52. package/dist/templates/dispatch-center/agent.test.ts +21 -30
  53. package/dist/templates/dispatch-center/agent.ts +4 -31
  54. package/dist/templates/dispatch-center/client.tsx +41 -28
  55. package/dist/templates/dispatch-center/shared.ts +73 -66
  56. package/dist/templates/dispatch-center/tools/incident_add_note.ts +13 -15
  57. package/dist/templates/dispatch-center/tools/incident_create.ts +42 -45
  58. package/dist/templates/dispatch-center/tools/incident_escalate.ts +60 -62
  59. package/dist/templates/dispatch-center/tools/incident_get.ts +10 -6
  60. package/dist/templates/dispatch-center/tools/incident_triage.ts +44 -46
  61. package/dist/templates/dispatch-center/tools/incident_update_status.ts +40 -42
  62. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +3 -6
  63. package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +21 -24
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +53 -55
  66. package/dist/templates/dispatch-center/tools/resources_get_available.ts +3 -5
  67. package/dist/templates/dispatch-center/tools/resources_update_status.ts +40 -43
  68. package/dist/templates/embedded-assets/agent.test.ts +83 -0
  69. package/dist/templates/embedded-assets/agent.ts +1 -48
  70. package/dist/templates/embedded-assets/shared.ts +17 -0
  71. package/dist/templates/embedded-assets/system-prompt.md +13 -0
  72. package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
  73. package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
  74. package/dist/templates/health-assistant/agent.test.ts +169 -0
  75. package/dist/templates/health-assistant/agent.ts +1 -166
  76. package/dist/templates/health-assistant/fda.ts +100 -0
  77. package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
  78. package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
  79. package/dist/templates/infocom-adventure/agent.test.ts +205 -0
  80. package/dist/templates/infocom-adventure/agent.ts +3 -106
  81. package/dist/templates/infocom-adventure/client.tsx +32 -28
  82. package/dist/templates/infocom-adventure/shared.ts +36 -16
  83. package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
  84. package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
  85. package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
  86. package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
  87. package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
  88. package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
  89. package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
  90. package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
  91. package/dist/templates/link-digest/agent.test.ts +223 -0
  92. package/dist/templates/link-digest/agent.ts +79 -0
  93. package/dist/templates/link-digest/client.tsx +146 -0
  94. package/dist/templates/link-digest/workflows/digest.ts +248 -0
  95. package/dist/templates/math-buddy/agent.ts +0 -2
  96. package/dist/templates/night-owl/agent.test.ts +80 -0
  97. package/dist/templates/night-owl/agent.ts +1 -68
  98. package/dist/templates/night-owl/system-prompt.md +10 -0
  99. package/dist/templates/night-owl/tools/recommend.ts +64 -0
  100. package/dist/templates/personal-finance/agent.ts +0 -2
  101. package/dist/templates/pizza-ordering/agent.test.ts +59 -58
  102. package/dist/templates/pizza-ordering/agent.ts +10 -159
  103. package/dist/templates/pizza-ordering/client.tsx +4 -4
  104. package/dist/templates/pizza-ordering/shared.ts +44 -27
  105. package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
  106. package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
  107. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
  108. package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
  109. package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
  110. package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
  111. package/dist/templates/plan-and-execute/agent.test.ts +411 -0
  112. package/dist/templates/plan-and-execute/agent.ts +23 -0
  113. package/dist/templates/plan-and-execute/client.tsx +115 -0
  114. package/dist/templates/plan-and-execute/graph.ts +178 -0
  115. package/dist/templates/plan-and-execute/prompts.ts +110 -0
  116. package/dist/templates/plan-and-execute/shared.ts +161 -0
  117. package/dist/templates/plan-and-execute/system-prompt.md +18 -0
  118. package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
  119. package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
  120. package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
  121. package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
  122. package/dist/templates/recap-workflow/agent.test.ts +762 -0
  123. package/dist/templates/recap-workflow/agent.ts +95 -0
  124. package/dist/templates/recap-workflow/shared.ts +43 -0
  125. package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
  126. package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
  127. package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
  128. package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
  129. package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
  130. package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
  131. package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
  132. package/dist/templates/redline/agent.test.ts +222 -0
  133. package/dist/templates/redline/agent.ts +89 -0
  134. package/dist/templates/redline/client.tsx +184 -0
  135. package/dist/templates/redline/workflows/prompts.ts +68 -0
  136. package/dist/templates/redline/workflows/redline.ts +258 -0
  137. package/dist/templates/research-workflow/agent.test.ts +525 -0
  138. package/dist/templates/research-workflow/agent.ts +94 -0
  139. package/dist/templates/research-workflow/shared.ts +35 -0
  140. package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
  141. package/dist/templates/research-workflow/tools/request_research.ts +33 -0
  142. package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
  143. package/dist/templates/research-workflow/tools/research_status.ts +40 -0
  144. package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
  145. package/dist/templates/research-workflow/workflows/research.ts +558 -0
  146. package/dist/templates/retail/agent.test.ts +319 -256
  147. package/dist/templates/retail/agent.ts +6 -46
  148. package/dist/templates/retail/authenticate.ts +2 -2
  149. package/dist/templates/retail/client.tsx +45 -24
  150. package/dist/templates/retail/registry.test.ts +40 -25
  151. package/dist/templates/retail/resolve.test.ts +17 -16
  152. package/dist/templates/retail/resolve.ts +32 -67
  153. package/dist/templates/retail/shared.test.ts +21 -19
  154. package/dist/templates/retail/shared.ts +61 -23
  155. package/dist/templates/retail/store.test.ts +47 -52
  156. package/dist/templates/retail/store.ts +92 -79
  157. package/dist/templates/retail/swap.test.ts +20 -17
  158. package/dist/templates/retail/swap.ts +7 -14
  159. package/dist/templates/retail/tools/cancel_pending_order.ts +7 -11
  160. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +26 -14
  161. package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
  162. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
  163. package/dist/templates/retail/tools/get_item_details.ts +7 -8
  164. package/dist/templates/retail/tools/get_order_details.ts +7 -8
  165. package/dist/templates/retail/tools/get_product_details.ts +6 -8
  166. package/dist/templates/retail/tools/get_user_details.ts +6 -8
  167. package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
  168. package/dist/templates/retail/tools/modify_pending_order_address.ts +6 -10
  169. package/dist/templates/retail/tools/modify_pending_order_items.ts +8 -20
  170. package/dist/templates/retail/tools/modify_pending_order_payment.ts +7 -13
  171. package/dist/templates/retail/tools/modify_user_address.ts +6 -10
  172. package/dist/templates/retail/tools/return_delivered_order_items.ts +7 -13
  173. package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
  174. package/dist/templates/solo-rpg/agent.test.ts +169 -54
  175. package/dist/templates/solo-rpg/agent.ts +4 -22
  176. package/dist/templates/solo-rpg/client.tsx +19 -6
  177. package/dist/templates/solo-rpg/shared.ts +20 -20
  178. package/dist/templates/solo-rpg/tools/action_roll.ts +3 -5
  179. package/dist/templates/solo-rpg/tools/burn_momentum.ts +3 -5
  180. package/dist/templates/solo-rpg/tools/check_state.ts +7 -7
  181. package/dist/templates/solo-rpg/tools/load_game.ts +3 -3
  182. package/dist/templates/solo-rpg/tools/oracle.ts +19 -12
  183. package/dist/templates/solo-rpg/tools/save_game.ts +11 -8
  184. package/dist/templates/solo-rpg/tools/setup_character.ts +3 -3
  185. package/dist/templates/solo-rpg/tools/update_state.ts +13 -13
  186. package/dist/templates/support-line/agent.test.ts +343 -0
  187. package/dist/templates/support-line/agent.ts +27 -0
  188. package/dist/templates/support-line/client.tsx +122 -0
  189. package/dist/templates/support-line/graph.ts +224 -0
  190. package/dist/templates/support-line/knowledge.json +65 -0
  191. package/dist/templates/support-line/prompts.ts +95 -0
  192. package/dist/templates/support-line/shared.ts +240 -0
  193. package/dist/templates/support-line/system-prompt.md +17 -0
  194. package/dist/templates/support-line/tools/answer_question.ts +72 -0
  195. package/dist/templates/support-line/tools/list_topics.ts +22 -0
  196. package/dist/templates/support-line/tools/log_ticket.ts +30 -0
  197. package/dist/templates/transcription-workflow/agent.test.ts +563 -0
  198. package/dist/templates/transcription-workflow/agent.ts +92 -0
  199. package/dist/templates/transcription-workflow/client.tsx +234 -0
  200. package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
  201. package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
  202. package/dist/templates/travel-concierge/agent.test.ts +338 -0
  203. package/dist/templates/travel-concierge/agent.ts +29 -0
  204. package/dist/templates/travel-concierge/client.tsx +161 -0
  205. package/dist/templates/travel-concierge/routing.ts +138 -0
  206. package/dist/templates/travel-concierge/shared.ts +547 -0
  207. package/dist/templates/travel-concierge/system-prompt.md +24 -0
  208. package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
  209. package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
  210. package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
  211. package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
  212. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
  213. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
  214. package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
  215. package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
  216. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
  217. package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
  218. package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
  219. package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
  220. package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
  221. package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
  222. package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
  223. package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
  224. package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
  225. package/dist/templates/web-researcher/agent.ts +0 -2
  226. package/dist/templates/web-researcher/system-prompt.md +10 -0
  227. package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
  228. package/dist/typecheck.mjs +17 -15
  229. package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
  230. package/dist/worker-bundler.d.ts +14 -0
  231. package/dist/worker-bundler.mjs +1 -108
  232. package/dist/workflow-B-O97AqI.mjs +125 -0
  233. package/dist/workflow-bundler.d.ts +133 -0
  234. package/dist/workflow.d.ts +67 -0
  235. package/package.json +13 -9
  236. package/dist/build-DQJ1ZSm9.mjs +0 -39
@@ -1,9 +1,14 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { ChatMessage } from "@alexkroman1/aai-ui";
3
- import { client, useAgentState, useSession } from "@alexkroman1/aai-ui";
4
- import { useEffect, useRef } from "react";
2
+ import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
3
+ import {
4
+ AutoScroll,
5
+ client,
6
+ useAgentState,
7
+ useSession,
8
+ useUserTranscript,
9
+ } from "@alexkroman1/aai-ui";
5
10
  import type { DashboardView, DispatchState, IncidentSummary, Severity, Status } from "./shared.ts";
6
- import { dashboardView } from "./shared.ts";
11
+ import { dashboardView, dispatchSlot } from "./shared.ts";
7
12
 
8
13
  const CSS = `
9
14
  @keyframes dc-pulse {
@@ -54,19 +59,26 @@ const statusColors: Record<string, string> = {
54
59
 
55
60
  // The board before the first tool call — derived from the projection so a
56
61
  // new DashboardView field can't silently miss the pre-first-tool-call render.
57
- const EMPTY_DASH = dashboardView({});
62
+ const EMPTY_DASH: DashboardView = dispatchSlot.projection(dashboardView)(undefined);
58
63
 
59
- function stateColor(state: string): string {
60
- return state === "listening"
61
- ? "#22c55e"
62
- : state === "thinking"
63
- ? "#eab308"
64
- : state === "speaking"
65
- ? "#3b82f6"
66
- : state === "ready"
67
- ? "#22c55e"
68
- : "#6b7280";
69
- }
64
+ // The dot's colour per session state, as an EXHAUSTIVE map rather than an
65
+ // if-chain with a grey default.
66
+ //
67
+ // The palette is this template's own — every client here paints the same six
68
+ // states in its own colours, so the SDK has nothing to share but the union. What
69
+ // the SDK does own is `AgentState`, and `satisfies Record<AgentState, string>`
70
+ // is what borrows it: a state added there stops compiling here, where the
71
+ // `state === ""` chain this replaced answered a new state with a silent grey
72
+ // badge in three separate files and no way to notice.
73
+ const STATE_COLORS = {
74
+ disconnected: "#6b7280",
75
+ connecting: "#6b7280",
76
+ ready: "#22c55e",
77
+ listening: "#22c55e",
78
+ thinking: "#eab308",
79
+ speaking: "#3b82f6",
80
+ error: "#6b7280",
81
+ } satisfies Record<AgentState, string>;
70
82
 
71
83
  function Panel({ title, children }: { title: string; children: React.ReactNode }) {
72
84
  return (
@@ -133,15 +145,14 @@ function IncidentCard({ inc }: { inc: IncidentSummary }) {
133
145
 
134
146
  function App() {
135
147
  const session = useSession();
136
- const messagesEndRef = useRef<HTMLDivElement>(null);
148
+ // `speaking` and `text` in place of a `userTranscript !== null` check: `null`
149
+ // is silence and `""` is speech detected with no words back yet, and reading
150
+ // them as one falsy value hides the indicator at the start of every turn.
151
+ const { speaking, text: userTranscript } = useUserTranscript();
137
152
  // The agent's own board, projected by `syncState`. This replaced a
138
153
  // useState mirror that merged incident deltas out of tool events — the
139
154
  // projection is already the complete list, so there is nothing to merge.
140
- const dash = useAgentState<DashboardView>() ?? EMPTY_DASH;
141
-
142
- useEffect(() => {
143
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
144
- }, [session.messages]);
155
+ const dash = useAgentState<DashboardView>(EMPTY_DASH);
145
156
 
146
157
  const incidentList = [...dash.incidents].reverse();
147
158
  const activeIncidents = incidentList.filter((i) => i.status !== "resolved");
@@ -175,7 +186,7 @@ function App() {
175
186
  <span
176
187
  className="w-2.5 h-2.5 rounded-full inline-block"
177
188
  style={{
178
- background: stateColor(session.state),
189
+ background: STATE_COLORS[session.state],
179
190
  animation:
180
191
  session.state === "listening"
181
192
  ? "dc-pulse 1.5s ease-in-out infinite"
@@ -222,7 +233,10 @@ function App() {
222
233
  className="flex flex-col overflow-hidden"
223
234
  style={{ borderRight: "1px solid #1e293b" }}
224
235
  >
225
- <div className="dc-messages flex-1 overflow-y-auto p-4 flex flex-col gap-2">
236
+ <AutoScroll
237
+ scrollClassName="dc-messages overflow-y-auto"
238
+ contentClassName="p-4 flex flex-col gap-2"
239
+ >
226
240
  {session.messages.length === 0 && (
227
241
  <div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
228
242
  Dispatch Command Center standing by. Click START to begin operations.
@@ -250,10 +264,9 @@ function App() {
250
264
  {m.content}
251
265
  </div>
252
266
  ))}
253
- <div ref={messagesEndRef} />
254
- </div>
267
+ </AutoScroll>
255
268
 
256
- {session.userTranscript !== null && (
269
+ {speaking && (
257
270
  <div
258
271
  className="flex items-center px-4 py-2 text-xs italic min-h-8"
259
272
  style={{ background: "#111827", borderTop: "1px solid #1e293b", color: "#64748b" }}
@@ -262,7 +275,7 @@ function App() {
262
275
  className="w-2.5 h-2.5 rounded-full inline-block mr-2"
263
276
  style={{ background: "#22c55e", animation: "dc-pulse 1.5s ease-in-out infinite" }}
264
277
  />
265
- {session.userTranscript === "" ? "..." : session.userTranscript}
278
+ {userTranscript}
266
279
  </div>
267
280
  )}
268
281
  {session.error && (
@@ -1,7 +1,7 @@
1
1
  // ─── Types ───────────────────────────────────────────────────────────────────
2
2
 
3
- import type { ToolContext } from "@alexkroman1/aai";
4
- import { createKeyedLock, withLock } from "@alexkroman1/aai";
3
+ import type { DeepReadonly, ToolFailure } from "@alexkroman1/aai";
4
+ import { 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];
@@ -101,7 +101,7 @@ export interface IncidentSummary {
101
101
  location: string;
102
102
  }
103
103
 
104
- export function incidentSummary(inc: Incident): IncidentSummary {
104
+ export function incidentSummary(inc: DeepReadonly<Incident>): IncidentSummary {
105
105
  return { id: inc.id, severity: inc.severity, status: inc.status, location: inc.location };
106
106
  }
107
107
 
@@ -111,22 +111,16 @@ export interface DashboardView {
111
111
  incidents: IncidentSummary[];
112
112
  }
113
113
 
114
- /** The `syncState` projection — the whole contract with client.tsx. */
115
- export function dashboardView(state: StateSlot): DashboardView {
116
- const dispatch = state.dispatch;
114
+ /** The `syncState` projection — the whole contract with client.tsx. Takes the
115
+ * board itself, not the slot: `dispatchSlot.projection` supplies a real one
116
+ * even before the first tool call. */
117
+ export function dashboardView(state: FrozenDispatchState): DashboardView {
117
118
  return {
118
- systemAlertLevel: dispatch?.alertLevel ?? "green",
119
- incidents: Object.values(dispatch?.incidents ?? {}).map(incidentSummary),
119
+ systemAlertLevel: state.alertLevel,
120
+ incidents: Object.values(state.incidents).map(incidentSummary),
120
121
  };
121
122
  }
122
123
 
123
- // ─── State helpers ───────────────────────────────────────────────────────────
124
- // The dispatch board lives in `ctx.state`, the agent's per-session mutable
125
- // state — sessions must not see each other's incidents, and ctx.state gives
126
- // that isolation by construction. Nothing here needs to outlive the session.
127
-
128
- export type StateSlot = { dispatch?: DispatchState };
129
-
130
124
  // ─── Resource generation ────────────────────────────────────────────────────
131
125
 
132
126
  const RESOURCE_DEFS: [string, Resource["type"], string, string[]][] = [
@@ -167,15 +161,10 @@ export function createDefaultState(): DispatchState {
167
161
  };
168
162
  }
169
163
 
170
- /** The session's live dispatch state. Mutations to the returned object
171
- * stick it is the object stored in `ctx.state`. */
172
- export function getState(ctx: ToolContext): DispatchState {
173
- const slot = ctx.state as StateSlot;
174
- slot.dispatch ??= createDefaultState();
175
- return slot.dispatch;
176
- }
177
-
178
- // ─── Serialized state updates ────────────────────────────────────────────────
164
+ // ─── State helpers ───────────────────────────────────────────────────────────
165
+ // The dispatch board lives in one `sessionSlot`, keyed per session — sessions
166
+ // must not see each other's incidents, and a slot gives that isolation by
167
+ // construction.
179
168
 
180
169
  /**
181
170
  * Growth caps. The whole dispatch state is one object whose summaries feed
@@ -183,8 +172,10 @@ export function getState(ctx: ToolContext): DispatchState {
183
172
  * timelines must be pruned or a long session's payloads grow without bound.
184
173
  */
185
174
  const MAX_RESOLVED_KEPT = 10;
186
- const MAX_TIMELINE_ENTRIES = 50;
175
+ export const MAX_TIMELINE_ENTRIES = 50;
187
176
 
177
+ /** Resolved incidents only — the timeline cap is held on append by
178
+ * `logEvent`, so nothing here has to re-trim it. */
188
179
  function pruneState(state: DispatchState): void {
189
180
  const resolved = Object.values(state.incidents)
190
181
  .filter((i) => i.status === "resolved")
@@ -192,46 +183,48 @@ function pruneState(state: DispatchState): void {
192
183
  for (const inc of resolved.slice(MAX_RESOLVED_KEPT)) {
193
184
  delete state.incidents[inc.id];
194
185
  }
195
- for (const inc of Object.values(state.incidents)) {
196
- if (inc.timeline.length > MAX_TIMELINE_ENTRIES) {
197
- inc.timeline = inc.timeline.slice(-MAX_TIMELINE_ENTRIES);
198
- }
199
- }
200
186
  }
201
187
 
202
- const sessionLock = createKeyedLock();
203
-
204
188
  /**
205
- * Serialized update of the session's dispatch state.
189
+ * The session's dispatch board, as one typed slot.
206
190
  *
207
- * The LLM loop executes parallel tool calls concurrently. The state lives in
208
- * `ctx.state` now (one shared object, no snapshot/save round-trip), but a
209
- * mutator may be async, and two interleaving async mutators can each observe
210
- * the other's half-applied changes. Holding the session's key makes each
211
- * update run against the previous one's finished result. It also centralizes
212
- * the shared bookkeeping every mutating tool needs: pruning and alert-level
213
- * recalculation.
191
+ * Every tool here is declared through the slot rather than reaching for it from
192
+ * inside a `tool()` body, and WHICH HALF is the whole decision: `updateTool`
193
+ * runs the body inside the mutation window and hands it a draft that is stored
194
+ * when the body returns, `tool` hands it the deep-frozen stored value. That
195
+ * makes "does this tool write?" a property of the declaration a reader can see,
196
+ * and choosing wrong a compile error rather than a `TypeError` on the first
197
+ * call.
214
198
  *
215
- * `createKeyedLock` rather than a hand-rolled promise chain per session: the
216
- * SDK's drops each key's entry once its chain drains (so a long-running agent
217
- * does not accumulate one per session id) and does it by ownership, which is
218
- * the part a hand-rolled chain gets wrong.
199
+ * The window is SYNCHRONOUS, which is what makes a read-modify-write atomic
200
+ * with no lock the LLM loop runs a step's tool calls concurrently. It does
201
+ * NOT serialize anything (an earlier version of this comment claimed it did):
202
+ * there is nothing to serialize, because no other JS turn can interleave with
203
+ * a synchronous body. `updateTool` is what enforces the rule, refusing a
204
+ * thenable body by name.
219
205
  *
220
- * There is no post-save callback: pushing the board to the client used to
221
- * need one, and `syncState` now does it after every tool call.
206
+ * `after` is the bookkeeping every mutating tool needs and none of them should
207
+ * have to remember pruning resolved incidents, and recalculating the alert
208
+ * level the dashboard reads. Declaring it here rather than at each call site is
209
+ * the point: a new mutating tool gets both for free.
222
210
  */
223
- export function updateState<R>(
224
- ctx: ToolContext,
225
- mutator: (state: DispatchState) => R | Promise<R>,
226
- ): Promise<R> {
227
- return withLock(sessionLock, ctx.sessionId, async () => {
228
- const state = getState(ctx);
229
- const result = await mutator(state);
211
+ export const dispatchSlot = sessionSlot("dispatch", createDefaultState, {
212
+ after: (state) => {
230
213
  pruneState(state);
231
214
  recalculateAlertLevel(state);
232
- return result;
233
- });
234
- }
215
+ },
216
+ });
217
+
218
+ /**
219
+ * The board as a READ hands it out: deep-frozen, and typed to say so.
220
+ *
221
+ * The pure helpers below take this rather than {@link DispatchState}, which is
222
+ * the widening a deep-readonly slot forces and the reason it is worth doing: a
223
+ * mutable board still satisfies it, so a helper called with an `updateTool`
224
+ * draft is unaffected, while a helper that WOULD have mutated stops compiling
225
+ * instead of throwing at its first call in production.
226
+ */
227
+ export type FrozenDispatchState = DeepReadonly<DispatchState>;
235
228
 
236
229
  // ─── Incident helpers ────────────────────────────────────────────────────────
237
230
 
@@ -263,32 +256,46 @@ export function createIncident(state: DispatchState, overrides: Partial<Incident
263
256
  return incident;
264
257
  }
265
258
 
266
- export function findIncident(
267
- state: DispatchState,
259
+ /**
260
+ * One incident by id, or the failure the model should read.
261
+ *
262
+ * Generic over the incident's own type rather than pinned to {@link Incident},
263
+ * so ONE lookup serves both halves of the slot: a mutating tool passes its
264
+ * draft and gets a mutable incident back to change, while `incident_get` passes
265
+ * what `dispatchSlot.tool` handed it — deep-frozen — and gets a readonly one.
266
+ * Pinning it would have made the read path the odd one out, and its way out
267
+ * would have been a cast.
268
+ */
269
+ export function findIncident<I extends DeepReadonly<Incident>>(
270
+ state: { readonly incidents: { readonly [id: string]: I } },
268
271
  incidentId: string,
269
- ): Incident | { error: string } {
272
+ ): I | ToolFailure {
270
273
  return state.incidents[incidentId] ?? { error: `Incident ${incidentId} not found` };
271
274
  }
272
275
 
273
- /** Append a timeline entry and touch `updatedAt`. */
276
+ /** Append a timeline entry and touch `updatedAt`, holding
277
+ * {@link MAX_TIMELINE_ENTRIES}. Capped on APPEND rather than swept later, so
278
+ * a single long-running incident cannot outgrow the payload between sweeps. */
274
279
  export function logEvent(inc: Incident, event: string): void {
275
280
  const time = Date.now();
276
- inc.timeline.push({ time, event });
281
+ pushCapped(inc.timeline, { time, event }, MAX_TIMELINE_ENTRIES);
277
282
  inc.updatedAt = time;
278
283
  }
279
284
 
280
285
  /** Minutes since the incident was created, rounded. */
281
- export function incidentAgeMinutes(inc: Incident): number {
286
+ export function incidentAgeMinutes(inc: DeepReadonly<Incident>): number {
282
287
  return Math.round((Date.now() - inc.createdAt) / 60_000);
283
288
  }
284
289
 
285
290
  /** A resource as tool results describe it to the LLM. */
286
- export function resourceBrief(r: Resource): {
291
+ export function resourceBrief(r: DeepReadonly<Resource>): {
287
292
  callsign: string;
288
293
  type: Resource["type"];
289
294
  capabilities: string[];
290
295
  } {
291
- return { callsign: r.callsign, type: r.type, capabilities: r.capabilities };
296
+ // `capabilities` is COPIED: a brief is a fresh object handed to the LLM, and
297
+ // the source list belongs either to a frozen slot value or to a live draft.
298
+ return { callsign: r.callsign, type: r.type, capabilities: [...r.capabilities] };
292
299
  }
293
300
 
294
301
  /**
@@ -296,7 +303,7 @@ export function resourceBrief(r: Resource): {
296
303
  * resources have been released (and possibly reassigned), so escalating,
297
304
  * re-resolving, or dispatching to it would corrupt resource assignments.
298
305
  */
299
- export function assertNotResolved(inc: Incident, action: string): { error: string } | null {
306
+ export function assertNotResolved(inc: DeepReadonly<Incident>, action: string): ToolFailure | null {
300
307
  if (inc.status === "resolved") {
301
308
  return {
302
309
  error: `Incident ${inc.id} is resolved — cannot ${action}. Create a new incident if the situation has reopened.`,
@@ -602,7 +609,7 @@ export function recommendResources(
602
609
  /** Fraction of resources not currently available, 0–1. The alert level and
603
610
  * the dashboard's displayed utilization both derive from this, so they can
604
611
  * never disagree about what "utilization" means. */
605
- export function resourceUtilization(state: DispatchState): number {
612
+ export function resourceUtilization(state: FrozenDispatchState): number {
606
613
  const available = state.resources.filter((r) => r.status === "available").length;
607
614
  return 1 - available / state.resources.length;
608
615
  }
@@ -1,27 +1,25 @@
1
- import { tool } from "@alexkroman1/aai";
1
+ import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { findIncident, logEvent, updateState } from "../shared.ts";
3
+ import { dispatchSlot, findIncident, logEvent } from "../shared.ts";
4
4
 
5
- export const incidentAddNote = tool({
5
+ export default dispatchSlot.updateTool({
6
6
  description: "Add a situational update note to an incident's timeline.",
7
7
  inputSchema: z.object({
8
8
  incidentId: z.string().max(20).describe("The incident ID"),
9
9
  note: z.string().max(1000).describe("The note to add"),
10
10
  source: z.string().max(100).describe("Who reported this — unit callsign or caller").optional(),
11
11
  }),
12
- async execute(args, ctx) {
13
- return updateState(ctx, (state) => {
14
- const inc = findIncident(state, args.incidentId);
15
- if ("error" in inc) return inc;
12
+ execute(args, state) {
13
+ const inc = findIncident(state, args.incidentId);
14
+ if (isToolFailure(inc)) return inc;
16
15
 
17
- const entry = args.source ? `[${args.source}] ${args.note}` : args.note;
18
- logEvent(inc, entry);
16
+ const entry = args.source ? `[${args.source}] ${args.note}` : args.note;
17
+ logEvent(inc, entry);
19
18
 
20
- return {
21
- incidentId: args.incidentId,
22
- noteAdded: entry,
23
- timelineEntries: inc.timeline.length,
24
- };
25
- });
19
+ return {
20
+ incidentId: args.incidentId,
21
+ noteAdded: entry,
22
+ timelineEntries: inc.timeline.length,
23
+ };
26
24
  },
27
25
  });
@@ -1,17 +1,16 @@
1
- import { tool } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
2
  import {
4
3
  calculateTriageScore,
5
4
  createIncident,
5
+ dispatchSlot,
6
6
  getApplicableProtocols,
7
7
  recommendResources,
8
8
  recommendSeverity,
9
9
  recommendType,
10
10
  resourceBrief,
11
- updateState,
12
11
  } from "../shared.ts";
13
12
 
14
- export const incidentCreate = tool({
13
+ export default dispatchSlot.updateTool({
15
14
  description: "Create a new incident from an incoming emergency call.",
16
15
  inputSchema: z.object({
17
16
  location: z.string().max(300).describe("Address or location description"),
@@ -30,50 +29,48 @@ export const incidentCreate = tool({
30
29
  .describe("Known hazards: fire, chemical, electrical, structural, weapons")
31
30
  .optional(),
32
31
  }),
33
- async execute(args, ctx) {
34
- return updateState(ctx, (state) => {
35
- const recSeverity = recommendSeverity(args.description);
36
- const recType = recommendType(args.description);
37
- const triageScore = calculateTriageScore(
38
- recSeverity,
39
- recType,
40
- args.estimatedCasualties ?? 0,
41
- args.hazards?.length ?? 0,
42
- );
32
+ execute(args, state) {
33
+ const recSeverity = recommendSeverity(args.description);
34
+ const recType = recommendType(args.description);
35
+ const triageScore = calculateTriageScore(
36
+ recSeverity,
37
+ recType,
38
+ args.estimatedCasualties ?? 0,
39
+ args.hazards?.length ?? 0,
40
+ );
43
41
 
44
- const incident = createIncident(state, {
45
- type: recType,
46
- severity: recSeverity,
47
- location: args.location,
48
- description: args.description,
49
- callerName: args.callerName ?? "Unknown",
50
- callerPhone: args.callerPhone ?? "Unknown",
51
- triageScore,
52
- timeline: [{ time: Date.now(), event: `Incident created: ${args.description}` }],
53
- casualties: {
54
- confirmed: 0,
55
- estimated: args.estimatedCasualties ?? 0,
56
- treated: 0,
57
- },
58
- hazards: args.hazards ?? [],
59
- });
60
- const id = incident.id;
42
+ const incident = createIncident(state, {
43
+ type: recType,
44
+ severity: recSeverity,
45
+ location: args.location,
46
+ description: args.description,
47
+ callerName: args.callerName ?? "Unknown",
48
+ callerPhone: args.callerPhone ?? "Unknown",
49
+ triageScore,
50
+ timeline: [{ time: Date.now(), event: `Incident created: ${args.description}` }],
51
+ casualties: {
52
+ confirmed: 0,
53
+ estimated: args.estimatedCasualties ?? 0,
54
+ treated: 0,
55
+ },
56
+ hazards: args.hazards ?? [],
57
+ });
58
+ const id = incident.id;
61
59
 
62
- const protocols = getApplicableProtocols(recType, recSeverity);
63
- const recommended = recommendResources(recType, recSeverity, state);
60
+ const protocols = getApplicableProtocols(recType, recSeverity);
61
+ const recommended = recommendResources(recType, recSeverity, state);
64
62
 
65
- return {
66
- incidentId: id,
67
- recommendedSeverity: recSeverity,
68
- recommendedType: recType,
69
- triageScore,
70
- applicableProtocols: protocols.map((p) => p.name),
71
- recommendedResources: recommended.map(resourceBrief),
72
- message:
73
- recSeverity === "critical"
74
- ? `PRIORITY ONE — ${id} created. Immediate dispatch recommended. ${protocols.length} protocol(s) applicable.`
75
- : `${id} created. Triage score ${triageScore}. ${recommended.length} resource(s) recommended.`,
76
- };
77
- });
63
+ return {
64
+ incidentId: id,
65
+ recommendedSeverity: recSeverity,
66
+ recommendedType: recType,
67
+ triageScore,
68
+ applicableProtocols: protocols.map((p) => p.name),
69
+ recommendedResources: recommended.map(resourceBrief),
70
+ message:
71
+ recSeverity === "critical"
72
+ ? `PRIORITY ONE — ${id} created. Immediate dispatch recommended. ${protocols.length} protocol(s) applicable.`
73
+ : `${id} created. Triage score ${triageScore}. ${recommended.length} resource(s) recommended.`,
74
+ };
78
75
  },
79
76
  });
@@ -1,15 +1,15 @@
1
- import { tool } from "@alexkroman1/aai";
1
+ import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  assertNotResolved,
5
5
  calculateTriageScore,
6
+ dispatchSlot,
6
7
  findIncident,
7
8
  logEvent,
8
9
  recommendResources,
9
- updateState,
10
10
  } from "../shared.ts";
11
11
 
12
- export const incidentEscalate = tool({
12
+ export default dispatchSlot.updateTool({
13
13
  description: "Escalate an incident when it exceeds current capacity or severity increases.",
14
14
  inputSchema: z.object({
15
15
  incidentId: z.string().max(20).describe("The incident ID"),
@@ -20,69 +20,67 @@ export const incidentEscalate = tool({
20
20
  .optional(),
21
21
  newSeverity: z.enum(["critical", "urgent"]).describe("Escalated severity level").optional(),
22
22
  }),
23
- async execute(args, ctx) {
24
- return updateState(ctx, (state) => {
25
- const inc = findIncident(state, args.incidentId);
26
- if ("error" in inc) return inc;
27
- const blocked = assertNotResolved(inc, "escalate");
28
- if (blocked) return blocked;
23
+ execute(args, state) {
24
+ const inc = findIncident(state, args.incidentId);
25
+ if (isToolFailure(inc)) return inc;
26
+ const blocked = assertNotResolved(inc, "escalate");
27
+ if (blocked) return blocked;
29
28
 
30
- inc.escalationLevel++;
31
- if (args.newSeverity) inc.severity = args.newSeverity;
32
- inc.status = "escalated";
33
- logEvent(inc, `ESCALATED (Level ${inc.escalationLevel}): ${args.reason}`);
29
+ inc.escalationLevel++;
30
+ if (args.newSeverity) inc.severity = args.newSeverity;
31
+ inc.status = "escalated";
32
+ logEvent(inc, `ESCALATED (Level ${inc.escalationLevel}): ${args.reason}`);
34
33
 
35
- if (args.requestMutualAid) {
36
- logEvent(inc, "Mutual aid requested from neighboring jurisdictions");
37
- // Counter-based ids/callsigns: Date.now() collides across rapid
38
- // escalations, and duplicate callsigns break dispatch-by-callsign.
39
- const medicN = ++state.mutualAidCounter;
40
- const engineN = ++state.mutualAidCounter;
41
- state.resources.push(
42
- {
43
- id: `MA-${medicN}`,
44
- type: "ambulance",
45
- callsign: `Mutual-Aid-Medic-${medicN}`,
46
- status: "available",
47
- assignedIncident: null,
48
- eta: null,
49
- capabilities: ["als"],
50
- },
51
- {
52
- id: `MA-${engineN}`,
53
- type: "fire_engine",
54
- callsign: `Mutual-Aid-Engine-${engineN}`,
55
- status: "available",
56
- assignedIncident: null,
57
- eta: null,
58
- capabilities: ["structural"],
59
- },
60
- );
61
- }
62
-
63
- inc.triageScore = calculateTriageScore(
64
- inc.severity,
65
- inc.type,
66
- inc.casualties.estimated,
67
- inc.hazards.length,
34
+ if (args.requestMutualAid) {
35
+ logEvent(inc, "Mutual aid requested from neighboring jurisdictions");
36
+ // Counter-based ids/callsigns: Date.now() collides across rapid
37
+ // escalations, and duplicate callsigns break dispatch-by-callsign.
38
+ const medicN = ++state.mutualAidCounter;
39
+ const engineN = ++state.mutualAidCounter;
40
+ state.resources.push(
41
+ {
42
+ id: `MA-${medicN}`,
43
+ type: "ambulance",
44
+ callsign: `Mutual-Aid-Medic-${medicN}`,
45
+ status: "available",
46
+ assignedIncident: null,
47
+ eta: null,
48
+ capabilities: ["als"],
49
+ },
50
+ {
51
+ id: `MA-${engineN}`,
52
+ type: "fire_engine",
53
+ callsign: `Mutual-Aid-Engine-${engineN}`,
54
+ status: "available",
55
+ assignedIncident: null,
56
+ eta: null,
57
+ capabilities: ["structural"],
58
+ },
68
59
  );
60
+ }
69
61
 
70
- const additionalResources = recommendResources(inc.type, inc.severity, state).filter(
71
- (r) => !inc.assignedResources.includes(r.id),
72
- );
62
+ inc.triageScore = calculateTriageScore(
63
+ inc.severity,
64
+ inc.type,
65
+ inc.casualties.estimated,
66
+ inc.hazards.length,
67
+ );
68
+
69
+ const additionalResources = recommendResources(inc.type, inc.severity, state).filter(
70
+ (r) => !inc.assignedResources.includes(r.id),
71
+ );
73
72
 
74
- return {
75
- incidentId: args.incidentId,
76
- escalationLevel: inc.escalationLevel,
77
- newSeverity: inc.severity,
78
- newTriageScore: inc.triageScore,
79
- mutualAidRequested: args.requestMutualAid ?? false,
80
- additionalResourcesAvailable: additionalResources.map((r) => ({
81
- callsign: r.callsign,
82
- type: r.type,
83
- })),
84
- message: `ESCALATION CONFIRMED — ${args.incidentId} now Level ${inc.escalationLevel}. ${additionalResources.length} additional resource(s) available for dispatch.`,
85
- };
86
- });
73
+ return {
74
+ incidentId: args.incidentId,
75
+ escalationLevel: inc.escalationLevel,
76
+ newSeverity: inc.severity,
77
+ newTriageScore: inc.triageScore,
78
+ mutualAidRequested: args.requestMutualAid ?? false,
79
+ additionalResourcesAvailable: additionalResources.map((r) => ({
80
+ callsign: r.callsign,
81
+ type: r.type,
82
+ })),
83
+ message: `ESCALATION CONFIRMED — ${args.incidentId} now Level ${inc.escalationLevel}. ${additionalResources.length} additional resource(s) available for dispatch.`,
84
+ };
87
85
  },
88
86
  });