@alexkroman1/aai 0.8.7 → 0.9.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 (288) hide show
  1. package/dist/{sdk/_internal_types.d.ts → _internal-types.d.ts} +0 -3
  2. package/dist/_internal-types.js +19 -0
  3. package/dist/{sdk/_mock_ws.d.ts → _mock-ws.d.ts} +16 -5
  4. package/dist/_mock-ws.js +158 -0
  5. package/dist/{sdk/_utils.d.ts → _utils.d.ts} +1 -2
  6. package/dist/_utils.js +8 -0
  7. package/dist/{sdk/builtin_tools.d.ts → builtin-tools.d.ts} +5 -8
  8. package/dist/builtin-tools.js +270 -0
  9. package/dist/{sdk/direct_executor.d.ts → direct-executor.d.ts} +3 -7
  10. package/dist/direct-executor.js +125 -0
  11. package/dist/{sdk/mod.d.ts → index.d.ts} +0 -4
  12. package/dist/index.js +2 -0
  13. package/dist/{sdk/kv.d.ts → kv.d.ts} +23 -20
  14. package/dist/kv.js +99 -0
  15. package/dist/{sdk/protocol.d.ts → protocol.d.ts} +67 -31
  16. package/dist/protocol.js +142 -0
  17. package/dist/runtime.d.ts +18 -0
  18. package/dist/runtime.js +16 -0
  19. package/dist/s2s.d.ts +110 -0
  20. package/dist/s2s.js +242 -0
  21. package/dist/{sdk/server.d.ts → server.d.ts} +3 -23
  22. package/dist/server.js +105 -0
  23. package/dist/{sdk/session.d.ts → session.d.ts} +4 -11
  24. package/dist/session.js +312 -0
  25. package/dist/tsdown.config.d.ts +2 -0
  26. package/dist/{sdk/types.d.ts → types.d.ts} +41 -25
  27. package/dist/types.js +139 -0
  28. package/dist/{sdk/vector.d.ts → vector.d.ts} +14 -15
  29. package/dist/vector.js +56 -0
  30. package/dist/{sdk/worker_entry.d.ts → worker-entry.d.ts} +2 -5
  31. package/dist/worker-entry.js +59 -0
  32. package/dist/{sdk/ws_handler.d.ts → ws-handler.d.ts} +10 -8
  33. package/dist/ws-handler.js +155 -0
  34. package/package.json +66 -149
  35. package/README.md +0 -34
  36. package/dist/aai.js +0 -3
  37. package/dist/cli/tsconfig.tsbuildinfo +0 -1
  38. package/dist/cli.js +0 -2811
  39. package/dist/sdk/_internal_types.d.ts.map +0 -1
  40. package/dist/sdk/_internal_types.js +0 -25
  41. package/dist/sdk/_internal_types.js.map +0 -1
  42. package/dist/sdk/_mock_ws.d.ts.map +0 -1
  43. package/dist/sdk/_mock_ws.js +0 -154
  44. package/dist/sdk/_mock_ws.js.map +0 -1
  45. package/dist/sdk/_render_check.d.ts +0 -10
  46. package/dist/sdk/_render_check.d.ts.map +0 -1
  47. package/dist/sdk/_render_check.js +0 -72
  48. package/dist/sdk/_render_check.js.map +0 -1
  49. package/dist/sdk/_utils.d.ts.map +0 -1
  50. package/dist/sdk/_utils.js +0 -7
  51. package/dist/sdk/_utils.js.map +0 -1
  52. package/dist/sdk/builtin_tools.d.ts.map +0 -1
  53. package/dist/sdk/builtin_tools.js +0 -309
  54. package/dist/sdk/builtin_tools.js.map +0 -1
  55. package/dist/sdk/capnweb.d.ts +0 -102
  56. package/dist/sdk/capnweb.d.ts.map +0 -1
  57. package/dist/sdk/capnweb.js +0 -219
  58. package/dist/sdk/capnweb.js.map +0 -1
  59. package/dist/sdk/define_agent.d.ts +0 -36
  60. package/dist/sdk/define_agent.d.ts.map +0 -1
  61. package/dist/sdk/define_agent.js +0 -71
  62. package/dist/sdk/define_agent.js.map +0 -1
  63. package/dist/sdk/direct_executor.d.ts.map +0 -1
  64. package/dist/sdk/direct_executor.js +0 -145
  65. package/dist/sdk/direct_executor.js.map +0 -1
  66. package/dist/sdk/host.d.ts +0 -59
  67. package/dist/sdk/host.d.ts.map +0 -1
  68. package/dist/sdk/host.js +0 -131
  69. package/dist/sdk/host.js.map +0 -1
  70. package/dist/sdk/kv.d.ts.map +0 -1
  71. package/dist/sdk/kv.js +0 -94
  72. package/dist/sdk/kv.js.map +0 -1
  73. package/dist/sdk/memory_tools.d.ts +0 -38
  74. package/dist/sdk/memory_tools.d.ts.map +0 -1
  75. package/dist/sdk/memory_tools.js +0 -77
  76. package/dist/sdk/memory_tools.js.map +0 -1
  77. package/dist/sdk/mod.d.ts.map +0 -1
  78. package/dist/sdk/mod.js +0 -27
  79. package/dist/sdk/mod.js.map +0 -1
  80. package/dist/sdk/protocol.d.ts.map +0 -1
  81. package/dist/sdk/protocol.js +0 -133
  82. package/dist/sdk/protocol.js.map +0 -1
  83. package/dist/sdk/runtime.d.ts +0 -36
  84. package/dist/sdk/runtime.d.ts.map +0 -1
  85. package/dist/sdk/runtime.js +0 -27
  86. package/dist/sdk/runtime.js.map +0 -1
  87. package/dist/sdk/s2s.d.ts +0 -74
  88. package/dist/sdk/s2s.d.ts.map +0 -1
  89. package/dist/sdk/s2s.js +0 -218
  90. package/dist/sdk/s2s.js.map +0 -1
  91. package/dist/sdk/server.d.ts.map +0 -1
  92. package/dist/sdk/server.js +0 -144
  93. package/dist/sdk/server.js.map +0 -1
  94. package/dist/sdk/session.d.ts.map +0 -1
  95. package/dist/sdk/session.js +0 -303
  96. package/dist/sdk/session.js.map +0 -1
  97. package/dist/sdk/system_prompt.d.ts +0 -6
  98. package/dist/sdk/system_prompt.d.ts.map +0 -1
  99. package/dist/sdk/system_prompt.js +0 -35
  100. package/dist/sdk/system_prompt.js.map +0 -1
  101. package/dist/sdk/tsconfig.tsbuildinfo +0 -1
  102. package/dist/sdk/types.d.ts.map +0 -1
  103. package/dist/sdk/types.js +0 -96
  104. package/dist/sdk/types.js.map +0 -1
  105. package/dist/sdk/vector.d.ts.map +0 -1
  106. package/dist/sdk/vector.js +0 -63
  107. package/dist/sdk/vector.js.map +0 -1
  108. package/dist/sdk/winterc_server.d.ts +0 -56
  109. package/dist/sdk/winterc_server.d.ts.map +0 -1
  110. package/dist/sdk/winterc_server.js +0 -77
  111. package/dist/sdk/winterc_server.js.map +0 -1
  112. package/dist/sdk/worker_entry.d.ts.map +0 -1
  113. package/dist/sdk/worker_entry.js +0 -68
  114. package/dist/sdk/worker_entry.js.map +0 -1
  115. package/dist/sdk/worker_shim.d.ts +0 -19
  116. package/dist/sdk/worker_shim.d.ts.map +0 -1
  117. package/dist/sdk/worker_shim.js +0 -141
  118. package/dist/sdk/worker_shim.js.map +0 -1
  119. package/dist/sdk/ws_handler.d.ts.map +0 -1
  120. package/dist/sdk/ws_handler.js +0 -158
  121. package/dist/sdk/ws_handler.js.map +0 -1
  122. package/dist/ui/_cn.d.ts +0 -5
  123. package/dist/ui/_cn.d.ts.map +0 -1
  124. package/dist/ui/_cn.js +0 -22
  125. package/dist/ui/_cn.js.map +0 -1
  126. package/dist/ui/_components/app.d.ts +0 -5
  127. package/dist/ui/_components/app.d.ts.map +0 -1
  128. package/dist/ui/_components/app.js +0 -12
  129. package/dist/ui/_components/app.js.map +0 -1
  130. package/dist/ui/_components/button.d.ts +0 -11
  131. package/dist/ui/_components/button.d.ts.map +0 -1
  132. package/dist/ui/_components/button.js +0 -17
  133. package/dist/ui/_components/button.js.map +0 -1
  134. package/dist/ui/_components/chat_view.d.ts +0 -5
  135. package/dist/ui/_components/chat_view.d.ts.map +0 -1
  136. package/dist/ui/_components/chat_view.js +0 -15
  137. package/dist/ui/_components/chat_view.js.map +0 -1
  138. package/dist/ui/_components/controls.d.ts +0 -4
  139. package/dist/ui/_components/controls.d.ts.map +0 -1
  140. package/dist/ui/_components/controls.js +0 -10
  141. package/dist/ui/_components/controls.js.map +0 -1
  142. package/dist/ui/_components/error_banner.d.ts +0 -8
  143. package/dist/ui/_components/error_banner.d.ts.map +0 -1
  144. package/dist/ui/_components/error_banner.js +0 -8
  145. package/dist/ui/_components/error_banner.js.map +0 -1
  146. package/dist/ui/_components/message_bubble.d.ts +0 -7
  147. package/dist/ui/_components/message_bubble.d.ts.map +0 -1
  148. package/dist/ui/_components/message_bubble.js +0 -11
  149. package/dist/ui/_components/message_bubble.js.map +0 -1
  150. package/dist/ui/_components/message_list.d.ts +0 -4
  151. package/dist/ui/_components/message_list.d.ts.map +0 -1
  152. package/dist/ui/_components/message_list.js +0 -45
  153. package/dist/ui/_components/message_list.js.map +0 -1
  154. package/dist/ui/_components/sidebar_layout.d.ts +0 -20
  155. package/dist/ui/_components/sidebar_layout.d.ts.map +0 -1
  156. package/dist/ui/_components/sidebar_layout.js +0 -19
  157. package/dist/ui/_components/sidebar_layout.js.map +0 -1
  158. package/dist/ui/_components/start_screen.d.ts +0 -25
  159. package/dist/ui/_components/start_screen.d.ts.map +0 -1
  160. package/dist/ui/_components/start_screen.js +0 -28
  161. package/dist/ui/_components/start_screen.js.map +0 -1
  162. package/dist/ui/_components/state_indicator.d.ts +0 -8
  163. package/dist/ui/_components/state_indicator.d.ts.map +0 -1
  164. package/dist/ui/_components/state_indicator.js +0 -6
  165. package/dist/ui/_components/state_indicator.js.map +0 -1
  166. package/dist/ui/_components/thinking_indicator.d.ts +0 -5
  167. package/dist/ui/_components/thinking_indicator.d.ts.map +0 -1
  168. package/dist/ui/_components/thinking_indicator.js +0 -10
  169. package/dist/ui/_components/thinking_indicator.js.map +0 -1
  170. package/dist/ui/_components/tool_call_block.d.ts +0 -7
  171. package/dist/ui/_components/tool_call_block.d.ts.map +0 -1
  172. package/dist/ui/_components/tool_call_block.js +0 -46
  173. package/dist/ui/_components/tool_call_block.js.map +0 -1
  174. package/dist/ui/_components/tool_icons.d.ts +0 -18
  175. package/dist/ui/_components/tool_icons.d.ts.map +0 -1
  176. package/dist/ui/_components/tool_icons.js +0 -26
  177. package/dist/ui/_components/tool_icons.js.map +0 -1
  178. package/dist/ui/_components/transcript.d.ts +0 -7
  179. package/dist/ui/_components/transcript.d.ts.map +0 -1
  180. package/dist/ui/_components/transcript.js +0 -9
  181. package/dist/ui/_components/transcript.js.map +0 -1
  182. package/dist/ui/_dom_shim.d.ts +0 -7
  183. package/dist/ui/_dom_shim.d.ts.map +0 -1
  184. package/dist/ui/_dom_shim.js +0 -21
  185. package/dist/ui/_dom_shim.js.map +0 -1
  186. package/dist/ui/_hooks.d.ts +0 -21
  187. package/dist/ui/_hooks.d.ts.map +0 -1
  188. package/dist/ui/_hooks.js +0 -35
  189. package/dist/ui/_hooks.js.map +0 -1
  190. package/dist/ui/_jsdom_setup.d.ts +0 -1
  191. package/dist/ui/_jsdom_setup.d.ts.map +0 -1
  192. package/dist/ui/_jsdom_setup.js +0 -6
  193. package/dist/ui/_jsdom_setup.js.map +0 -1
  194. package/dist/ui/_test_utils.js +0 -272
  195. package/dist/ui/_test_utils.js.map +0 -1
  196. package/dist/ui/audio.d.ts +0 -46
  197. package/dist/ui/audio.d.ts.map +0 -1
  198. package/dist/ui/audio.js +0 -130
  199. package/dist/ui/audio.js.map +0 -1
  200. package/dist/ui/components.d.ts +0 -14
  201. package/dist/ui/components.d.ts.map +0 -1
  202. package/dist/ui/components.js +0 -15
  203. package/dist/ui/components.js.map +0 -1
  204. package/dist/ui/components_mod.d.ts +0 -34
  205. package/dist/ui/components_mod.d.ts.map +0 -1
  206. package/dist/ui/components_mod.js +0 -32
  207. package/dist/ui/components_mod.js.map +0 -1
  208. package/dist/ui/mod.d.ts +0 -23
  209. package/dist/ui/mod.d.ts.map +0 -1
  210. package/dist/ui/mod.js +0 -22
  211. package/dist/ui/mod.js.map +0 -1
  212. package/dist/ui/mount.d.ts +0 -44
  213. package/dist/ui/mount.d.ts.map +0 -1
  214. package/dist/ui/mount.js +0 -61
  215. package/dist/ui/mount.js.map +0 -1
  216. package/dist/ui/mount_context.d.ts +0 -22
  217. package/dist/ui/mount_context.d.ts.map +0 -1
  218. package/dist/ui/mount_context.js +0 -10
  219. package/dist/ui/mount_context.js.map +0 -1
  220. package/dist/ui/session.d.ts +0 -96
  221. package/dist/ui/session.d.ts.map +0 -1
  222. package/dist/ui/session.js +0 -379
  223. package/dist/ui/session.js.map +0 -1
  224. package/dist/ui/session_mod.d.ts +0 -19
  225. package/dist/ui/session_mod.d.ts.map +0 -1
  226. package/dist/ui/session_mod.js +0 -18
  227. package/dist/ui/session_mod.js.map +0 -1
  228. package/dist/ui/signals.d.ts +0 -80
  229. package/dist/ui/signals.d.ts.map +0 -1
  230. package/dist/ui/signals.js +0 -137
  231. package/dist/ui/signals.js.map +0 -1
  232. package/dist/ui/tsconfig.tsbuildinfo +0 -1
  233. package/dist/ui/types.d.ts +0 -36
  234. package/dist/ui/types.d.ts.map +0 -1
  235. package/dist/ui/types.js +0 -4
  236. package/dist/ui/types.js.map +0 -1
  237. package/dist/ui/worklets/capture-processor.d.ts +0 -3
  238. package/dist/ui/worklets/capture-processor.d.ts.map +0 -1
  239. package/dist/ui/worklets/capture-processor.js +0 -61
  240. package/dist/ui/worklets/capture-processor.js.map +0 -1
  241. package/dist/ui/worklets/playback-processor.d.ts +0 -3
  242. package/dist/ui/worklets/playback-processor.d.ts.map +0 -1
  243. package/dist/ui/worklets/playback-processor.js +0 -109
  244. package/dist/ui/worklets/playback-processor.js.map +0 -1
  245. package/templates/.env +0 -1
  246. package/templates/_shared/.env.example +0 -5
  247. package/templates/_shared/CLAUDE.md +0 -1073
  248. package/templates/_shared/biome.json +0 -32
  249. package/templates/_shared/global.d.ts +0 -1
  250. package/templates/_shared/index.html +0 -16
  251. package/templates/_shared/package.json +0 -22
  252. package/templates/_shared/tsconfig.json +0 -16
  253. package/templates/code-interpreter/agent.ts +0 -27
  254. package/templates/code-interpreter/client.tsx +0 -3
  255. package/templates/dispatch-center/agent.ts +0 -1223
  256. package/templates/dispatch-center/client.tsx +0 -505
  257. package/templates/embedded-assets/agent.ts +0 -48
  258. package/templates/embedded-assets/client.tsx +0 -3
  259. package/templates/embedded-assets/knowledge.json +0 -20
  260. package/templates/health-assistant/agent.ts +0 -160
  261. package/templates/health-assistant/client.tsx +0 -3
  262. package/templates/infocom-adventure/agent.ts +0 -164
  263. package/templates/infocom-adventure/client.tsx +0 -300
  264. package/templates/math-buddy/agent.ts +0 -21
  265. package/templates/math-buddy/client.tsx +0 -3
  266. package/templates/memory-agent/agent.ts +0 -20
  267. package/templates/memory-agent/client.tsx +0 -3
  268. package/templates/night-owl/agent.ts +0 -98
  269. package/templates/night-owl/client.tsx +0 -12
  270. package/templates/personal-finance/agent.ts +0 -26
  271. package/templates/personal-finance/client.tsx +0 -3
  272. package/templates/pizza-ordering/agent.ts +0 -214
  273. package/templates/pizza-ordering/client.tsx +0 -264
  274. package/templates/simple/agent.ts +0 -6
  275. package/templates/simple/client.tsx +0 -3
  276. package/templates/smart-research/agent.ts +0 -164
  277. package/templates/smart-research/client.tsx +0 -3
  278. package/templates/solo-rpg/agent.ts +0 -1240
  279. package/templates/solo-rpg/client.tsx +0 -698
  280. package/templates/support/README.md +0 -62
  281. package/templates/support/agent.ts +0 -19
  282. package/templates/support/client.tsx +0 -3
  283. package/templates/travel-concierge/agent.ts +0 -29
  284. package/templates/travel-concierge/client.tsx +0 -3
  285. package/templates/tsconfig.json +0 -1
  286. package/templates/web-researcher/agent.ts +0 -17
  287. package/templates/web-researcher/client.tsx +0 -3
  288. package/ui/styles.css +0 -74
