@alexkroman1/aai-cli 5.1.1 → 5.3.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 (99) hide show
  1. package/dist/_agent.d.ts +39 -0
  2. package/dist/_api-client.d.ts +37 -0
  3. package/dist/_bundler.d.ts +46 -0
  4. package/dist/_config.d.ts +57 -0
  5. package/dist/_default-html.d.ts +12 -0
  6. package/dist/_deploy.d.ts +28 -0
  7. package/dist/_dev-server.d.ts +74 -0
  8. package/dist/{_init-B-LEVTHx.mjs → _init-DU-sXH6S.mjs} +44 -65
  9. package/dist/_init.d.ts +7 -0
  10. package/dist/_mock-api.d.ts +22 -0
  11. package/dist/_mock-registry.d.ts +17 -0
  12. package/dist/_output.d.ts +50 -0
  13. package/dist/_server-common.d.ts +17 -0
  14. package/dist/_slug-api.d.ts +15 -0
  15. package/dist/_templates.d.ts +21 -0
  16. package/dist/_typecheck-gate.d.ts +7 -0
  17. package/dist/_ui.d.ts +22 -0
  18. package/dist/_utils.d.ts +48 -0
  19. package/dist/_vite-env.d.ts +8 -0
  20. package/dist/build.d.ts +16 -0
  21. package/dist/cli.d.ts +1 -0
  22. package/dist/cli.mjs +1 -1
  23. package/dist/client-bundler.d.ts +21 -0
  24. package/dist/delete.d.ts +18 -0
  25. package/dist/deploy.d.ts +15 -0
  26. package/dist/dev.d.ts +13 -0
  27. package/dist/{init-tc3Rbgrf.mjs → init-BjeK8crW.mjs} +1 -1
  28. package/dist/init.d.ts +24 -0
  29. package/dist/scaffold/.env.example +11 -0
  30. package/dist/scaffold/CLAUDE.md +938 -0
  31. package/dist/scaffold/global.d.ts +1 -0
  32. package/dist/scaffold/package.json +32 -0
  33. package/dist/scaffold/pnpm-workspace.yaml +13 -0
  34. package/dist/scaffold/tsconfig.json +19 -0
  35. package/dist/scaffold/vite.config.ts +14 -0
  36. package/dist/scaffold/vitest.config.ts +24 -0
  37. package/dist/secret.d.ts +25 -0
  38. package/dist/storage.d.ts +22 -0
  39. package/dist/templates/code-interpreter/agent.ts +11 -0
  40. package/dist/templates/code-interpreter/system-prompt.md +18 -0
  41. package/dist/templates/dispatch-center/agent.test.ts +153 -0
  42. package/dist/templates/dispatch-center/agent.ts +50 -0
  43. package/dist/templates/dispatch-center/client.tsx +378 -0
  44. package/dist/templates/dispatch-center/shared.ts +631 -0
  45. package/dist/templates/dispatch-center/system-prompt.md +39 -0
  46. package/dist/templates/dispatch-center/tools/incident_add_note.ts +27 -0
  47. package/dist/templates/dispatch-center/tools/incident_create.ts +79 -0
  48. package/dist/templates/dispatch-center/tools/incident_escalate.ts +88 -0
  49. package/dist/templates/dispatch-center/tools/incident_get.ts +36 -0
  50. package/dist/templates/dispatch-center/tools/incident_triage.ts +78 -0
  51. package/dist/templates/dispatch-center/tools/incident_update_status.ts +68 -0
  52. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +52 -0
  53. package/dist/templates/dispatch-center/tools/ops_protocols.ts +27 -0
  54. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +149 -0
  55. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +97 -0
  56. package/dist/templates/dispatch-center/tools/resources_get_available.ts +36 -0
  57. package/dist/templates/dispatch-center/tools/resources_update_status.ts +62 -0
  58. package/dist/templates/embedded-assets/agent.ts +55 -0
  59. package/dist/templates/embedded-assets/knowledge.json +20 -0
  60. package/dist/templates/health-assistant/agent.ts +174 -0
  61. package/dist/templates/health-assistant/system-prompt.md +17 -0
  62. package/dist/templates/infocom-adventure/agent.ts +119 -0
  63. package/dist/templates/infocom-adventure/client.tsx +279 -0
  64. package/dist/templates/infocom-adventure/shared.ts +38 -0
  65. package/dist/templates/infocom-adventure/system-prompt.md +43 -0
  66. package/dist/templates/math-buddy/agent.ts +16 -0
  67. package/dist/templates/math-buddy/system-prompt.md +12 -0
  68. package/dist/templates/night-owl/agent.ts +76 -0
  69. package/dist/templates/night-owl/client.tsx +130 -0
  70. package/dist/templates/night-owl/shared.ts +5 -0
  71. package/dist/templates/personal-finance/agent.ts +11 -0
  72. package/dist/templates/personal-finance/system-prompt.md +17 -0
  73. package/dist/templates/pipeline-simple/agent.test.ts +32 -0
  74. package/dist/templates/pipeline-simple/agent.ts +13 -0
  75. package/dist/templates/pizza-ordering/agent.test.ts +235 -0
  76. package/dist/templates/pizza-ordering/agent.ts +174 -0
  77. package/dist/templates/pizza-ordering/client.tsx +120 -0
  78. package/dist/templates/pizza-ordering/shared.ts +142 -0
  79. package/dist/templates/pizza-ordering/system-prompt.md +17 -0
  80. package/dist/templates/simple/agent.ts +5 -0
  81. package/dist/templates/solo-rpg/agent.test.ts +477 -0
  82. package/dist/templates/solo-rpg/agent.ts +37 -0
  83. package/dist/templates/solo-rpg/client.tsx +809 -0
  84. package/dist/templates/solo-rpg/shared.ts +747 -0
  85. package/dist/templates/solo-rpg/system-prompt.md +115 -0
  86. package/dist/templates/solo-rpg/tools/action_roll.ts +98 -0
  87. package/dist/templates/solo-rpg/tools/burn_momentum.ts +69 -0
  88. package/dist/templates/solo-rpg/tools/check_state.ts +11 -0
  89. package/dist/templates/solo-rpg/tools/load_game.ts +25 -0
  90. package/dist/templates/solo-rpg/tools/oracle.ts +248 -0
  91. package/dist/templates/solo-rpg/tools/save_game.ts +20 -0
  92. package/dist/templates/solo-rpg/tools/setup_character.ts +208 -0
  93. package/dist/templates/solo-rpg/tools/update_state.ts +170 -0
  94. package/dist/templates/web-researcher/agent.ts +11 -0
  95. package/dist/test.d.ts +39 -0
  96. package/dist/tsdown.config.d.ts +2 -0
  97. package/dist/typecheck.d.ts +26 -0
  98. package/dist/worker-bundler.d.ts +29 -0
  99. package/package.json +9 -6
