@alexkroman1/aai 0.8.3 → 0.8.4

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 (164) hide show
  1. package/dist/cli/tsconfig.tsbuildinfo +1 -0
  2. package/dist/cli.js +1223 -1701
  3. package/dist/sdk/_mock_ws.js +2 -2
  4. package/dist/sdk/_mock_ws.js.map +1 -1
  5. package/dist/sdk/_render_check.d.ts.map +1 -1
  6. package/dist/sdk/_render_check.js +29 -2
  7. package/dist/sdk/_render_check.js.map +1 -1
  8. package/dist/sdk/_utils.d.ts +4 -0
  9. package/dist/sdk/_utils.d.ts.map +1 -0
  10. package/dist/sdk/_utils.js +7 -0
  11. package/dist/sdk/_utils.js.map +1 -0
  12. package/dist/sdk/builtin_tools.d.ts +35 -11
  13. package/dist/sdk/builtin_tools.d.ts.map +1 -1
  14. package/dist/sdk/builtin_tools.js +118 -76
  15. package/dist/sdk/builtin_tools.js.map +1 -1
  16. package/dist/sdk/capnweb.d.ts +76 -47
  17. package/dist/sdk/capnweb.d.ts.map +1 -1
  18. package/dist/sdk/capnweb.js +99 -242
  19. package/dist/sdk/capnweb.js.map +1 -1
  20. package/dist/sdk/direct_executor.d.ts.map +1 -1
  21. package/dist/sdk/direct_executor.js +0 -2
  22. package/dist/sdk/direct_executor.js.map +1 -1
  23. package/dist/sdk/host.d.ts +59 -0
  24. package/dist/sdk/host.d.ts.map +1 -0
  25. package/dist/sdk/host.js +131 -0
  26. package/dist/sdk/host.js.map +1 -0
  27. package/dist/sdk/mod.d.ts +2 -4
  28. package/dist/sdk/mod.d.ts.map +1 -1
  29. package/dist/sdk/mod.js +2 -3
  30. package/dist/sdk/mod.js.map +1 -1
  31. package/dist/sdk/protocol.d.ts +33 -135
  32. package/dist/sdk/protocol.d.ts.map +1 -1
  33. package/dist/sdk/protocol.js +49 -51
  34. package/dist/sdk/protocol.js.map +1 -1
  35. package/dist/sdk/runtime.d.ts +0 -1
  36. package/dist/sdk/runtime.d.ts.map +1 -1
  37. package/dist/sdk/runtime.js +5 -24
  38. package/dist/sdk/runtime.js.map +1 -1
  39. package/dist/sdk/s2s.d.ts +14 -3
  40. package/dist/sdk/s2s.d.ts.map +1 -1
  41. package/dist/sdk/s2s.js +72 -113
  42. package/dist/sdk/s2s.js.map +1 -1
  43. package/dist/sdk/server.d.ts +1 -1
  44. package/dist/sdk/server.d.ts.map +1 -1
  45. package/dist/sdk/server.js +26 -10
  46. package/dist/sdk/server.js.map +1 -1
  47. package/dist/sdk/session.d.ts +5 -1
  48. package/dist/sdk/session.d.ts.map +1 -1
  49. package/dist/sdk/session.js +131 -137
  50. package/dist/sdk/session.js.map +1 -1
  51. package/dist/sdk/tsconfig.tsbuildinfo +1 -0
  52. package/dist/sdk/types.d.ts +30 -3
  53. package/dist/sdk/types.d.ts.map +1 -1
  54. package/dist/sdk/types.js +37 -0
  55. package/dist/sdk/types.js.map +1 -1
  56. package/dist/sdk/winterc_server.d.ts +0 -1
  57. package/dist/sdk/winterc_server.d.ts.map +1 -1
  58. package/dist/sdk/winterc_server.js +0 -1
  59. package/dist/sdk/winterc_server.js.map +1 -1
  60. package/dist/sdk/worker_entry.d.ts +3 -11
  61. package/dist/sdk/worker_entry.d.ts.map +1 -1
  62. package/dist/sdk/worker_entry.js +8 -18
  63. package/dist/sdk/worker_entry.js.map +1 -1
  64. package/dist/sdk/worker_shim.d.ts +5 -6
  65. package/dist/sdk/worker_shim.d.ts.map +1 -1
  66. package/dist/sdk/worker_shim.js +93 -136
  67. package/dist/sdk/worker_shim.js.map +1 -1
  68. package/dist/sdk/ws_handler.d.ts +1 -3
  69. package/dist/sdk/ws_handler.d.ts.map +1 -1
  70. package/dist/sdk/ws_handler.js +14 -25
  71. package/dist/sdk/ws_handler.js.map +1 -1
  72. package/dist/ui/_cn.d.ts +5 -0
  73. package/dist/ui/_cn.d.ts.map +1 -0
  74. package/dist/ui/_cn.js +22 -0
  75. package/dist/ui/_cn.js.map +1 -0
  76. package/dist/ui/_components/app.d.ts +3 -1
  77. package/dist/ui/_components/app.d.ts.map +1 -1
  78. package/dist/ui/_components/app.js +2 -2
  79. package/dist/ui/_components/app.js.map +1 -1
  80. package/dist/ui/_components/button.d.ts +11 -0
  81. package/dist/ui/_components/button.d.ts.map +1 -0
  82. package/dist/ui/_components/button.js +17 -0
  83. package/dist/ui/_components/button.js.map +1 -0
  84. package/dist/ui/_components/chat_view.d.ts +3 -1
  85. package/dist/ui/_components/chat_view.d.ts.map +1 -1
  86. package/dist/ui/_components/chat_view.js +4 -2
  87. package/dist/ui/_components/chat_view.js.map +1 -1
  88. package/dist/ui/_components/controls.d.ts +3 -1
  89. package/dist/ui/_components/controls.d.ts.map +1 -1
  90. package/dist/ui/_components/controls.js +4 -5
  91. package/dist/ui/_components/controls.js.map +1 -1
  92. package/dist/ui/_components/error_banner.d.ts +2 -1
  93. package/dist/ui/_components/error_banner.d.ts.map +1 -1
  94. package/dist/ui/_components/error_banner.js +3 -2
  95. package/dist/ui/_components/error_banner.js.map +1 -1
  96. package/dist/ui/_components/message_bubble.d.ts +2 -1
  97. package/dist/ui/_components/message_bubble.d.ts.map +1 -1
  98. package/dist/ui/_components/message_bubble.js +5 -3
  99. package/dist/ui/_components/message_bubble.js.map +1 -1
  100. package/dist/ui/_components/message_list.d.ts +3 -1
  101. package/dist/ui/_components/message_list.d.ts.map +1 -1
  102. package/dist/ui/_components/message_list.js +7 -15
  103. package/dist/ui/_components/message_list.js.map +1 -1
  104. package/dist/ui/_components/sidebar_layout.d.ts +2 -1
  105. package/dist/ui/_components/sidebar_layout.d.ts.map +1 -1
  106. package/dist/ui/_components/sidebar_layout.js +5 -7
  107. package/dist/ui/_components/sidebar_layout.js.map +1 -1
  108. package/dist/ui/_components/start_screen.d.ts +2 -1
  109. package/dist/ui/_components/start_screen.d.ts.map +1 -1
  110. package/dist/ui/_components/start_screen.js +5 -2
  111. package/dist/ui/_components/start_screen.js.map +1 -1
  112. package/dist/ui/_components/state_indicator.d.ts +2 -1
  113. package/dist/ui/_components/state_indicator.d.ts.map +1 -1
  114. package/dist/ui/_components/state_indicator.js +3 -2
  115. package/dist/ui/_components/state_indicator.js.map +1 -1
  116. package/dist/ui/_components/thinking_indicator.d.ts +3 -1
  117. package/dist/ui/_components/thinking_indicator.d.ts.map +1 -1
  118. package/dist/ui/_components/thinking_indicator.js +4 -2
  119. package/dist/ui/_components/thinking_indicator.js.map +1 -1
  120. package/dist/ui/_components/tool_call_block.d.ts +2 -1
  121. package/dist/ui/_components/tool_call_block.d.ts.map +1 -1
  122. package/dist/ui/_components/tool_call_block.js +13 -25
  123. package/dist/ui/_components/tool_call_block.js.map +1 -1
  124. package/dist/ui/_components/transcript.d.ts +2 -1
  125. package/dist/ui/_components/transcript.d.ts.map +1 -1
  126. package/dist/ui/_components/transcript.js +3 -2
  127. package/dist/ui/_components/transcript.js.map +1 -1
  128. package/dist/ui/_jsdom_setup.d.ts +1 -0
  129. package/dist/ui/_jsdom_setup.d.ts.map +1 -0
  130. package/dist/ui/_jsdom_setup.js +6 -0
  131. package/dist/ui/_jsdom_setup.js.map +1 -0
  132. package/dist/ui/audio.d.ts.map +1 -1
  133. package/dist/ui/audio.js +4 -4
  134. package/dist/ui/audio.js.map +1 -1
  135. package/dist/ui/components.d.ts +13 -55
  136. package/dist/ui/components.d.ts.map +1 -1
  137. package/dist/ui/components.js +13 -42
  138. package/dist/ui/components.js.map +1 -1
  139. package/dist/ui/components_mod.d.ts +14 -3
  140. package/dist/ui/components_mod.d.ts.map +1 -1
  141. package/dist/ui/components_mod.js +14 -3
  142. package/dist/ui/components_mod.js.map +1 -1
  143. package/dist/ui/mod.d.ts +1 -8
  144. package/dist/ui/mod.d.ts.map +1 -1
  145. package/dist/ui/mod.js +1 -5
  146. package/dist/ui/mod.js.map +1 -1
  147. package/dist/ui/mount.d.ts +1 -1
  148. package/dist/ui/mount.d.ts.map +1 -1
  149. package/dist/ui/mount.js +1 -0
  150. package/dist/ui/mount.js.map +1 -1
  151. package/dist/ui/session.d.ts +0 -2
  152. package/dist/ui/session.d.ts.map +1 -1
  153. package/dist/ui/session.js +9 -18
  154. package/dist/ui/session.js.map +1 -1
  155. package/dist/ui/signals.d.ts +8 -3
  156. package/dist/ui/signals.d.ts.map +1 -1
  157. package/dist/ui/signals.js +22 -11
  158. package/dist/ui/signals.js.map +1 -1
  159. package/dist/ui/tsconfig.tsbuildinfo +1 -0
  160. package/dist/ui/worklets/playback-processor.js +3 -3
  161. package/package.json +37 -17
  162. package/templates/_shared/CLAUDE.md +4 -7
  163. package/templates/dispatch-center/agent.ts +85 -397
  164. package/templates/solo-rpg/agent.ts +3 -3