@@ -1,1223 +0,0 @@
1
- import { defineAgent, tool } from "@alexkroman1/aai";
2
- import { z } from "zod";
3
- import type { HookContext, ToolContext } from "@alexkroman1/aai";
4
-
5
- // ─── Types ───────────────────────────────────────────────────────────────────
6
-
7
- type Severity = "critical" | "urgent" | "moderate" | "minor";
8
- type IncidentType =
9
- | "medical"
10
- | "fire"
11
- | "hazmat"
12
- | "traffic"
13
- | "crime"
14
- | "natural_disaster"
15
- | "utility"
16
- | "other";
17
- type Status =
18
- | "incoming"
19
- | "triaged"
20
- | "dispatched"
21
- | "en_route"
22
- | "on_scene"
23
- | "resolved"
24
- | "escalated";
25
-
26
- interface Resource {
27
- id: string;
28
- type:
29
- | "ambulance"
30
- | "fire_engine"
31
- | "police"
32
- | "hazmat_team"
33
- | "helicopter"
34
- | "k9_unit"
35
- | "swat"
36
- | "ems_supervisor";
37
- callsign: string;
38
- status: "available" | "dispatched" | "en_route" | "on_scene" | "returning";
39
- assignedIncident: string | null;
40
- eta: number | null; // minutes
41
- capabilities: string[];
42
- }
43
-
44
- interface Incident {
45
- id: string;
46
- type: IncidentType;
47
- severity: Severity;
48
- status: Status;
49
- location: string;
50
- description: string;
51
- callerName: string;
52
- callerPhone: string;
53
- triageScore: number;
54
- assignedResources: string[];
55
- timeline: { time: number; event: string }[];
56
- notes: string[];
57
- createdAt: number;
58
- updatedAt: number;
59
- escalationLevel: number;
60
- protocolsActivated: string[];
61
- casualties: { confirmed: number; estimated: number; treated: number };
62
- hazards: string[];
63
- }
64
-
65
- interface DispatchState {
66
- incidents: Record<string, Incident>;
67
- resources: Resource[];
68
- incidentCounter: number;
69
- alertLevel: "green" | "yellow" | "orange" | "red"; // system-wide
70
- mutualAidRequested: boolean;
71
- }
72
-
73
- // ─── Session state ───────────────────────────────────────────────────────────
74
-
75
- function createState(): DispatchState {
76
- return {
77
- incidents: {},
78
- resources: generateResources(),
79
- incidentCounter: 0,
80
- alertLevel: "green",
81
- mutualAidRequested: false,
82
- };
83
- }
84
-
85
- const RESOURCE_DEFS: [string, Resource["type"], string, string[]][] = [
86
- ["R1", "ambulance", "Medic-1", ["als", "cardiac", "pediatric"]],
87
- ["R2", "ambulance", "Medic-2", ["als", "trauma"]],
88
- ["R3", "ambulance", "Medic-3", ["bls"]],
89
- ["R4", "fire_engine", "Engine-7", ["structural", "rescue", "ems_first_response"]],
90
- ["R5", "fire_engine", "Ladder-2", ["aerial", "rescue", "ventilation"]],
91
- ["R6", "police", "Unit-12", ["patrol", "traffic_control"]],
92
- ["R7", "police", "Unit-15", ["patrol", "investigation"]],
93
- ["R8", "hazmat_team", "HazMat-1", ["chemical", "biological", "radiological", "decon"]],
94
- ["R9", "helicopter", "LifeFlight-1", ["medevac", "search_rescue", "thermal_imaging"]],
95
- ["R10", "ems_supervisor", "EMS-Sup-1", ["mass_casualty", "triage_lead", "command"]],
96
- ["R11", "k9_unit", "K9-3", ["tracking", "narcotics", "explosives"]],
97
- ["R12", "swat", "TAC-1", ["tactical", "hostage_rescue", "high_risk_warrant"]],
98
- ];
99
-
100
- function generateResources(): Resource[] {
101
- return RESOURCE_DEFS.map(([id, type, callsign, capabilities]) => ({
102
- id, type, callsign, capabilities, status: "available" as const, assignedIncident: null, eta: null,
103
- }));
104
- }
105
-
106
- // ─── Triage & scoring ────────────────────────────────────────────────────────
107
-
108
- const SEVERITY_WEIGHTS: Record<Severity, number> = {
109
- critical: 100,
110
- urgent: 70,
111
- moderate: 40,
112
- minor: 10,
113
- };
114
-
115
- const TYPE_MULTIPLIERS: Record<IncidentType, number> = {
116
- medical: 1.2,
117
- fire: 1.3,
118
- hazmat: 1.5,
119
- traffic: 1.0,
120
- crime: 1.1,
121
- "natural_disaster": 1.8,
122
- utility: 0.8,
123
- other: 0.7,
124
- };
125
-
126
- function calculateTriageScore(
127
- severity: Severity,
128
- type: IncidentType,
129
- casualties: number,
130
- hazards: number,
131
- ): number {
132
- let score = SEVERITY_WEIGHTS[severity] * TYPE_MULTIPLIERS[type];
133
- score += Math.min(casualties * 15, 60); // up to 60 pts for casualties
134
- score += Math.min(hazards * 10, 30); // up to 30 pts for hazards
135
- return Math.round(Math.min(score, 250));
136
- }
137
-
138
- const SEVERITY_KEYWORDS: [Severity, string[]][] = [
139
- ["critical", ["unconscious", "not breathing", "cardiac arrest", "trapped", "collapse", "explosion", "active shooter", "mass casualty"]],
140
- ["urgent", ["bleeding", "chest pain", "difficulty breathing", "fire", "hazmat", "shooting", "stabbing", "multi-vehicle"]],
141
- ["moderate", ["fall", "broken", "fracture", "smoke", "minor fire", "assault", "theft"]],
142
- ];
143
-
144
- function recommendSeverity(description: string): Severity {
145
- const d = description.toLowerCase();
146
- for (const [sev, kws] of SEVERITY_KEYWORDS) {
147
- if (kws.some((k) => d.includes(k))) return sev;
148
- }
149
- return "minor";
150
- }
151
-
152
- const TYPE_KEYWORDS: Record<IncidentType, string[]> = {
153
- medical: ["chest pain", "breathing", "unconscious", "seizure", "allergic", "overdose", "cardiac", "stroke", "diabetic", "bleeding", "fall", "injury"],
154
- fire: ["fire", "smoke", "flames", "burning", "arson"],
155
- hazmat: ["chemical", "spill", "gas leak", "fumes", "radiation", "contamination", "hazmat"],
156
- traffic: ["accident", "crash", "collision", "vehicle", "rollover", "pedestrian struck", "hit and run"],
157
- crime: ["robbery", "assault", "shooting", "stabbing", "burglar", "theft", "domestic", "hostage", "active shooter"],
158
- natural_disaster: ["earthquake", "flood", "tornado", "hurricane", "landslide", "wildfire", "tsunami"],
159
- utility: ["power outage", "downed line", "water main", "gas main", "transformer"],
160
- other: [],
161
- };
162
-
163
- function recommendType(description: string): IncidentType {
164
- const d = description.toLowerCase();
165
- let best: IncidentType = "other";
166
- let bestCount = 0;
167
- for (const [type, keywords] of Object.entries(TYPE_KEYWORDS)) {
168
- const count = keywords.filter((k) => d.includes(k)).length;
169
- if (count > bestCount) { bestCount = count; best = type as IncidentType; }
170
- }
171
- return best;
172
- }
173
-
174
- // ─── Protocol engine ─────────────────────────────────────────────────────────
175
-
176
- interface Protocol {
177
- name: string;
178
- triggers: { types: IncidentType[]; minSeverity: Severity };
179
- steps: string[];
180
- requiredResources: Resource["type"][];
181
- }
182
-
183
- const PROTOCOLS: Protocol[] = [
184
- { name: "Mass Casualty Incident (MCI)",
185
- triggers: { types: ["medical", "fire", "natural_disaster", "traffic"], minSeverity: "critical" },
186
- steps: ["Establish Incident Command", "Request mutual aid if >10 casualties", "Set up triage: Immediate (red), Delayed (yellow), Minor (green), Deceased (black)", "Assign triage lead (EMS supervisor)", "Establish patient collection point", "Coordinate helicopter landing zone if needed", "Notify receiving hospitals and activate surge protocols"],
187
- requiredResources: ["ambulance", "ems_supervisor", "fire_engine"] },
188
- { name: "Structure Fire - Working Fire",
189
- triggers: { types: ["fire"], minSeverity: "urgent" },
190
- steps: ["Dispatch minimum 2 engines and 1 ladder", "Establish incident command and 360-degree size-up", "Confirm water supply", "Search and rescue primary sweep", "Ventilation operations", "Establish RIT (Rapid Intervention Team)", "Request additional alarms if not contained in 10 min"],
191
- requiredResources: ["fire_engine"] },
192
- { name: "Hazardous Materials Response",
193
- triggers: { types: ["hazmat"], minSeverity: "moderate" },
194
- steps: ["Identify substance via placard numbers or SDS", "Establish hot, warm, and cold zones", "Evacuate downwind 1000+ feet for unknowns", "Deploy HazMat team in appropriate PPE", "Set up decontamination corridor", "Monitor air quality and wind continuously", "Coordinate with poison control"],
195
- requiredResources: ["hazmat_team", "fire_engine", "ambulance"] },
196
- { name: "Active Threat / Active Shooter",
197
- triggers: { types: ["crime"], minSeverity: "critical" },
198
- steps: ["Dispatch SWAT and multiple patrol units", "Establish inner and outer perimeters", "Activate Rescue Task Force — police escort EMS into warm zone", "Stage ambulances outside hot zone", "Request LifeFlight on standby", "Get building floor plans", "Establish family reunification point"],
199
- requiredResources: ["swat", "police", "ambulance", "ems_supervisor"] },
200
- { name: "Multi-Vehicle Accident",
201
- triggers: { types: ["traffic"], minSeverity: "urgent" },
202
- steps: ["Dispatch engine for extrication", "Request traffic control to shut lanes", "Triage using START protocol", "Check for fuel/hazmat spills", "Establish helicopter landing zone if needed", "Coordinate with DOT for road closures"],
203
- requiredResources: ["fire_engine", "ambulance", "police"] },
204
- { name: "Cardiac Arrest Protocol",
205
- triggers: { types: ["medical"], minSeverity: "critical" },
206
- steps: ["Instruct caller: CPR — 30 compressions, 2 breaths", "Dispatch closest ALS unit and fire engine", "Guide caller through AED use if available", "Target first defibrillation under 8 minutes", "Prepare for advanced airway management"],
207
- requiredResources: ["ambulance", "fire_engine"] },
208
- ];
209
-
210
- function getApplicableProtocols(
211
- type: IncidentType,
212
- severity: Severity,
213
- ): Protocol[] {
214
- const severityRank: Record<Severity, number> = {
215
- critical: 4,
216
- urgent: 3,
217
- moderate: 2,
218
- minor: 1,
219
- };
220
- return PROTOCOLS.filter((p) =>
221
- p.triggers.types.includes(type) &&
222
- severityRank[severity] >= severityRank[p.triggers.minSeverity]
223
- );
224
- }
225
-
226
- // ─── Resource recommendation engine ──────────────────────────────────────────
227
-
228
- function recommendResources(
229
- type: IncidentType,
230
- severity: Severity,
231
- state: DispatchState,
232
- ): Resource[] {
233
- const needed: Resource["type"][] = [];
234
-
235
- // Base resource needs by incident type
236
- const baseNeeds: Record<IncidentType, Resource["type"][]> = {
237
- medical: ["ambulance"],
238
- fire: ["fire_engine", "ambulance"],
239
- hazmat: ["hazmat_team", "fire_engine", "ambulance"],
240
- traffic: ["police", "ambulance", "fire_engine"],
241
- crime: ["police"],
242
- "natural_disaster": ["fire_engine", "ambulance", "police"],
243
- utility: ["fire_engine"],
244
- other: [],
245
- };
246
-
247
- needed.push(...(baseNeeds[type] || []));
248
-
249
- // Severity escalation
250
- if (severity === "critical") {
251
- if (!needed.includes("ambulance")) needed.push("ambulance");
252
- needed.push("ems_supervisor");
253
- if (type === "crime") needed.push("swat");
254
- }
255
- if (severity === "urgent" && type === "fire") {
256
- needed.push("fire_engine"); // second engine
257
- }
258
-
259
- // Find available resources matching needs
260
- const recommended: Resource[] = [];
261
- const usedIds = new Set<string>();
262
-
263
- for (const needType of needed) {
264
- const available = state.resources.find(
265
- (r) =>
266
- r.type === needType && r.status === "available" && !usedIds.has(r.id),
267
- );
268
- if (available) {
269
- recommended.push(available);
270
- usedIds.add(available.id);
271
- }
272
- }
273
-
274
- return recommended;
275
- }
276
-
277
- // ─── System alert level calculation ──────────────────────────────────────────
278
-
279
- function recalculateAlertLevel(state: DispatchState): void {
280
- const activeIncidents = Object.values(state.incidents).filter((i) =>
281
- !["resolved"].includes(i.status)
282
- );
283
- const criticalCount =
284
- activeIncidents.filter((i) => i.severity === "critical").length;
285
- const totalActive = activeIncidents.length;
286
- const availableResources =
287
- state.resources.filter((r) => r.status === "available").length;
288
- const totalResources = state.resources.length;
289
- const resourceUtilization = 1 - (availableResources / totalResources);
290
-
291
- if (criticalCount >= 3 || resourceUtilization > 0.85 || totalActive >= 8) {
292
- state.alertLevel = "red";
293
- } else if (
294
- criticalCount >= 2 || resourceUtilization > 0.65 || totalActive >= 5
295
- ) {
296
- state.alertLevel = "orange";
297
- } else if (
298
- criticalCount >= 1 || resourceUtilization > 0.4 || totalActive >= 3
299
- ) {
300
- state.alertLevel = "yellow";
301
- } else {
302
- state.alertLevel = "green";
303
- }
304
-
305
- // Auto-request mutual aid at red
306
- if (state.alertLevel === "red" && !state.mutualAidRequested) {
307
- state.mutualAidRequested = true;
308
- }
309
- }
310
-
311
- function now(): number {
312
- return Date.now();
313
- }
314
-
315
- // ─── KV persistence ─────────────────────────────────────────────────────────
316
-
317
- const STATE_KEY = "dispatch:state";
318
-
319
- async function saveState(
320
- ctx: { kv: ToolContext["kv"]; state: unknown },
321
- ): Promise<void> {
322
- await ctx.kv.set(STATE_KEY, ctx.state);
323
- }
324
-
325
- async function loadState(ctx: HookContext<DispatchState>): Promise<void> {
326
- const saved = await ctx.kv.get<DispatchState>(STATE_KEY);
327
- if (saved) {
328
- Object.assign(ctx.state, saved);
329
- }
330
- }
331
-
332
- // ─── Agent definition ────────────────────────────────────────────────────────
333
-
334
- export default defineAgent({
335
- name: "Dispatch Command Center",
336
-
337
- greeting:
338
- "Dispatch Command Center online. Restoring operational state. I'm ready to take incoming calls, manage active incidents, or run dispatch operations. Say 'dashboard' for a full status report. What do we have.",
339
-
340
- instructions:
341
- `You are the AI-powered Emergency Dispatch Command Center. You coordinate emergency response for a metropolitan area. You manage incidents from initial 911 call through resolution.
342
-
343
- 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.
344
-
345
- Your tools:
346
-
347
- INCIDENT MANAGEMENT:
348
- - incident_create: Log a new incident. Ask for location first, then nature of emergency, then caller info. Speed matters for critical calls.
349
- - incident_triage: After creating, assess severity. The system recommends severity, type, and protocols. Review and confirm or override.
350
- - incident_update_status: Move incidents through the workflow (en_route, on_scene, resolved, escalated).
351
- - incident_get: Get details on a specific incident.
352
- - incident_escalate: Escalate when an incident exceeds current capacity or severity increases.
353
- - incident_add_note: Add ongoing situational updates.
354
-
355
- RESOURCE MANAGEMENT:
356
- - resources_dispatch: Assign units. The system recommends optimal resources based on incident type and severity. You can also manually dispatch specific units.
357
- - resources_get_available: See what units are free.
358
- - resources_update_status: Update unit status when units radio in.
359
-
360
- OPERATIONS:
361
- - ops_dashboard: Get the full operational picture.
362
- - ops_protocols: Retrieve step-by-step response protocols. Follow them precisely for critical incidents.
363
- - ops_run_scenario: Run training exercises.
364
-
365
- SEARCH: Use web_search to look up hazmat placard numbers, drug interactions, building addresses, or other reference information during active incidents.
366
-
367
- CALCULATIONS: Use run_code for ETA calculations, resource optimization, or casualty estimates.
368
-
369
- Operational rules:
370
- - Location is always the first priority in any emergency call
371
- - Critical incidents get immediate dispatch, triage can happen simultaneously
372
- - Never leave a critical incident without at least one resource dispatched
373
- - Monitor resource utilization. If it exceeds 65 percent, warn about degraded capacity
374
- - At red alert level, recommend mutual aid from neighboring jurisdictions
375
- - Track time on all incidents. Escalate if critical incidents have no on-scene resources within 8 minutes
376
- - When reporting the dashboard, lead with the most severe active incidents
377
- - Use plain language for medical instructions to callers, dispatch terminology for unit communications
378
-
379
- Radio style: "Medic-1, respond priority one to 400 Oak Street, report of cardiac arrest, CPR in progress." Keep it tight and professional.`,
380
-
381
- builtinTools: [],
382
-
383
- state: createState,
384
-
385
- onConnect: async (ctx) => {
386
- await loadState(ctx);
387
- },
388
-
389
- tools: {
390
- incident_create: tool({
391
- description: "Create a new incident from an incoming emergency call.",
392
- parameters: z.object({
393
- location: z.string().describe("Address or location description"),
394
- description: z.string().describe(
395
- "Nature of the emergency as described by caller",
396
- ),
397
- callerName: z.string().describe("Caller's name").optional(),
398
- callerPhone: z.string().describe("Callback number").optional(),
399
- estimatedCasualties: z.number().describe(
400
- "Estimated number of casualties if known",
401
- ).optional(),
402
- hazards: z.array(z.string()).describe(
403
- "Known hazards: fire, chemical, electrical, structural, weapons",
404
- ).optional(),
405
- }),
406
- execute: async (
407
- {
408
- location,
409
- description,
410
- callerName,
411
- callerPhone,
412
- estimatedCasualties,
413
- hazards,
414
- },
415
- ctx,
416
- ) => {
417
- const state = ctx.state as DispatchState;
418
- state.incidentCounter++;
419
- const id = `INC-${String(state.incidentCounter).padStart(4, "0")}`;
420
-
421
- const recSeverity = recommendSeverity(description);
422
- const recType = recommendType(description);
423
- const triageScore = calculateTriageScore(
424
- recSeverity,
425
- recType,
426
- estimatedCasualties || 0,
427
- hazards?.length || 0,
428
- );
429
-
430
- const incident: Incident = {
431
- id,
432
- type: recType,
433
- severity: recSeverity,
434
- status: "incoming",
435
- location,
436
- description,
437
- callerName: callerName || "Unknown",
438
- callerPhone: callerPhone || "Unknown",
439
- triageScore,
440
- assignedResources: [],
441
- timeline: [{
442
- time: now(),
443
- event: `Incident created: ${description}`,
444
- }],
445
- notes: [],
446
- createdAt: now(),
447
- updatedAt: now(),
448
- escalationLevel: 0,
449
- protocolsActivated: [],
450
- casualties: {
451
- confirmed: 0,
452
- estimated: estimatedCasualties || 0,
453
- treated: 0,
454
- },
455
- hazards: hazards || [],
456
- };
457
-
458
- state.incidents[id] = incident;
459
- recalculateAlertLevel(state);
460
- await saveState(ctx);
461
-
462
- const protocols = getApplicableProtocols(recType, recSeverity);
463
- const recommended = recommendResources(
464
- recType,
465
- recSeverity,
466
- state,
467
- );
468
-
469
- return {
470
- incidentId: id,
471
- recommendedSeverity: recSeverity,
472
- recommendedType: recType,
473
- triageScore,
474
- applicableProtocols: protocols.map((p) => p.name),
475
- recommendedResources: recommended.map((r) => ({
476
- callsign: r.callsign,
477
- type: r.type,
478
- capabilities: r.capabilities,
479
- })),
480
- systemAlertLevel: state.alertLevel,
481
- message: recSeverity === "critical"
482
- ? `PRIORITY ONE — ${id} created. Immediate dispatch recommended. ${protocols.length} protocol(s) applicable.`
483
- : `${id} created. Triage score ${triageScore}. ${recommended.length} resource(s) recommended.`,
484
- };
485
- },
486
- }),
487
-
488
- incident_triage: tool({
489
- description:
490
- "Triage an incident — confirm or override severity, type, hazards, and casualty count.",
491
- parameters: z.object({
492
- incidentId: z.string().describe("The incident ID"),
493
- severity: z.enum(["critical", "urgent", "moderate", "minor"])
494
- .describe("Confirmed severity after triage").optional(),
495
- type: z.enum([
496
- "medical",
497
- "fire",
498
- "hazmat",
499
- "traffic",
500
- "crime",
501
- "natural_disaster",
502
- "utility",
503
- "other",
504
- ]).describe("Confirmed incident type").optional(),
505
- additionalHazards: z.array(z.string()).describe(
506
- "Any additional hazards identified",
507
- ).optional(),
508
- casualtyUpdate: z.number().describe("Updated casualty count")
509
- .optional(),
510
- notes: z.string().describe("Triage notes").optional(),
511
- }),
512
- execute: async (
513
- {
514
- incidentId,
515
- severity,
516
- type,
517
- additionalHazards,
518
- casualtyUpdate,
519
- notes,
520
- },
521
- ctx,
522
- ) => {
523
- const state = ctx.state as DispatchState;
524
- const inc = state.incidents[incidentId];
525
- if (!inc) return { error: `Incident ${incidentId} not found` };
526
-
527
- if (severity) inc.severity = severity;
528
- if (type) inc.type = type;
529
- if (additionalHazards) inc.hazards.push(...additionalHazards);
530
- if (casualtyUpdate !== undefined) {
531
- inc.casualties.estimated = casualtyUpdate;
532
- }
533
- if (notes) inc.notes.push(notes);
534
-
535
- inc.triageScore = calculateTriageScore(
536
- inc.severity,
537
- inc.type,
538
- inc.casualties.estimated,
539
- inc.hazards.length,
540
- );
541
- inc.status = "triaged";
542
- inc.updatedAt = now();
543
- inc.timeline.push({
544
- time: now(),
545
- event:
546
- `Triaged: ${inc.severity} ${inc.type}, score ${inc.triageScore}`,
547
- });
548
-
549
- recalculateAlertLevel(state);
550
- await saveState(ctx);
551
-
552
- const protocols = getApplicableProtocols(inc.type, inc.severity);
553
- const recommended = recommendResources(
554
- inc.type,
555
- inc.severity,
556
- state,
557
- );
558
-
559
- return {
560
- incidentId,
561
- severity: inc.severity,
562
- type: inc.type,
563
- triageScore: inc.triageScore,
564
- hazards: inc.hazards,
565
- estimatedCasualties: inc.casualties.estimated,
566
- protocols: protocols.map((p) => ({
567
- name: p.name,
568
- steps: p.steps,
569
- requiredResources: p.requiredResources,
570
- })),
571
- recommendedResources: recommended.map((r) => ({
572
- callsign: r.callsign,
573
- type: r.type,
574
- })),
575
- systemAlertLevel: state.alertLevel,
576
- };
577
- },
578
- }),
579
-
580
- incident_update_status: tool({
581
- description:
582
- "Update an incident's status (en_route, on_scene, resolved, escalated).",
583
- parameters: z.object({
584
- incidentId: z.string().describe("The incident ID"),
585
- status: z.enum(["en_route", "on_scene", "resolved", "escalated"])
586
- .describe("New status"),
587
- notes: z.string().describe("Status update notes").optional(),
588
- casualtyUpdate: z.object({
589
- confirmed: z.number().optional(),
590
- treated: z.number().optional(),
591
- }).describe("Updated casualty numbers").optional(),
592
- }),
593
- execute: async (
594
- { incidentId, status, notes, casualtyUpdate },
595
- ctx,
596
- ) => {
597
- const state = ctx.state as DispatchState;
598
- const inc = state.incidents[incidentId];
599
- if (!inc) return { error: `Incident ${incidentId} not found` };
600
-
601
- inc.status = status;
602
- inc.updatedAt = now();
603
- inc.timeline.push({
604
- time: now(),
605
- event: `Status → ${status}${notes ? `: ${notes}` : ""}`,
606
- });
607
- if (notes) inc.notes.push(notes);
608
-
609
- if (casualtyUpdate) {
610
- if (casualtyUpdate.confirmed !== undefined) {
611
- inc.casualties.confirmed = casualtyUpdate.confirmed;
612
- }
613
- if (casualtyUpdate.treated !== undefined) {
614
- inc.casualties.treated = casualtyUpdate.treated;
615
- }
616
- }
617
-
618
- // Release resources on resolution
619
- if (status === "resolved") {
620
- for (const rId of inc.assignedResources) {
621
- const r = state.resources.find((r) => r.id === rId);
622
- if (r) {
623
- r.status = "returning";
624
- r.assignedIncident = null;
625
- r.eta = null;
626
- // Auto-return to available after a delay (simulated)
627
- setTimeout(() => {
628
- r.status = "available";
629
- }, 2000);
630
- }
631
- }
632
- inc.timeline.push({
633
- time: now(),
634
- event: "All resources released — incident closed",
635
- });
636
- }
637
-
638
- // Update resource statuses for en_route/on_scene
639
- if (status === "en_route" || status === "on_scene") {
640
- for (const rId of inc.assignedResources) {
641
- const r = state.resources.find((r) => r.id === rId);
642
- if (r) r.status = status;
643
- }
644
- }
645
-
646
- recalculateAlertLevel(state);
647
- await saveState(ctx);
648
-
649
- return {
650
- incidentId,
651
- newStatus: status,
652
- timeline: inc.timeline.slice(-5).map((t) => t.event),
653
- casualties: inc.casualties,
654
- systemAlertLevel: state.alertLevel,
655
- };
656
- },
657
- }),
658
-
659
- incident_escalate: tool({
660
- description:
661
- "Escalate an incident when it exceeds current capacity or severity increases.",
662
- parameters: z.object({
663
- incidentId: z.string().describe("The incident ID"),
664
- reason: z.string().describe("Reason for escalation"),
665
- requestMutualAid: z.boolean().describe(
666
- "Whether to request mutual aid from neighboring jurisdictions",
667
- ).optional(),
668
- newSeverity: z.enum(["critical", "urgent"]).describe(
669
- "Escalated severity level",
670
- ).optional(),
671
- }),
672
- execute: async (
673
- { incidentId, reason, requestMutualAid, newSeverity },
674
- ctx,
675
- ) => {
676
- const state = ctx.state as DispatchState;
677
- const inc = state.incidents[incidentId];
678
- if (!inc) return { error: `Incident ${incidentId} not found` };
679
-
680
- inc.escalationLevel++;
681
- if (newSeverity) inc.severity = newSeverity;
682
- inc.status = "escalated";
683
- inc.updatedAt = now();
684
- inc.timeline.push({
685
- time: now(),
686
- event: `ESCALATED (Level ${inc.escalationLevel}): ${reason}`,
687
- });
688
- inc.notes.push(`Escalation: ${reason}`);
689
-
690
- if (requestMutualAid) {
691
- state.mutualAidRequested = true;
692
- inc.timeline.push({
693
- time: now(),
694
- event: "Mutual aid requested from neighboring jurisdictions",
695
- });
696
- // Simulate mutual aid resources arriving
697
- state.resources.push(
698
- {
699
- id: `MA-${Date.now()}-1`,
700
- type: "ambulance",
701
- callsign: "Mutual-Aid-Medic",
702
- status: "available",
703
- assignedIncident: null,
704
- eta: null,
705
- capabilities: ["als"],
706
- },
707
- {
708
- id: `MA-${Date.now()}-2`,
709
- type: "fire_engine",
710
- callsign: "Mutual-Aid-Engine",
711
- status: "available",
712
- assignedIncident: null,
713
- eta: null,
714
- capabilities: ["structural"],
715
- },
716
- );
717
- }
718
-
719
- inc.triageScore = calculateTriageScore(
720
- inc.severity,
721
- inc.type,
722
- inc.casualties.estimated,
723
- inc.hazards.length,
724
- );
725
- recalculateAlertLevel(state);
726
- await saveState(ctx);
727
-
728
- const additionalResources = recommendResources(
729
- inc.type,
730
- inc.severity,
731
- state,
732
- ).filter(
733
- (r) => !inc.assignedResources.includes(r.id),
734
- );
735
-
736
- return {
737
- incidentId,
738
- escalationLevel: inc.escalationLevel,
739
- newSeverity: inc.severity,
740
- newTriageScore: inc.triageScore,
741
- mutualAidRequested: requestMutualAid || false,
742
- additionalResourcesAvailable: additionalResources.map((r) => ({
743
- callsign: r.callsign,
744
- type: r.type,
745
- })),
746
- systemAlertLevel: state.alertLevel,
747
- message:
748
- `ESCALATION CONFIRMED — ${incidentId} now Level ${inc.escalationLevel}. ${additionalResources.length} additional resource(s) available for dispatch.`,
749
- };
750
- },
751
- }),
752
-
753
- incident_get: tool({
754
- description:
755
- "Get full details on a specific incident including timeline and assigned resources.",
756
- parameters: z.object({
757
- incidentId: z.string().describe("The incident ID"),
758
- }),
759
- execute: ({ incidentId }, ctx) => {
760
- const state = ctx.state as DispatchState;
761
- const inc = state.incidents[incidentId];
762
- if (!inc) return { error: `Incident ${incidentId} not found` };
763
-
764
- const assignedResourceDetails = inc.assignedResources.map(
765
- (rId) => {
766
- const r = state.resources.find((r) => r.id === rId);
767
- return r
768
- ? {
769
- callsign: r.callsign,
770
- type: r.type,
771
- status: r.status,
772
- eta: r.eta,
773
- }
774
- : null;
775
- },
776
- ).filter(Boolean);
777
-
778
- const ageMinutes = Math.round((now() - inc.createdAt) / 60000);
779
-
780
- return {
781
- ...inc,
782
- ageMinutes,
783
- assignedResourceDetails,
784
- applicableProtocols: getApplicableProtocols(
785
- inc.type,
786
- inc.severity,
787
- )
788
- .map((p) => p.name),
789
- };
790
- },
791
- }),
792
-
793
- incident_add_note: tool({
794
- description: "Add a situational update note to an incident.",
795
- parameters: z.object({
796
- incidentId: z.string().describe("The incident ID"),
797
- note: z.string().describe("The note to add"),
798
- source: z.string().describe(
799
- "Who reported this — unit callsign or caller",
800
- ).optional(),
801
- }),
802
- execute: async ({ incidentId, note, source }, ctx) => {
803
- const state = ctx.state as DispatchState;
804
- const inc = state.incidents[incidentId];
805
- if (!inc) return { error: `Incident ${incidentId} not found` };
806
-
807
- const entry = source ? `[${source}] ${note}` : note;
808
- inc.notes.push(entry);
809
- inc.timeline.push({ time: now(), event: entry });
810
- inc.updatedAt = now();
811
- await saveState(ctx);
812
-
813
- return {
814
- incidentId,
815
- noteAdded: entry,
816
- totalNotes: inc.notes.length,
817
- };
818
- },
819
- }),
820
-
821
- resources_dispatch: tool({
822
- description:
823
- "Dispatch units to an incident. Can auto-dispatch recommended resources or manually specify callsigns.",
824
- parameters: z.object({
825
- incidentId: z.string().describe("The incident ID"),
826
- callsigns: z.array(z.string()).describe(
827
- "Resource callsigns to dispatch. Use 'auto' for system-recommended resources.",
828
- ).optional(),
829
- autoDispatch: z.boolean().describe(
830
- "If true, automatically dispatch recommended resources",
831
- ).optional(),
832
- priority: z.enum(["routine", "priority", "emergency"]).describe(
833
- "Dispatch priority — affects simulated ETA",
834
- ).optional(),
835
- }),
836
- execute: async (
837
- { incidentId, callsigns, autoDispatch, priority },
838
- ctx,
839
- ) => {
840
- const state = ctx.state as DispatchState;
841
- const inc = state.incidents[incidentId];
842
- if (!inc) return { error: `Incident ${incidentId} not found` };
843
-
844
- const dispatched: {
845
- callsign: string;
846
- type: string;
847
- eta: number;
848
- }[] = [];
849
- const failed: { callsign: string; reason: string }[] = [];
850
-
851
- let resourcesToDispatch: Resource[] = [];
852
-
853
- if (autoDispatch) {
854
- resourcesToDispatch = recommendResources(
855
- inc.type,
856
- inc.severity,
857
- state,
858
- );
859
- } else if (callsigns) {
860
- for (const cs of callsigns) {
861
- const r = state.resources.find((r) =>
862
- r.callsign.toLowerCase() === cs.toLowerCase()
863
- );
864
- if (!r) {
865
- failed.push({ callsign: cs, reason: "Not found" });
866
- continue;
867
- }
868
- if (r.status !== "available") {
869
- failed.push({
870
- callsign: cs,
871
- reason: `Currently ${r.status}`,
872
- });
873
- continue;
874
- }
875
- resourcesToDispatch.push(r);
876
- }
877
- }
878
-
879
- const etaBase = priority === "emergency"
880
- ? 3
881
- : priority === "priority"
882
- ? 6
883
- : 10;
884
-
885
- for (const r of resourcesToDispatch) {
886
- const eta = etaBase + Math.floor(Math.random() * 5);
887
- r.status = "dispatched";
888
- r.assignedIncident = incidentId;
889
- r.eta = eta;
890
- inc.assignedResources.push(r.id);
891
- dispatched.push({ callsign: r.callsign, type: r.type, eta });
892
- inc.timeline.push({
893
- time: now(),
894
- event: `Dispatched ${r.callsign} — ETA ${eta} min`,
895
- });
896
- }
897
-
898
- if (dispatched.length > 0) {
899
- inc.status = "dispatched";
900
- inc.updatedAt = now();
901
- }
902
-
903
- recalculateAlertLevel(state);
904
- await saveState(ctx);
905
-
906
- const availableCount = state.resources.filter((r) =>
907
- r.status === "available"
908
- ).length;
909
-
910
- return {
911
- incidentId,
912
- dispatched,
913
- failed: failed.length > 0 ? failed : undefined,
914
- totalAssignedToIncident: inc.assignedResources.length,
915
- remainingAvailableResources: availableCount,
916
- systemAlertLevel: state.alertLevel,
917
- capacityWarning: availableCount <= 3
918
- ? "WARNING: Resource capacity critically low. Consider mutual aid."
919
- : undefined,
920
- };
921
- },
922
- }),
923
-
924
- resources_get_available: tool({
925
- description: "List available resources, optionally filtered by type.",
926
- parameters: z.object({
927
- type: z.enum([
928
- "ambulance",
929
- "fire_engine",
930
- "police",
931
- "hazmat_team",
932
- "helicopter",
933
- "k9_unit",
934
- "swat",
935
- "ems_supervisor",
936
- "all",
937
- ]).describe("Filter by resource type, or 'all'").optional(),
938
- }),
939
- execute: ({ type }, ctx) => {
940
- const state = ctx.state as DispatchState;
941
- let resources = state.resources;
942
- if (type && type !== "all") {
943
- resources = resources.filter((r) => r.type === type);
944
- }
945
-
946
- return {
947
- resources: resources.map((r) => ({
948
- callsign: r.callsign,
949
- type: r.type,
950
- status: r.status,
951
- assignedIncident: r.assignedIncident,
952
- eta: r.eta,
953
- capabilities: r.capabilities,
954
- })),
955
- summary: {
956
- total: resources.length,
957
- available: resources.filter((r) => r.status === "available")
958
- .length,
959
- committed: resources.filter((r) => r.status !== "available")
960
- .length,
961
- },
962
- };
963
- },
964
- }),
965
-
966
- resources_update_status: tool({
967
- description: "Update a resource unit's status when it radios in.",
968
- parameters: z.object({
969
- callsign: z.string().describe("The resource callsign"),
970
- status: z.enum([
971
- "available",
972
- "dispatched",
973
- "en_route",
974
- "on_scene",
975
- "returning",
976
- ]).describe("New status"),
977
- notes: z.string().describe("Status notes").optional(),
978
- }),
979
- execute: async ({ callsign, status, notes }, ctx) => {
980
- const state = ctx.state as DispatchState;
981
- const resource = state.resources.find((r) =>
982
- r.callsign.toLowerCase() === callsign.toLowerCase()
983
- );
984
- if (!resource) {
985
- return { error: `Resource ${callsign} not found` };
986
- }
987
-
988
- const previousStatus = resource.status;
989
- resource.status = status;
990
-
991
- if (status === "available") {
992
- resource.assignedIncident = null;
993
- resource.eta = null;
994
- }
995
-
996
- // Log to incident timeline if assigned
997
- if (resource.assignedIncident) {
998
- const inc = state.incidents[resource.assignedIncident];
999
- if (inc) {
1000
- inc.timeline.push({
1001
- time: now(),
1002
- event: `${callsign}: ${previousStatus} → ${status}${
1003
- notes ? ` (${notes})` : ""
1004
- }`,
1005
- });
1006
- inc.updatedAt = now();
1007
- }
1008
- }
1009
-
1010
- recalculateAlertLevel(state);
1011
- await saveState(ctx);
1012
-
1013
- return {
1014
- callsign: resource.callsign,
1015
- previousStatus,
1016
- newStatus: status,
1017
- assignedIncident: resource.assignedIncident,
1018
- systemAlertLevel: state.alertLevel,
1019
- };
1020
- },
1021
- }),
1022
-
1023
- ops_dashboard: {
1024
- description:
1025
- "Get the full operational dashboard: alert level, resource utilization, active incidents, and available resources.",
1026
- execute: (_args, ctx) => {
1027
- const state = ctx.state as DispatchState;
1028
-
1029
- const activeIncidents = Object.values(state.incidents)
1030
- .filter((i) => i.status !== "resolved")
1031
- .sort((a, b) => b.triageScore - a.triageScore);
1032
-
1033
- const resolvedCount =
1034
- Object.values(state.incidents).filter((i) => i.status === "resolved")
1035
- .length;
1036
-
1037
- const resourceSummary = {
1038
- total: state.resources.length,
1039
- available:
1040
- state.resources.filter((r) => r.status === "available").length,
1041
- dispatched:
1042
- state.resources.filter((r) => r.status === "dispatched").length,
1043
- enRoute:
1044
- state.resources.filter((r) => r.status === "en_route").length,
1045
- onScene:
1046
- state.resources.filter((r) => r.status === "on_scene").length,
1047
- returning:
1048
- state.resources.filter((r) => r.status === "returning").length,
1049
- };
1050
-
1051
- const utilization = Math.round(
1052
- (1 - resourceSummary.available / resourceSummary.total) * 100,
1053
- );
1054
-
1055
- return {
1056
- systemAlertLevel: state.alertLevel,
1057
- mutualAidActive: state.mutualAidRequested,
1058
- resourceUtilization: `${utilization}%`,
1059
- resourceSummary,
1060
- activeIncidentCount: activeIncidents.length,
1061
- resolvedIncidentCount: resolvedCount,
1062
- activeIncidents: activeIncidents.map((i) => ({
1063
- id: i.id,
1064
- type: i.type,
1065
- severity: i.severity,
1066
- status: i.status,
1067
- location: i.location,
1068
- triageScore: i.triageScore,
1069
- assignedResourceCount: i.assignedResources.length,
1070
- ageMinutes: Math.round((now() - i.createdAt) / 60000),
1071
- casualties: i.casualties,
1072
- })),
1073
- availableResources: state.resources.filter((r) =>
1074
- r.status === "available"
1075
- ).map((r) => ({
1076
- callsign: r.callsign,
1077
- type: r.type,
1078
- capabilities: r.capabilities,
1079
- })),
1080
- };
1081
- },
1082
- },
1083
-
1084
- ops_protocols: tool({
1085
- description:
1086
- "Look up step-by-step response protocols for a given incident type and severity.",
1087
- parameters: z.object({
1088
- incidentType: z.enum([
1089
- "medical",
1090
- "fire",
1091
- "hazmat",
1092
- "traffic",
1093
- "crime",
1094
- "natural_disaster",
1095
- "utility",
1096
- "other",
1097
- ]).describe("Type of incident"),
1098
- severity: z.enum(["critical", "urgent", "moderate", "minor"])
1099
- .describe("Severity level"),
1100
- }),
1101
- execute: ({ incidentType, severity }) => {
1102
- const protocols = getApplicableProtocols(
1103
- incidentType,
1104
- severity,
1105
- );
1106
- if (protocols.length === 0) {
1107
- return {
1108
- message:
1109
- "No specific protocols for this combination. Use standard operating procedures.",
1110
- protocols: [],
1111
- };
1112
- }
1113
- return {
1114
- protocols: protocols.map((p) => ({
1115
- name: p.name,
1116
- steps: p.steps,
1117
- requiredResources: p.requiredResources,
1118
- })),
1119
- };
1120
- },
1121
- }),
1122
-
1123
- ops_run_scenario: tool({
1124
- description:
1125
- "Run a training scenario that creates simulated incidents for dispatch practice.",
1126
- parameters: z.object({
1127
- scenario: z.enum([
1128
- "mass_casualty",
1129
- "multi_alarm_fire",
1130
- "active_shooter",
1131
- "natural_disaster",
1132
- "highway_pileup",
1133
- ]).describe("Scenario type to simulate"),
1134
- }),
1135
- execute: async ({ scenario }, ctx) => {
1136
- const state = ctx.state as DispatchState;
1137
- type ScenarioDef = { narrative: string; incidents: Partial<Incident>[] };
1138
- const inc = (location: string, description: string, type: IncidentType, severity: Severity): Partial<Incident> =>
1139
- ({ location, description, type, severity });
1140
-
1141
- const scenarios: Record<string, ScenarioDef> = {
1142
- mass_casualty: { narrative: "Bus crash at Main and 5th. School bus vs delivery truck. Multiple pediatric patients. Fuel spill.",
1143
- incidents: [
1144
- inc("Main St and 5th Ave intersection", "School bus collision with delivery truck, multiple children injured, bus on its side, fuel leaking", "traffic", "critical"),
1145
- inc("Main St and 5th Ave — fuel spill", "Diesel fuel spill from delivery truck spreading toward storm drain, ~50 gallons", "hazmat", "urgent"),
1146
- ] },
1147
- multi_alarm_fire: { narrative: "Working structure fire at 200 Industrial Parkway. 3-story warehouse, heavy smoke. Workers possibly trapped.",
1148
- incidents: [
1149
- inc("200 Industrial Parkway", "3-story warehouse fully involved, possible trapped occupants on 2nd/3rd floor", "fire", "critical"),
1150
- inc("200 Industrial Parkway — medical", "2 workers with smoke inhalation, one with burns", "medical", "urgent"),
1151
- ] },
1152
- active_shooter: { narrative: "Active shooter at Riverside Mall. Multiple shots fired, crowds fleeing. At least 3 victims down in food court.",
1153
- incidents: [
1154
- inc("Riverside Mall, 1500 River Road — food court", "Active shooter, multiple shots, at least 3 victims down, shooter moving toward west entrance", "crime", "critical"),
1155
- inc("Riverside Mall parking lot", "Crowd crush injuries, several trampled near east exit", "medical", "urgent"),
1156
- ] },
1157
- natural_disaster: { narrative: "EF-3 tornado in residential area. Oak Street corridor. Multiple structures collapsed. Power lines down.",
1158
- incidents: [
1159
- inc("Oak Street between 10th and 15th", "Tornado damage, homes collapsed, people trapped, gas lines ruptured", "natural_disaster", "critical"),
1160
- inc("Oak Street Elementary School", "School roof partially collapsed, staff sheltering students", "natural_disaster", "critical"),
1161
- inc("Oak Street and 12th — utility", "Downed power lines sparking, gas main rupture, area needs isolation", "utility", "urgent"),
1162
- ] },
1163
- highway_pileup: { narrative: "20+ vehicle pileup on I-95 southbound mile marker 42. Fog. Multiple entrapments. Tanker truck involved.",
1164
- incidents: [
1165
- inc("I-95 southbound mile marker 42", "Multi-vehicle pileup, 20+ vehicles, multiple entrapments, tanker with unknown cargo, heavy fog", "traffic", "critical"),
1166
- inc("I-95 southbound — hazmat", "Tanker leaking unknown liquid, placards not visible, exclusion zone being set up", "hazmat", "critical"),
1167
- ] },
1168
- };
1169
-
1170
- const s = scenarios[scenario];
1171
- if (!s) return { error: "Unknown scenario" };
1172
-
1173
- const created: string[] = [];
1174
- for (const inc of s.incidents) {
1175
- state.incidentCounter++;
1176
- const id = `INC-${String(state.incidentCounter).padStart(4, "0")}`;
1177
- const fullInc: Incident = {
1178
- id,
1179
- type: inc.type || "other",
1180
- severity: inc.severity || "moderate",
1181
- status: "incoming",
1182
- location: inc.location || "Unknown",
1183
- description: inc.description || "",
1184
- callerName: "Scenario",
1185
- callerPhone: "N/A",
1186
- triageScore: calculateTriageScore(
1187
- (inc.severity || "moderate") as Severity,
1188
- (inc.type || "other") as IncidentType,
1189
- 0,
1190
- 0,
1191
- ),
1192
- assignedResources: [],
1193
- timeline: [{
1194
- time: now(),
1195
- event: `SCENARIO: ${inc.description}`,
1196
- }],
1197
- notes: [],
1198
- createdAt: now(),
1199
- updatedAt: now(),
1200
- escalationLevel: 0,
1201
- protocolsActivated: [],
1202
- casualties: { confirmed: 0, estimated: 0, treated: 0 },
1203
- hazards: [],
1204
- };
1205
- state.incidents[id] = fullInc;
1206
- created.push(id);
1207
- }
1208
-
1209
- recalculateAlertLevel(state);
1210
- await saveState(ctx);
1211
-
1212
- return {
1213
- scenario,
1214
- narrative: s.narrative,
1215
- incidentsCreated: created,
1216
- systemAlertLevel: state.alertLevel,
1217
- message:
1218
- `SCENARIO ACTIVE: ${s.narrative}. ${created.length} incidents created. Awaiting dispatch orders.`,
1219
- };
1220
- },
1221
- }),
1222
- },
1223
- });