@@ -0,0 +1,631 @@
1
+ // ─── Types ───────────────────────────────────────────────────────────────────
2
+
3
+ import type { ToolContext } from "@alexkroman1/aai";
4
+
5
+ export const SEVERITIES = ["critical", "urgent", "moderate", "minor"] as const;
6
+ export type Severity = (typeof SEVERITIES)[number];
7
+
8
+ export const INCIDENT_TYPES = [
9
+ "medical",
10
+ "fire",
11
+ "hazmat",
12
+ "traffic",
13
+ "crime",
14
+ "natural_disaster",
15
+ "utility",
16
+ "other",
17
+ ] as const;
18
+ export type IncidentType = (typeof INCIDENT_TYPES)[number];
19
+
20
+ export type Status =
21
+ | "incoming"
22
+ | "triaged"
23
+ | "dispatched"
24
+ | "en_route"
25
+ | "on_scene"
26
+ | "resolved"
27
+ | "escalated";
28
+
29
+ export const RESOURCE_TYPES = [
30
+ "ambulance",
31
+ "fire_engine",
32
+ "police",
33
+ "hazmat_team",
34
+ "helicopter",
35
+ "k9_unit",
36
+ "swat",
37
+ "ems_supervisor",
38
+ ] as const;
39
+
40
+ export const RESOURCE_STATUSES = [
41
+ "available",
42
+ "dispatched",
43
+ "en_route",
44
+ "on_scene",
45
+ "returning",
46
+ ] as const;
47
+
48
+ export interface Resource {
49
+ id: string;
50
+ type: (typeof RESOURCE_TYPES)[number];
51
+ callsign: string;
52
+ status: (typeof RESOURCE_STATUSES)[number];
53
+ assignedIncident: string | null;
54
+ eta: number | null;
55
+ capabilities: string[];
56
+ }
57
+
58
+ export interface Incident {
59
+ id: string;
60
+ type: IncidentType;
61
+ severity: Severity;
62
+ status: Status;
63
+ location: string;
64
+ description: string;
65
+ callerName: string;
66
+ callerPhone: string;
67
+ triageScore: number;
68
+ assignedResources: string[];
69
+ timeline: { time: number; event: string }[];
70
+ createdAt: number;
71
+ updatedAt: number;
72
+ escalationLevel: number;
73
+ protocolsActivated: string[];
74
+ casualties: { confirmed: number; estimated: number; treated: number };
75
+ hazards: string[];
76
+ }
77
+
78
+ export interface DispatchState {
79
+ incidents: Record<string, Incident>;
80
+ resources: Resource[];
81
+ incidentCounter: number;
82
+ mutualAidCounter: number;
83
+ alertLevel: "green" | "yellow" | "orange" | "red";
84
+ mutualAidRequested: boolean;
85
+ }
86
+
87
+ /**
88
+ * Trimmed, non-PII incident view sent to the browser. Never include caller
89
+ * name/phone here — this reaches the connected browser client, which only
90
+ * renders these fields.
91
+ *
92
+ * This is precisely why `syncState` takes a projection rather than a flag:
93
+ * `DispatchState` holds caller details, and the author decides what leaves
94
+ * the server.
95
+ */
96
+ export interface IncidentSummary {
97
+ id: string;
98
+ severity: Severity;
99
+ status: Status;
100
+ location: string;
101
+ }
102
+
103
+ export function incidentSummary(inc: Incident): IncidentSummary {
104
+ return { id: inc.id, severity: inc.severity, status: inc.status, location: inc.location };
105
+ }
106
+
107
+ /** One incident as the browser sees it — see `dashboardView`. */
108
+ export interface DashboardView {
109
+ systemAlertLevel: DispatchState["alertLevel"];
110
+ incidents: IncidentSummary[];
111
+ }
112
+
113
+ /** The `syncState` projection — the whole contract with client.tsx. */
114
+ export function dashboardView(state: StateSlot): DashboardView {
115
+ const dispatch = state.dispatch;
116
+ return {
117
+ systemAlertLevel: dispatch?.alertLevel ?? "green",
118
+ incidents: Object.values(dispatch?.incidents ?? {}).map(incidentSummary),
119
+ };
120
+ }
121
+
122
+ // ─── State helpers ───────────────────────────────────────────────────────────
123
+ // The dispatch board lives in `ctx.state`, the agent's per-session mutable
124
+ // state — sessions must not see each other's incidents, and ctx.state gives
125
+ // that isolation by construction. Nothing here needs to outlive the session.
126
+
127
+ export type StateSlot = { dispatch?: DispatchState };
128
+
129
+ // ─── Resource generation ────────────────────────────────────────────────────
130
+
131
+ const RESOURCE_DEFS: [string, Resource["type"], string, string[]][] = [
132
+ ["R1", "ambulance", "Medic-1", ["als", "cardiac", "pediatric"]],
133
+ ["R2", "ambulance", "Medic-2", ["als", "trauma"]],
134
+ ["R3", "ambulance", "Medic-3", ["bls"]],
135
+ ["R4", "fire_engine", "Engine-7", ["structural", "rescue", "ems_first_response"]],
136
+ ["R5", "fire_engine", "Ladder-2", ["aerial", "rescue", "ventilation"]],
137
+ ["R6", "police", "Unit-12", ["patrol", "traffic_control"]],
138
+ ["R7", "police", "Unit-15", ["patrol", "investigation"]],
139
+ ["R8", "hazmat_team", "HazMat-1", ["chemical", "biological", "radiological", "decon"]],
140
+ ["R9", "helicopter", "LifeFlight-1", ["medevac", "search_rescue", "thermal_imaging"]],
141
+ ["R10", "ems_supervisor", "EMS-Sup-1", ["mass_casualty", "triage_lead", "command"]],
142
+ ["R11", "k9_unit", "K9-3", ["tracking", "narcotics", "explosives"]],
143
+ ["R12", "swat", "TAC-1", ["tactical", "hostage_rescue", "high_risk_warrant"]],
144
+ ];
145
+
146
+ function generateResources(): Resource[] {
147
+ return RESOURCE_DEFS.map(([id, type, callsign, capabilities]) => ({
148
+ id,
149
+ type,
150
+ callsign,
151
+ capabilities,
152
+ status: "available" as const,
153
+ assignedIncident: null,
154
+ eta: null,
155
+ }));
156
+ }
157
+
158
+ export function createDefaultState(): DispatchState {
159
+ return {
160
+ incidents: {},
161
+ resources: generateResources(),
162
+ incidentCounter: 0,
163
+ mutualAidCounter: 0,
164
+ alertLevel: "green",
165
+ mutualAidRequested: false,
166
+ };
167
+ }
168
+
169
+ /** The session's live dispatch state. Mutations to the returned object
170
+ * stick — it is the object stored in `ctx.state`. */
171
+ export function getState(ctx: ToolContext): DispatchState {
172
+ const slot = ctx.state as StateSlot;
173
+ slot.dispatch ??= createDefaultState();
174
+ return slot.dispatch;
175
+ }
176
+
177
+ // ─── Serialized state updates ────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Growth caps. The whole dispatch state is one object whose summaries feed
181
+ * both the LLM and the dashboard event, so resolved incidents and long
182
+ * timelines must be pruned or a long session's payloads grow without bound.
183
+ */
184
+ const MAX_RESOLVED_KEPT = 10;
185
+ const MAX_TIMELINE_ENTRIES = 50;
186
+
187
+ function pruneState(state: DispatchState): void {
188
+ const resolved = Object.values(state.incidents)
189
+ .filter((i) => i.status === "resolved")
190
+ .sort((a, b) => b.updatedAt - a.updatedAt);
191
+ for (const inc of resolved.slice(MAX_RESOLVED_KEPT)) {
192
+ delete state.incidents[inc.id];
193
+ }
194
+ for (const inc of Object.values(state.incidents)) {
195
+ if (inc.timeline.length > MAX_TIMELINE_ENTRIES) {
196
+ inc.timeline = inc.timeline.slice(-MAX_TIMELINE_ENTRIES);
197
+ }
198
+ }
199
+ }
200
+
201
+ const sessionLocks = new Map<string, Promise<unknown>>();
202
+
203
+ /**
204
+ * Serialized update of the session's dispatch state.
205
+ *
206
+ * The LLM loop executes parallel tool calls concurrently. The state lives in
207
+ * `ctx.state` now (one shared object, no snapshot/save round-trip), but a
208
+ * mutator may be async, and two interleaving async mutators can each observe
209
+ * the other's half-applied changes. This per-session promise chain makes
210
+ * each update run against the previous one's finished result. It also
211
+ * centralizes the shared bookkeeping every mutating tool needs: pruning and
212
+ * alert-level recalculation.
213
+ *
214
+ * There is no post-save callback: pushing the board to the client used to
215
+ * need one, and `syncState` now does it after every tool call.
216
+ */
217
+ export async function updateState<R>(
218
+ ctx: ToolContext,
219
+ mutator: (state: DispatchState) => R | Promise<R>,
220
+ ): Promise<R> {
221
+ const prev = sessionLocks.get(ctx.sessionId) ?? Promise.resolve();
222
+ const run = prev.then(async () => {
223
+ const state = getState(ctx);
224
+ const result = await mutator(state);
225
+ pruneState(state);
226
+ recalculateAlertLevel(state);
227
+ return result;
228
+ });
229
+ // Keep the chain alive across failures, and drop it once idle.
230
+ const tail = run.catch(() => {});
231
+ sessionLocks.set(ctx.sessionId, tail);
232
+ tail.then(() => {
233
+ if (sessionLocks.get(ctx.sessionId) === tail) sessionLocks.delete(ctx.sessionId);
234
+ });
235
+ return run;
236
+ }
237
+
238
+ // ─── Incident helpers ────────────────────────────────────────────────────────
239
+
240
+ export function createIncident(state: DispatchState, overrides: Partial<Incident>): Incident {
241
+ state.incidentCounter++;
242
+ const id = `INC-${String(state.incidentCounter).padStart(4, "0")}`;
243
+ const time = Date.now();
244
+ const incident: Incident = {
245
+ id,
246
+ type: "other",
247
+ severity: "moderate",
248
+ status: "incoming",
249
+ location: "Unknown",
250
+ description: "",
251
+ callerName: "Unknown",
252
+ callerPhone: "Unknown",
253
+ triageScore: 0,
254
+ assignedResources: [],
255
+ timeline: [],
256
+ createdAt: time,
257
+ updatedAt: time,
258
+ escalationLevel: 0,
259
+ protocolsActivated: [],
260
+ casualties: { confirmed: 0, estimated: 0, treated: 0 },
261
+ hazards: [],
262
+ ...overrides,
263
+ };
264
+ state.incidents[id] = incident;
265
+ return incident;
266
+ }
267
+
268
+ export function findIncident(
269
+ state: DispatchState,
270
+ incidentId: string,
271
+ ): Incident | { error: string } {
272
+ return state.incidents[incidentId] ?? { error: `Incident ${incidentId} not found` };
273
+ }
274
+
275
+ /** Append a timeline entry and touch `updatedAt`. */
276
+ export function logEvent(inc: Incident, event: string): void {
277
+ const time = Date.now();
278
+ inc.timeline.push({ time, event });
279
+ inc.updatedAt = time;
280
+ }
281
+
282
+ /** Minutes since the incident was created, rounded. */
283
+ export function incidentAgeMinutes(inc: Incident): number {
284
+ return Math.round((Date.now() - inc.createdAt) / 60_000);
285
+ }
286
+
287
+ /** A resource as tool results describe it to the LLM. */
288
+ export function resourceBrief(r: Resource): {
289
+ callsign: string;
290
+ type: Resource["type"];
291
+ capabilities: string[];
292
+ } {
293
+ return { callsign: r.callsign, type: r.type, capabilities: r.capabilities };
294
+ }
295
+
296
+ /**
297
+ * Status-transition guard. `resolved` is terminal: a resolved incident's
298
+ * resources have been released (and possibly reassigned), so escalating,
299
+ * re-resolving, or dispatching to it would corrupt resource assignments.
300
+ */
301
+ export function assertNotResolved(inc: Incident, action: string): { error: string } | null {
302
+ if (inc.status === "resolved") {
303
+ return {
304
+ error: `Incident ${inc.id} is resolved — cannot ${action}. Create a new incident if the situation has reopened.`,
305
+ };
306
+ }
307
+ return null;
308
+ }
309
+
310
+ // ─── Triage & scoring ────────────────────────────────────────────────────────
311
+
312
+ const SEVERITY_WEIGHTS: Record<Severity, number> = {
313
+ critical: 100,
314
+ urgent: 70,
315
+ moderate: 40,
316
+ minor: 10,
317
+ };
318
+
319
+ const TYPE_MULTIPLIERS: Record<IncidentType, number> = {
320
+ medical: 1.2,
321
+ fire: 1.3,
322
+ hazmat: 1.5,
323
+ traffic: 1.0,
324
+ crime: 1.1,
325
+ natural_disaster: 1.8,
326
+ utility: 0.8,
327
+ other: 0.7,
328
+ };
329
+
330
+ export function calculateTriageScore(
331
+ severity: Severity,
332
+ type: IncidentType,
333
+ casualties: number,
334
+ hazards: number,
335
+ ): number {
336
+ let score = SEVERITY_WEIGHTS[severity] * TYPE_MULTIPLIERS[type];
337
+ score += Math.min(casualties * 15, 60);
338
+ score += Math.min(hazards * 10, 30);
339
+ return Math.round(Math.max(0, Math.min(score, 250)));
340
+ }
341
+
342
+ const SEVERITY_KEYWORDS: [Severity, string[]][] = [
343
+ [
344
+ "critical",
345
+ [
346
+ "unconscious",
347
+ "not breathing",
348
+ "cardiac arrest",
349
+ "trapped",
350
+ "collapse",
351
+ "explosion",
352
+ "active shooter",
353
+ "mass casualty",
354
+ ],
355
+ ],
356
+ [
357
+ "urgent",
358
+ [
359
+ "bleeding",
360
+ "chest pain",
361
+ "difficulty breathing",
362
+ "fire",
363
+ "hazmat",
364
+ "shooting",
365
+ "stabbing",
366
+ "multi-vehicle",
367
+ ],
368
+ ],
369
+ ["moderate", ["fall", "broken", "fracture", "smoke", "minor fire", "assault", "theft"]],
370
+ ];
371
+
372
+ export function recommendSeverity(description: string): Severity {
373
+ const d = description.toLowerCase();
374
+ for (const [sev, kws] of SEVERITY_KEYWORDS) {
375
+ if (kws.some((k) => d.includes(k))) return sev;
376
+ }
377
+ return "minor";
378
+ }
379
+
380
+ const TYPE_KEYWORDS: Record<IncidentType, string[]> = {
381
+ medical: [
382
+ "chest pain",
383
+ "breathing",
384
+ "unconscious",
385
+ "seizure",
386
+ "allergic",
387
+ "overdose",
388
+ "cardiac",
389
+ "stroke",
390
+ "diabetic",
391
+ "bleeding",
392
+ "fall",
393
+ "injury",
394
+ ],
395
+ fire: ["fire", "smoke", "flames", "burning", "arson"],
396
+ hazmat: ["chemical", "spill", "gas leak", "fumes", "radiation", "contamination", "hazmat"],
397
+ traffic: [
398
+ "accident",
399
+ "crash",
400
+ "collision",
401
+ "vehicle",
402
+ "rollover",
403
+ "pedestrian struck",
404
+ "hit and run",
405
+ ],
406
+ crime: [
407
+ "robbery",
408
+ "assault",
409
+ "shooting",
410
+ "stabbing",
411
+ "burglar",
412
+ "theft",
413
+ "domestic",
414
+ "hostage",
415
+ "active shooter",
416
+ ],
417
+ natural_disaster: [
418
+ "earthquake",
419
+ "flood",
420
+ "tornado",
421
+ "hurricane",
422
+ "landslide",
423
+ "wildfire",
424
+ "tsunami",
425
+ ],
426
+ utility: ["power outage", "downed line", "water main", "gas main", "transformer"],
427
+ other: [],
428
+ };
429
+
430
+ export function recommendType(description: string): IncidentType {
431
+ const d = description.toLowerCase();
432
+ let best: IncidentType = "other";
433
+ let bestCount = 0;
434
+ for (const [type, keywords] of Object.entries(TYPE_KEYWORDS)) {
435
+ const count = keywords.filter((k) => d.includes(k)).length;
436
+ if (count > bestCount) {
437
+ bestCount = count;
438
+ best = type as IncidentType;
439
+ }
440
+ }
441
+ return best;
442
+ }
443
+
444
+ // ─── Protocol engine ─────────────────────────────────────────────────────────
445
+
446
+ interface Protocol {
447
+ name: string;
448
+ triggers: { types: IncidentType[]; minSeverity: Severity };
449
+ steps: string[];
450
+ requiredResources: Resource["type"][];
451
+ }
452
+
453
+ const PROTOCOLS: Protocol[] = [
454
+ {
455
+ name: "Mass Casualty Incident (MCI)",
456
+ triggers: {
457
+ types: ["medical", "fire", "natural_disaster", "traffic"],
458
+ minSeverity: "critical",
459
+ },
460
+ steps: [
461
+ "Establish Incident Command",
462
+ "Request mutual aid if >10 casualties",
463
+ "Set up triage: Immediate (red), Delayed (yellow), Minor (green), Deceased (black)",
464
+ "Assign triage lead (EMS supervisor)",
465
+ "Establish patient collection point",
466
+ "Coordinate helicopter landing zone if needed",
467
+ "Notify receiving hospitals and activate surge protocols",
468
+ ],
469
+ requiredResources: ["ambulance", "ems_supervisor", "fire_engine"],
470
+ },
471
+ {
472
+ name: "Structure Fire - Working Fire",
473
+ triggers: { types: ["fire"], minSeverity: "urgent" },
474
+ steps: [
475
+ "Dispatch minimum 2 engines and 1 ladder",
476
+ "Establish incident command and 360-degree size-up",
477
+ "Confirm water supply",
478
+ "Search and rescue primary sweep",
479
+ "Ventilation operations",
480
+ "Establish RIT (Rapid Intervention Team)",
481
+ "Request additional alarms if not contained in 10 min",
482
+ ],
483
+ requiredResources: ["fire_engine"],
484
+ },
485
+ {
486
+ name: "Hazardous Materials Response",
487
+ triggers: { types: ["hazmat"], minSeverity: "moderate" },
488
+ steps: [
489
+ "Identify substance via placard numbers or SDS",
490
+ "Establish hot, warm, and cold zones",
491
+ "Evacuate downwind 1000+ feet for unknowns",
492
+ "Deploy HazMat team in appropriate PPE",
493
+ "Set up decontamination corridor",
494
+ "Monitor air quality and wind continuously",
495
+ "Coordinate with poison control",
496
+ ],
497
+ requiredResources: ["hazmat_team", "fire_engine", "ambulance"],
498
+ },
499
+ {
500
+ name: "Active Threat / Active Shooter",
501
+ triggers: { types: ["crime"], minSeverity: "critical" },
502
+ steps: [
503
+ "Dispatch SWAT and multiple patrol units",
504
+ "Establish inner and outer perimeters",
505
+ "Activate Rescue Task Force — police escort EMS into warm zone",
506
+ "Stage ambulances outside hot zone",
507
+ "Request LifeFlight on standby",
508
+ "Get building floor plans",
509
+ "Establish family reunification point",
510
+ ],
511
+ requiredResources: ["swat", "police", "ambulance", "ems_supervisor"],
512
+ },
513
+ {
514
+ name: "Multi-Vehicle Accident",
515
+ triggers: { types: ["traffic"], minSeverity: "urgent" },
516
+ steps: [
517
+ "Dispatch engine for extrication",
518
+ "Request traffic control to shut lanes",
519
+ "Triage using START protocol",
520
+ "Check for fuel/hazmat spills",
521
+ "Establish helicopter landing zone if needed",
522
+ "Coordinate with DOT for road closures",
523
+ ],
524
+ requiredResources: ["fire_engine", "ambulance", "police"],
525
+ },
526
+ {
527
+ name: "Cardiac Arrest Protocol",
528
+ triggers: { types: ["medical"], minSeverity: "critical" },
529
+ steps: [
530
+ "Instruct caller: CPR — 30 compressions, 2 breaths",
531
+ "Dispatch closest ALS unit and fire engine",
532
+ "Guide caller through AED use if available",
533
+ "Target first defibrillation under 8 minutes",
534
+ "Prepare for advanced airway management",
535
+ ],
536
+ requiredResources: ["ambulance", "fire_engine"],
537
+ },
538
+ ];
539
+
540
+ const SEVERITY_RANK: Record<Severity, number> = {
541
+ critical: 4,
542
+ urgent: 3,
543
+ moderate: 2,
544
+ minor: 1,
545
+ };
546
+
547
+ export function getApplicableProtocols(type: IncidentType, severity: Severity): Protocol[] {
548
+ return PROTOCOLS.filter(
549
+ (p) =>
550
+ p.triggers.types.includes(type) &&
551
+ SEVERITY_RANK[severity] >= SEVERITY_RANK[p.triggers.minSeverity],
552
+ );
553
+ }
554
+
555
+ // ─── Resource recommendation engine ──────────────────────────────────────────
556
+
557
+ const BASE_NEEDS: Record<IncidentType, Resource["type"][]> = {
558
+ medical: ["ambulance"],
559
+ fire: ["fire_engine", "ambulance"],
560
+ hazmat: ["hazmat_team", "fire_engine", "ambulance"],
561
+ traffic: ["police", "ambulance", "fire_engine"],
562
+ crime: ["police"],
563
+ natural_disaster: ["fire_engine", "ambulance", "police"],
564
+ utility: ["fire_engine"],
565
+ other: [],
566
+ };
567
+
568
+ export function recommendResources(
569
+ type: IncidentType,
570
+ severity: Severity,
571
+ state: DispatchState,
572
+ ): Resource[] {
573
+ const needed: Resource["type"][] = [];
574
+
575
+ needed.push(...(BASE_NEEDS[type] || []));
576
+
577
+ if (severity === "critical") {
578
+ if (!needed.includes("ambulance")) needed.push("ambulance");
579
+ needed.push("ems_supervisor");
580
+ if (type === "crime") needed.push("swat");
581
+ }
582
+ if (severity === "urgent" && type === "fire") {
583
+ needed.push("fire_engine");
584
+ }
585
+
586
+ const recommended: Resource[] = [];
587
+ const usedIds = new Set<string>();
588
+
589
+ for (const needType of needed) {
590
+ const available = state.resources.find(
591
+ (r) => r.type === needType && r.status === "available" && !usedIds.has(r.id),
592
+ );
593
+ if (available) {
594
+ recommended.push(available);
595
+ usedIds.add(available.id);
596
+ }
597
+ }
598
+
599
+ return recommended;
600
+ }
601
+
602
+ // ─── System alert level calculation ──────────────────────────────────────────
603
+
604
+ /** Fraction of resources not currently available, 0–1. The alert level and
605
+ * the dashboard's displayed utilization both derive from this, so they can
606
+ * never disagree about what "utilization" means. */
607
+ export function resourceUtilization(state: DispatchState): number {
608
+ const available = state.resources.filter((r) => r.status === "available").length;
609
+ return 1 - available / state.resources.length;
610
+ }
611
+
612
+ export function recalculateAlertLevel(state: DispatchState): void {
613
+ const activeIncidents = Object.values(state.incidents).filter((i) => i.status !== "resolved");
614
+ const criticalCount = activeIncidents.filter((i) => i.severity === "critical").length;
615
+ const totalActive = activeIncidents.length;
616
+ const utilization = resourceUtilization(state);
617
+
618
+ if (criticalCount >= 3 || utilization > 0.85 || totalActive >= 8) {
619
+ state.alertLevel = "red";
620
+ } else if (criticalCount >= 2 || utilization > 0.65 || totalActive >= 5) {
621
+ state.alertLevel = "orange";
622
+ } else if (criticalCount >= 1 || utilization > 0.4 || totalActive >= 3) {
623
+ state.alertLevel = "yellow";
624
+ } else {
625
+ state.alertLevel = "green";
626
+ }
627
+
628
+ // Mutual aid tracks system posture: requested at red alert, stood down
629
+ // when the alert level drops back below red.
630
+ state.mutualAidRequested = state.alertLevel === "red";
631
+ }
@@ -0,0 +1,39 @@
1
+ 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.
2
+
3
+ Your role combines call-taker, dispatcher, and incident commander. You speak like an experienced dispatcher: calm, precise, and authoritative. Never panic. Use brevity codes and dispatch terminology naturally.
4
+
5
+ Your tools:
6
+
7
+ INCIDENT MANAGEMENT:
8
+ - incident_create: Log a new incident. Ask for location first, then nature of emergency, then caller info. Speed matters for critical calls.
9
+ - incident_triage: After creating, assess severity. The system recommends severity, type, and protocols. Review and confirm or override.
10
+ - incident_update_status: Move incidents through the workflow (en_route, on_scene, resolved, escalated).
11
+ - incident_get: Get details on a specific incident.
12
+ - incident_escalate: Escalate when an incident exceeds current capacity or severity increases.
13
+ - incident_add_note: Add ongoing situational updates.
14
+
15
+ RESOURCE MANAGEMENT:
16
+ - resources_dispatch: Assign units. The system recommends optimal resources based on incident type and severity. You can also manually dispatch specific units.
17
+ - resources_get_available: See what units are free.
18
+ - resources_update_status: Update unit status when units radio in.
19
+
20
+ OPERATIONS:
21
+ - ops_dashboard: Get the full operational picture.
22
+ - ops_protocols: Retrieve step-by-step response protocols. Follow them precisely for critical incidents.
23
+ - ops_run_scenario: Run training exercises.
24
+
25
+ SEARCH: Use web_search to look up hazmat placard numbers, drug interactions, building addresses, or other reference information during active incidents.
26
+
27
+ CALCULATIONS: Use run_code for ETA calculations, resource optimization, or casualty estimates.
28
+
29
+ Operational rules:
30
+ - Location is always the first priority in any emergency call
31
+ - Critical incidents get immediate dispatch, triage can happen simultaneously
32
+ - Never leave a critical incident without at least one resource dispatched
33
+ - Monitor resource utilization. If it exceeds 65 percent, warn about degraded capacity
34
+ - At red alert level, recommend mutual aid from neighboring jurisdictions
35
+ - Track time on all incidents. Escalate if critical incidents have no on-scene resources within 8 minutes
36
+ - When reporting the dashboard, lead with the most severe active incidents
37
+ - Use plain language for medical instructions to callers, dispatch terminology for unit communications
38
+
39
+ Radio style: "Medic-1, respond priority one to 400 Oak Street, report of cardiac arrest, CPR in progress." Keep it tight and professional.
@@ -0,0 +1,27 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { findIncident, logEvent, updateState } from "../shared.ts";
4
+
5
+ export const incidentAddNote = tool({
6
+ description: "Add a situational update note to an incident's timeline.",
7
+ parameters: z.object({
8
+ incidentId: z.string().max(20).describe("The incident ID"),
9
+ note: z.string().max(1000).describe("The note to add"),
10
+ source: z.string().max(100).describe("Who reported this — unit callsign or caller").optional(),
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;
16
+
17
+ const entry = args.source ? `[${args.source}] ${args.note}` : args.note;
18
+ logEvent(inc, entry);
19
+
20
+ return {
21
+ incidentId: args.incidentId,
22
+ noteAdded: entry,
23
+ timelineEntries: inc.timeline.length,
24
+ };
25
+ });
26
+ },
27
+ });