@@ -82,117 +82,25 @@ function createState(): DispatchState {
82
82
  };
83
83
  }
84
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
+
85
100
  function generateResources(): Resource[] {
86
- return [
87
- {
88
- id: "R1",
89
- type: "ambulance",
90
- callsign: "Medic-1",
91
- status: "available",
92
- assignedIncident: null,
93
- eta: null,
94
- capabilities: ["als", "cardiac", "pediatric"],
95
- },
96
- {
97
- id: "R2",
98
- type: "ambulance",
99
- callsign: "Medic-2",
100
- status: "available",
101
- assignedIncident: null,
102
- eta: null,
103
- capabilities: ["als", "trauma"],
104
- },
105
- {
106
- id: "R3",
107
- type: "ambulance",
108
- callsign: "Medic-3",
109
- status: "available",
110
- assignedIncident: null,
111
- eta: null,
112
- capabilities: ["bls"],
113
- },
114
- {
115
- id: "R4",
116
- type: "fire_engine",
117
- callsign: "Engine-7",
118
- status: "available",
119
- assignedIncident: null,
120
- eta: null,
121
- capabilities: ["structural", "rescue", "ems_first_response"],
122
- },
123
- {
124
- id: "R5",
125
- type: "fire_engine",
126
- callsign: "Ladder-2",
127
- status: "available",
128
- assignedIncident: null,
129
- eta: null,
130
- capabilities: ["aerial", "rescue", "ventilation"],
131
- },
132
- {
133
- id: "R6",
134
- type: "police",
135
- callsign: "Unit-12",
136
- status: "available",
137
- assignedIncident: null,
138
- eta: null,
139
- capabilities: ["patrol", "traffic_control"],
140
- },
141
- {
142
- id: "R7",
143
- type: "police",
144
- callsign: "Unit-15",
145
- status: "available",
146
- assignedIncident: null,
147
- eta: null,
148
- capabilities: ["patrol", "investigation"],
149
- },
150
- {
151
- id: "R8",
152
- type: "hazmat_team",
153
- callsign: "HazMat-1",
154
- status: "available",
155
- assignedIncident: null,
156
- eta: null,
157
- capabilities: ["chemical", "biological", "radiological", "decon"],
158
- },
159
- {
160
- id: "R9",
161
- type: "helicopter",
162
- callsign: "LifeFlight-1",
163
- status: "available",
164
- assignedIncident: null,
165
- eta: null,
166
- capabilities: ["medevac", "search_rescue", "thermal_imaging"],
167
- },
168
- {
169
- id: "R10",
170
- type: "ems_supervisor",
171
- callsign: "EMS-Sup-1",
172
- status: "available",
173
- assignedIncident: null,
174
- eta: null,
175
- capabilities: ["mass_casualty", "triage_lead", "command"],
176
- },
177
- {
178
- id: "R11",
179
- type: "k9_unit",
180
- callsign: "K9-3",
181
- status: "available",
182
- assignedIncident: null,
183
- eta: null,
184
- capabilities: ["tracking", "narcotics", "explosives"],
185
- },
186
- {
187
- id: "R12",
188
- type: "swat",
189
- callsign: "TAC-1",
190
- status: "available",
191
- assignedIncident: null,
192
- eta: null,
193
- capabilities: ["tactical", "hostage_rescue", "high_risk_warrant"],
194
- },
195
- ];
101
+ return RESOURCE_DEFS.map(([id, type, callsign, capabilities]) => ({
102
+ id, type, callsign, capabilities, status: "available" as const, assignedIncident: null, eta: null,
103
+ }));
196
104
  }
197
105
 
198
106
  // ─── Triage & scoring ────────────────────────────────────────────────────────
@@ -227,118 +135,38 @@ function calculateTriageScore(
227
135
  return Math.round(Math.min(score, 250));
228
136
  }
229
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
+
230
144
  function recommendSeverity(description: string): Severity {
231
145
  const d = description.toLowerCase();
232
- const criticalKeywords = [
233
- "unconscious",
234
- "not breathing",
235
- "cardiac arrest",
236
- "trapped",
237
- "collapse",
238
- "explosion",
239
- "active shooter",
240
- "mass casualty",
241
- ];
242
- const urgentKeywords = [
243
- "bleeding",
244
- "chest pain",
245
- "difficulty breathing",
246
- "fire",
247
- "hazmat",
248
- "shooting",
249
- "stabbing",
250
- "multi-vehicle",
251
- ];
252
- const moderateKeywords = [
253
- "fall",
254
- "broken",
255
- "fracture",
256
- "smoke",
257
- "minor fire",
258
- "assault",
259
- "theft",
260
- ];
261
-
262
- if (criticalKeywords.some((k) => d.includes(k))) return "critical";
263
- if (urgentKeywords.some((k) => d.includes(k))) return "urgent";
264
- if (moderateKeywords.some((k) => d.includes(k))) return "moderate";
146
+ for (const [sev, kws] of SEVERITY_KEYWORDS) {
147
+ if (kws.some((k) => d.includes(k))) return sev;
148
+ }
265
149
  return "minor";
266
150
  }
267
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
+
268
163
  function recommendType(description: string): IncidentType {
269
164
  const d = description.toLowerCase();
270
- const typeKeywords: Record<IncidentType, string[]> = {
271
- medical: [
272
- "chest pain",
273
- "breathing",
274
- "unconscious",
275
- "seizure",
276
- "allergic",
277
- "overdose",
278
- "cardiac",
279
- "stroke",
280
- "diabetic",
281
- "bleeding",
282
- "fall",
283
- "injury",
284
- ],
285
- fire: ["fire", "smoke", "flames", "burning", "arson"],
286
- hazmat: [
287
- "chemical",
288
- "spill",
289
- "gas leak",
290
- "fumes",
291
- "radiation",
292
- "contamination",
293
- "hazmat",
294
- ],
295
- traffic: [
296
- "accident",
297
- "crash",
298
- "collision",
299
- "vehicle",
300
- "rollover",
301
- "pedestrian struck",
302
- "hit and run",
303
- ],
304
- crime: [
305
- "robbery",
306
- "assault",
307
- "shooting",
308
- "stabbing",
309
- "burglar",
310
- "theft",
311
- "domestic",
312
- "hostage",
313
- "active shooter",
314
- ],
315
- "natural_disaster": [
316
- "earthquake",
317
- "flood",
318
- "tornado",
319
- "hurricane",
320
- "landslide",
321
- "wildfire",
322
- "tsunami",
323
- ],
324
- utility: [
325
- "power outage",
326
- "downed line",
327
- "water main",
328
- "gas main",
329
- "transformer",
330
- ],
331
- other: [],
332
- };
333
-
334
165
  let best: IncidentType = "other";
335
166
  let bestCount = 0;
336
- for (const [type, keywords] of Object.entries(typeKeywords)) {
167
+ for (const [type, keywords] of Object.entries(TYPE_KEYWORDS)) {
337
168
  const count = keywords.filter((k) => d.includes(k)).length;
338
- if (count > bestCount) {
339
- bestCount = count;
340
- best = type as IncidentType;
341
- }
169
+ if (count > bestCount) { bestCount = count; best = type as IncidentType; }
342
170
  }
343
171
  return best;
344
172
  }
@@ -353,90 +181,30 @@ interface Protocol {
353
181
  }
354
182
 
355
183
  const PROTOCOLS: Protocol[] = [
356
- {
357
- name: "Mass Casualty Incident (MCI)",
358
- triggers: {
359
- types: ["medical", "fire", "natural_disaster", "traffic"],
360
- minSeverity: "critical",
361
- },
362
- steps: [
363
- "Establish Incident Command",
364
- "Request mutual aid if more than 10 casualties estimated",
365
- "Set up triage area using START protocol: Immediate (red), Delayed (yellow), Minor (green), Deceased (black)",
366
- "Assign triage lead (EMS supervisor)",
367
- "Establish patient collection point and treatment area",
368
- "Coordinate helicopter landing zone if needed",
369
- "Notify receiving hospitals and activate surge protocols",
370
- ],
371
- requiredResources: ["ambulance", "ems_supervisor", "fire_engine"],
372
- },
373
- {
374
- name: "Structure Fire - Working Fire",
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",
375
189
  triggers: { types: ["fire"], minSeverity: "urgent" },
376
- steps: [
377
- "Dispatch minimum 2 engines and 1 ladder",
378
- "Establish incident command and 360-degree size-up",
379
- "Confirm water supply — nearest hydrant",
380
- "Search and rescue primary sweep",
381
- "Ventilation operations",
382
- "Establish RIT (Rapid Intervention Team)",
383
- "Request additional alarms if fire is not contained in 10 minutes",
384
- ],
385
- requiredResources: ["fire_engine"],
386
- },
387
- {
388
- name: "Hazardous Materials Response",
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",
389
193
  triggers: { types: ["hazmat"], minSeverity: "moderate" },
390
- steps: [
391
- "Identify the substance using placard numbers or SDS if available",
392
- "Establish hot, warm, and cold zones",
393
- "Evacuate downwind at minimum 1000 feet for unknown substances",
394
- "Deploy HazMat team in appropriate PPE level",
395
- "Set up decontamination corridor",
396
- "Monitor air quality and wind direction continuously",
397
- "Coordinate with poison control and environmental agency",
398
- ],
399
- requiredResources: ["hazmat_team", "fire_engine", "ambulance"],
400
- },
401
- {
402
- name: "Active Threat / Active Shooter",
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",
403
197
  triggers: { types: ["crime"], minSeverity: "critical" },
404
- steps: [
405
- "Dispatch SWAT and multiple patrol units",
406
- "Establish inner and outer perimeters",
407
- "Activate Rescue Task Force protocol — police escort for EMS into warm zone",
408
- "Stage ambulances at casualty collection point outside hot zone",
409
- "Request LifeFlight on standby",
410
- "Coordinate with school/building security for floor plans",
411
- "Establish family reunification point",
412
- ],
413
- requiredResources: ["swat", "police", "ambulance", "ems_supervisor"],
414
- },
415
- {
416
- name: "Multi-Vehicle Accident",
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",
417
201
  triggers: { types: ["traffic"], minSeverity: "urgent" },
418
- steps: [
419
- "Dispatch engine company for extrication capability",
420
- "Request traffic control units to shut down lanes",
421
- "Triage patients using START protocol",
422
- "Check for fuel or hazmat spills",
423
- "Establish landing zone if helicopter transport needed",
424
- "Coordinate with DOT for road closures and detours",
425
- ],
426
- requiredResources: ["fire_engine", "ambulance", "police"],
427
- },
428
- {
429
- name: "Cardiac Arrest Protocol",
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",
430
205
  triggers: { types: ["medical"], minSeverity: "critical" },
431
- steps: [
432
- "Instruct caller to begin CPR immediately — 30 compressions, 2 breaths",
433
- "Dispatch closest ALS unit and fire engine for first response",
434
- "Guide caller through AED use if available",
435
- "Time from call to first defibrillation is critical — target under 8 minutes",
436
- "Prepare for advanced airway management on arrival",
437
- ],
438
- requiredResources: ["ambulance", "fire_engine"],
439
- },
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"] },
440
208
  ];
441
209
 
442
210
  function getApplicableProtocols(
@@ -1366,117 +1134,37 @@ Radio style: "Medic-1, respond priority one to 400 Oak Street, report of cardiac
1366
1134
  }),
1367
1135
  execute: async ({ scenario }, ctx) => {
1368
1136
  const state = ctx.state as DispatchState;
1369
- const scenarios: Record<
1370
- string,
1371
- { incidents: Partial<Incident>[]; narrative: string }
1372
- > = {
1373
- "mass_casualty": {
1374
- narrative:
1375
- "Bus crash at Main and 5th. School bus vs delivery truck. Multiple pediatric patients. Fuel spill on roadway.",
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.",
1376
1143
  incidents: [
1377
- {
1378
- location: "Main St and 5th Ave intersection",
1379
- description:
1380
- "School bus collision with delivery truck, multiple children injured, bus on its side, fuel leaking",
1381
- type: "traffic",
1382
- severity: "critical",
1383
- },
1384
- {
1385
- location: "Main St and 5th Ave — fuel spill",
1386
- description:
1387
- "Diesel fuel spill from delivery truck spreading toward storm drain, approximately 50 gallons",
1388
- type: "hazmat",
1389
- severity: "urgent",
1390
- },
1391
- ],
1392
- },
1393
- "multi_alarm_fire": {
1394
- narrative:
1395
- "Working structure fire at 200 Industrial Parkway. 3-story warehouse, heavy smoke showing. Reports of workers possibly trapped on upper floors.",
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.",
1396
1148
  incidents: [
1397
- {
1398
- location: "200 Industrial Parkway",
1399
- description:
1400
- "3-story warehouse fully involved, heavy fire showing from all floors, possible trapped occupants on 2nd and 3rd floor, exposure buildings within 50 feet",
1401
- type: "fire",
1402
- severity: "critical",
1403
- },
1404
- {
1405
- location: "200 Industrial Parkway — medical",
1406
- description:
1407
- "2 workers with smoke inhalation evacuated from ground floor, one with burns to hands and arms",
1408
- type: "medical",
1409
- severity: "urgent",
1410
- },
1411
- ],
1412
- },
1413
- "active_shooter": {
1414
- narrative:
1415
- "Reports of active shooter at Riverside Mall. Multiple shots fired, crowds fleeing. At least 3 victims reported down in food court area.",
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.",
1416
1153
  incidents: [
1417
- {
1418
- location: "Riverside Mall, 1500 River Road food court",
1419
- description:
1420
- "Active shooter in food court area, multiple shots fired, at least 3 victims down, shooter last seen moving toward west entrance",
1421
- type: "crime",
1422
- severity: "critical",
1423
- },
1424
- {
1425
- location: "Riverside Mall parking lot",
1426
- description:
1427
- "Crowd crush injuries as people fled the building, several people trampled near east exit",
1428
- type: "medical",
1429
- severity: "urgent",
1430
- },
1431
- ],
1432
- },
1433
- "natural_disaster": {
1434
- narrative:
1435
- "EF-3 tornado touched down in residential area. Path of destruction along Oak Street corridor. Multiple structures collapsed. Power lines down.",
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.",
1436
1158
  incidents: [
1437
- {
1438
- location: "Oak Street between 10th and 15th",
1439
- description:
1440
- "Tornado damage, multiple homes collapsed, people trapped in rubble, gas lines ruptured",
1441
- type: "natural_disaster",
1442
- severity: "critical",
1443
- },
1444
- {
1445
- location: "Oak Street Elementary School",
1446
- description:
1447
- "School roof partially collapsed, staff sheltering students in interior rooms, requesting welfare check",
1448
- type: "natural_disaster",
1449
- severity: "critical",
1450
- },
1451
- {
1452
- location: "Oak Street and 12th — utility",
1453
- description:
1454
- "Multiple downed power lines sparking, gas main rupture, area needs immediate isolation",
1455
- type: "utility",
1456
- severity: "urgent",
1457
- },
1458
- ],
1459
- },
1460
- "highway_pileup": {
1461
- narrative:
1462
- "20-plus vehicle pileup on Interstate 95 southbound near mile marker 42. Fog conditions. Multiple entrapments. Tanker truck involved.",
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.",
1463
1164
  incidents: [
1464
- {
1465
- location: "I-95 southbound mile marker 42",
1466
- description:
1467
- "Multi-vehicle pileup, 20-plus vehicles, multiple entrapments, tanker truck involved with unknown cargo, heavy fog limiting visibility",
1468
- type: "traffic",
1469
- severity: "critical",
1470
- },
1471
- {
1472
- location: "I-95 southbound — hazmat",
1473
- description:
1474
- "Tanker truck leaking unknown liquid, placards not yet visible due to fog, setting up exclusion zone",
1475
- type: "hazmat",
1476
- severity: "critical",
1477
- },
1478
- ],
1479
- },
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
+ ] },
1480
1168
  };
1481
1169
 
1482
1170
  const s = scenarios[scenario];
@@ -609,9 +609,9 @@ VOICE:
609
609
 
610
610
  state: () => structuredClone(defaultState),
611
611
 
612
- // Auto-load saved game on connect (uses persistent uid from browser localStorage).
612
+ // Auto-load saved game on connect.
613
613
  onConnect: async (ctx) => {
614
- const saved = await ctx.kv.get<GameState>(`save:${ctx.sessionId}`);
614
+ const saved = await ctx.kv.get<GameState>("save:game");
615
615
  if (saved) Object.assign(ctx.state as GameState, saved);
616
616
  },
617
617
 
@@ -619,7 +619,7 @@ VOICE:
619
619
  onTurn: async (_text, ctx) => {
620
620
  const state = ctx.state as GameState;
621
621
  if (state.initialized) {
622
- await ctx.kv.set(`save:${ctx.sessionId}`, state);
622
+ await ctx.kv.set("save:game", state);
623
623
  }
624
624
  },
625
625