@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,79 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import {
4
+ calculateTriageScore,
5
+ createIncident,
6
+ getApplicableProtocols,
7
+ recommendResources,
8
+ recommendSeverity,
9
+ recommendType,
10
+ resourceBrief,
11
+ updateState,
12
+ } from "../shared.ts";
13
+
14
+ export const incidentCreate = tool({
15
+ description: "Create a new incident from an incoming emergency call.",
16
+ parameters: z.object({
17
+ location: z.string().max(300).describe("Address or location description"),
18
+ description: z.string().max(2000).describe("Nature of the emergency as described by caller"),
19
+ callerName: z.string().max(120).describe("Caller's name").optional(),
20
+ callerPhone: z.string().max(40).describe("Callback number").optional(),
21
+ estimatedCasualties: z
22
+ .number()
23
+ .int()
24
+ .nonnegative()
25
+ .describe("Estimated number of casualties if known")
26
+ .optional(),
27
+ hazards: z
28
+ .array(z.string().max(100))
29
+ .max(20)
30
+ .describe("Known hazards: fire, chemical, electrical, structural, weapons")
31
+ .optional(),
32
+ }),
33
+ async execute(args, ctx) {
34
+ return updateState(ctx, (state) => {
35
+ const recSeverity = recommendSeverity(args.description);
36
+ const recType = recommendType(args.description);
37
+ const triageScore = calculateTriageScore(
38
+ recSeverity,
39
+ recType,
40
+ args.estimatedCasualties ?? 0,
41
+ args.hazards?.length ?? 0,
42
+ );
43
+
44
+ const incident = createIncident(state, {
45
+ type: recType,
46
+ severity: recSeverity,
47
+ location: args.location,
48
+ description: args.description,
49
+ callerName: args.callerName ?? "Unknown",
50
+ callerPhone: args.callerPhone ?? "Unknown",
51
+ triageScore,
52
+ timeline: [{ time: Date.now(), event: `Incident created: ${args.description}` }],
53
+ casualties: {
54
+ confirmed: 0,
55
+ estimated: args.estimatedCasualties ?? 0,
56
+ treated: 0,
57
+ },
58
+ hazards: args.hazards ?? [],
59
+ });
60
+ const id = incident.id;
61
+
62
+ const protocols = getApplicableProtocols(recType, recSeverity);
63
+ const recommended = recommendResources(recType, recSeverity, state);
64
+
65
+ return {
66
+ incidentId: id,
67
+ recommendedSeverity: recSeverity,
68
+ recommendedType: recType,
69
+ triageScore,
70
+ applicableProtocols: protocols.map((p) => p.name),
71
+ recommendedResources: recommended.map(resourceBrief),
72
+ message:
73
+ recSeverity === "critical"
74
+ ? `PRIORITY ONE — ${id} created. Immediate dispatch recommended. ${protocols.length} protocol(s) applicable.`
75
+ : `${id} created. Triage score ${triageScore}. ${recommended.length} resource(s) recommended.`,
76
+ };
77
+ });
78
+ },
79
+ });
@@ -0,0 +1,88 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import {
4
+ assertNotResolved,
5
+ calculateTriageScore,
6
+ findIncident,
7
+ logEvent,
8
+ recommendResources,
9
+ updateState,
10
+ } from "../shared.ts";
11
+
12
+ export const incidentEscalate = tool({
13
+ description: "Escalate an incident when it exceeds current capacity or severity increases.",
14
+ parameters: z.object({
15
+ incidentId: z.string().max(20).describe("The incident ID"),
16
+ reason: z.string().max(1000).describe("Reason for escalation"),
17
+ requestMutualAid: z
18
+ .boolean()
19
+ .describe("Whether to request mutual aid from neighboring jurisdictions")
20
+ .optional(),
21
+ newSeverity: z.enum(["critical", "urgent"]).describe("Escalated severity level").optional(),
22
+ }),
23
+ async execute(args, ctx) {
24
+ return updateState(ctx, (state) => {
25
+ const inc = findIncident(state, args.incidentId);
26
+ if ("error" in inc) return inc;
27
+ const blocked = assertNotResolved(inc, "escalate");
28
+ if (blocked) return blocked;
29
+
30
+ inc.escalationLevel++;
31
+ if (args.newSeverity) inc.severity = args.newSeverity;
32
+ inc.status = "escalated";
33
+ logEvent(inc, `ESCALATED (Level ${inc.escalationLevel}): ${args.reason}`);
34
+
35
+ if (args.requestMutualAid) {
36
+ logEvent(inc, "Mutual aid requested from neighboring jurisdictions");
37
+ // Counter-based ids/callsigns: Date.now() collides across rapid
38
+ // escalations, and duplicate callsigns break dispatch-by-callsign.
39
+ const medicN = ++state.mutualAidCounter;
40
+ const engineN = ++state.mutualAidCounter;
41
+ state.resources.push(
42
+ {
43
+ id: `MA-${medicN}`,
44
+ type: "ambulance",
45
+ callsign: `Mutual-Aid-Medic-${medicN}`,
46
+ status: "available",
47
+ assignedIncident: null,
48
+ eta: null,
49
+ capabilities: ["als"],
50
+ },
51
+ {
52
+ id: `MA-${engineN}`,
53
+ type: "fire_engine",
54
+ callsign: `Mutual-Aid-Engine-${engineN}`,
55
+ status: "available",
56
+ assignedIncident: null,
57
+ eta: null,
58
+ capabilities: ["structural"],
59
+ },
60
+ );
61
+ }
62
+
63
+ inc.triageScore = calculateTriageScore(
64
+ inc.severity,
65
+ inc.type,
66
+ inc.casualties.estimated,
67
+ inc.hazards.length,
68
+ );
69
+
70
+ const additionalResources = recommendResources(inc.type, inc.severity, state).filter(
71
+ (r) => !inc.assignedResources.includes(r.id),
72
+ );
73
+
74
+ return {
75
+ incidentId: args.incidentId,
76
+ escalationLevel: inc.escalationLevel,
77
+ newSeverity: inc.severity,
78
+ newTriageScore: inc.triageScore,
79
+ mutualAidRequested: args.requestMutualAid ?? false,
80
+ additionalResourcesAvailable: additionalResources.map((r) => ({
81
+ callsign: r.callsign,
82
+ type: r.type,
83
+ })),
84
+ message: `ESCALATION CONFIRMED — ${args.incidentId} now Level ${inc.escalationLevel}. ${additionalResources.length} additional resource(s) available for dispatch.`,
85
+ };
86
+ });
87
+ },
88
+ });
@@ -0,0 +1,36 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { findIncident, getApplicableProtocols, getState, incidentAgeMinutes } from "../shared.ts";
4
+
5
+ export const incidentGet = tool({
6
+ description: "Get full details on a specific incident including timeline and assigned resources.",
7
+ parameters: z.object({
8
+ incidentId: z.string().max(20).describe("The incident ID"),
9
+ }),
10
+ async execute(args, ctx) {
11
+ const state = getState(ctx);
12
+ const inc = findIncident(state, args.incidentId);
13
+ if ("error" in inc) return inc;
14
+
15
+ const assignedResourceDetails = inc.assignedResources
16
+ .map((rId) => {
17
+ const r = state.resources.find((r) => r.id === rId);
18
+ return r
19
+ ? {
20
+ callsign: r.callsign,
21
+ type: r.type,
22
+ status: r.status,
23
+ eta: r.eta,
24
+ }
25
+ : null;
26
+ })
27
+ .filter(Boolean);
28
+
29
+ return {
30
+ ...inc,
31
+ ageMinutes: incidentAgeMinutes(inc),
32
+ assignedResourceDetails,
33
+ applicableProtocols: getApplicableProtocols(inc.type, inc.severity).map((p) => p.name),
34
+ };
35
+ },
36
+ });
@@ -0,0 +1,78 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import {
4
+ assertNotResolved,
5
+ calculateTriageScore,
6
+ findIncident,
7
+ getApplicableProtocols,
8
+ INCIDENT_TYPES,
9
+ logEvent,
10
+ recommendResources,
11
+ SEVERITIES,
12
+ updateState,
13
+ } from "../shared.ts";
14
+
15
+ export const incidentTriage = tool({
16
+ description:
17
+ "Triage an incident — confirm or override severity, type, hazards, and casualty count.",
18
+ parameters: z.object({
19
+ incidentId: z.string().max(20).describe("The incident ID"),
20
+ severity: z.enum(SEVERITIES).describe("Confirmed severity after triage").optional(),
21
+ type: z.enum(INCIDENT_TYPES).describe("Confirmed incident type").optional(),
22
+ additionalHazards: z
23
+ .array(z.string().max(100))
24
+ .max(20)
25
+ .describe("Any additional hazards identified")
26
+ .optional(),
27
+ casualtyUpdate: z.number().int().nonnegative().describe("Updated casualty count").optional(),
28
+ notes: z.string().max(1000).describe("Triage notes").optional(),
29
+ }),
30
+ async execute(args, ctx) {
31
+ return updateState(ctx, (state) => {
32
+ const inc = findIncident(state, args.incidentId);
33
+ if ("error" in inc) return inc;
34
+ const blocked = assertNotResolved(inc, "triage");
35
+ if (blocked) return blocked;
36
+
37
+ if (args.severity) inc.severity = args.severity;
38
+ if (args.type) inc.type = args.type;
39
+ for (const hazard of args.additionalHazards ?? []) {
40
+ if (!inc.hazards.includes(hazard)) inc.hazards.push(hazard);
41
+ }
42
+ if (args.casualtyUpdate !== undefined) {
43
+ inc.casualties.estimated = args.casualtyUpdate;
44
+ }
45
+ if (args.notes) logEvent(inc, `Triage note: ${args.notes}`);
46
+
47
+ inc.triageScore = calculateTriageScore(
48
+ inc.severity,
49
+ inc.type,
50
+ inc.casualties.estimated,
51
+ inc.hazards.length,
52
+ );
53
+ inc.status = "triaged";
54
+ logEvent(inc, `Triaged: ${inc.severity} ${inc.type}, score ${inc.triageScore}`);
55
+
56
+ const protocols = getApplicableProtocols(inc.type, inc.severity);
57
+ const recommended = recommendResources(inc.type, inc.severity, state);
58
+
59
+ return {
60
+ incidentId: args.incidentId,
61
+ severity: inc.severity,
62
+ type: inc.type,
63
+ triageScore: inc.triageScore,
64
+ hazards: inc.hazards,
65
+ estimatedCasualties: inc.casualties.estimated,
66
+ protocols: protocols.map((p) => ({
67
+ name: p.name,
68
+ steps: p.steps,
69
+ requiredResources: p.requiredResources,
70
+ })),
71
+ recommendedResources: recommended.map((r) => ({
72
+ callsign: r.callsign,
73
+ type: r.type,
74
+ })),
75
+ };
76
+ });
77
+ },
78
+ });
@@ -0,0 +1,68 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { assertNotResolved, findIncident, logEvent, updateState } from "../shared.ts";
4
+
5
+ export const incidentUpdateStatus = tool({
6
+ description: "Update an incident's status (en_route, on_scene, resolved, escalated).",
7
+ parameters: z.object({
8
+ incidentId: z.string().max(20).describe("The incident ID"),
9
+ status: z.enum(["en_route", "on_scene", "resolved", "escalated"]).describe("New status"),
10
+ notes: z.string().max(1000).describe("Status update notes").optional(),
11
+ casualtyUpdate: z
12
+ .object({
13
+ confirmed: z.number().int().nonnegative().optional(),
14
+ treated: z.number().int().nonnegative().optional(),
15
+ })
16
+ .describe("Updated casualty numbers")
17
+ .optional(),
18
+ }),
19
+ async execute(args, ctx) {
20
+ return updateState(ctx, (state) => {
21
+ const inc = findIncident(state, args.incidentId);
22
+ if ("error" in inc) return inc;
23
+ const blocked = assertNotResolved(inc, `set status to ${args.status}`);
24
+ if (blocked) return blocked;
25
+
26
+ inc.status = args.status;
27
+ logEvent(inc, `Status → ${args.status}${args.notes ? `: ${args.notes}` : ""}`);
28
+
29
+ if (args.casualtyUpdate) {
30
+ if (args.casualtyUpdate.confirmed !== undefined) {
31
+ inc.casualties.confirmed = args.casualtyUpdate.confirmed;
32
+ }
33
+ if (args.casualtyUpdate.treated !== undefined) {
34
+ inc.casualties.treated = args.casualtyUpdate.treated;
35
+ }
36
+ }
37
+
38
+ // Only touch resources still assigned to THIS incident — a unit that
39
+ // radioed available and was re-dispatched elsewhere belongs to its
40
+ // new incident now.
41
+ const ownResources = state.resources.filter((r) => r.assignedIncident === inc.id);
42
+
43
+ if (args.status === "resolved") {
44
+ for (const r of ownResources) {
45
+ r.status = "returning";
46
+ r.assignedIncident = null;
47
+ r.eta = null;
48
+ }
49
+ inc.assignedResources = [];
50
+ logEvent(inc, "All resources released — incident closed");
51
+ }
52
+
53
+ if (args.status === "en_route" || args.status === "on_scene") {
54
+ for (const r of ownResources) {
55
+ r.status = args.status;
56
+ if (args.status === "on_scene") r.eta = null;
57
+ }
58
+ }
59
+
60
+ return {
61
+ incidentId: args.incidentId,
62
+ newStatus: args.status,
63
+ timeline: inc.timeline.slice(-5).map((t) => t.event),
64
+ casualties: inc.casualties,
65
+ };
66
+ });
67
+ },
68
+ });
@@ -0,0 +1,52 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { getState, incidentAgeMinutes, resourceBrief, resourceUtilization } from "../shared.ts";
3
+
4
+ export const opsDashboard = tool({
5
+ description:
6
+ "Get the full operational dashboard: alert level, resource utilization, active incidents, and available resources.",
7
+ async execute(_args, ctx) {
8
+ const state = getState(ctx);
9
+
10
+ const activeIncidents = Object.values(state.incidents)
11
+ .filter((i) => i.status !== "resolved")
12
+ .sort((a, b) => b.triageScore - a.triageScore);
13
+
14
+ const resolvedCount = Object.values(state.incidents).filter(
15
+ (i) => i.status === "resolved",
16
+ ).length;
17
+
18
+ const resourceSummary = {
19
+ total: state.resources.length,
20
+ available: state.resources.filter((r) => r.status === "available").length,
21
+ dispatched: state.resources.filter((r) => r.status === "dispatched").length,
22
+ enRoute: state.resources.filter((r) => r.status === "en_route").length,
23
+ onScene: state.resources.filter((r) => r.status === "on_scene").length,
24
+ returning: state.resources.filter((r) => r.status === "returning").length,
25
+ };
26
+
27
+ const utilization = Math.round(resourceUtilization(state) * 100);
28
+
29
+ return {
30
+ systemAlertLevel: state.alertLevel,
31
+ mutualAidActive: state.mutualAidRequested,
32
+ resourceUtilization: `${utilization}%`,
33
+ resourceSummary,
34
+ activeIncidentCount: activeIncidents.length,
35
+ resolvedIncidentCount: resolvedCount,
36
+ activeIncidents: activeIncidents.map((i) => ({
37
+ id: i.id,
38
+ type: i.type,
39
+ severity: i.severity,
40
+ status: i.status,
41
+ location: i.location,
42
+ triageScore: i.triageScore,
43
+ assignedResourceCount: i.assignedResources.length,
44
+ ageMinutes: incidentAgeMinutes(i),
45
+ casualties: i.casualties,
46
+ })),
47
+ availableResources: state.resources
48
+ .filter((r) => r.status === "available")
49
+ .map(resourceBrief),
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,27 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { getApplicableProtocols, INCIDENT_TYPES, SEVERITIES } from "../shared.ts";
4
+
5
+ export const opsProtocols = tool({
6
+ description: "Look up step-by-step response protocols for a given incident type and severity.",
7
+ parameters: z.object({
8
+ incidentType: z.enum(INCIDENT_TYPES).describe("Type of incident"),
9
+ severity: z.enum(SEVERITIES).describe("Severity level"),
10
+ }),
11
+ async execute(args) {
12
+ const protocols = getApplicableProtocols(args.incidentType, args.severity);
13
+ if (protocols.length === 0) {
14
+ return {
15
+ message: "No specific protocols for this combination. Use standard operating procedures.",
16
+ protocols: [],
17
+ };
18
+ }
19
+ return {
20
+ protocols: protocols.map((p) => ({
21
+ name: p.name,
22
+ steps: p.steps,
23
+ requiredResources: p.requiredResources,
24
+ })),
25
+ };
26
+ },
27
+ });
@@ -0,0 +1,149 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import type { Incident, IncidentType, Severity } from "../shared.ts";
4
+ import { calculateTriageScore, createIncident, updateState } from "../shared.ts";
5
+
6
+ type ScenarioIncident = Pick<Incident, "location" | "description" | "type" | "severity">;
7
+ type ScenarioDef = { narrative: string; incidents: ScenarioIncident[] };
8
+
9
+ const inc = (
10
+ location: string,
11
+ description: string,
12
+ type: IncidentType,
13
+ severity: Severity,
14
+ ): ScenarioIncident => ({ location, description, type, severity });
15
+
16
+ // `satisfies` keeps the keys literal, so the tool's enum below is derived —
17
+ // adding a scenario here is the only edit needed.
18
+ const scenarios = {
19
+ mass_casualty: {
20
+ narrative:
21
+ "Bus crash at Main and 5th. School bus vs delivery truck. Multiple pediatric patients. Fuel spill.",
22
+ incidents: [
23
+ inc(
24
+ "Main St and 5th Ave intersection",
25
+ "School bus collision with delivery truck, multiple children injured, bus on its side, fuel leaking",
26
+ "traffic",
27
+ "critical",
28
+ ),
29
+ inc(
30
+ "Main St and 5th Ave — fuel spill",
31
+ "Diesel fuel spill from delivery truck spreading toward storm drain, ~50 gallons",
32
+ "hazmat",
33
+ "urgent",
34
+ ),
35
+ ],
36
+ },
37
+ multi_alarm_fire: {
38
+ narrative:
39
+ "Working structure fire at 200 Industrial Parkway. 3-story warehouse, heavy smoke. Workers possibly trapped.",
40
+ incidents: [
41
+ inc(
42
+ "200 Industrial Parkway",
43
+ "3-story warehouse fully involved, possible trapped occupants on 2nd/3rd floor",
44
+ "fire",
45
+ "critical",
46
+ ),
47
+ inc(
48
+ "200 Industrial Parkway — medical",
49
+ "2 workers with smoke inhalation, one with burns",
50
+ "medical",
51
+ "urgent",
52
+ ),
53
+ ],
54
+ },
55
+ active_shooter: {
56
+ narrative:
57
+ "Active shooter at Riverside Mall. Multiple shots fired, crowds fleeing. At least 3 victims down in food court.",
58
+ incidents: [
59
+ inc(
60
+ "Riverside Mall, 1500 River Road — food court",
61
+ "Active shooter, multiple shots, at least 3 victims down, shooter moving toward west entrance",
62
+ "crime",
63
+ "critical",
64
+ ),
65
+ inc(
66
+ "Riverside Mall parking lot",
67
+ "Crowd crush injuries, several trampled near east exit",
68
+ "medical",
69
+ "urgent",
70
+ ),
71
+ ],
72
+ },
73
+ natural_disaster: {
74
+ narrative:
75
+ "EF-3 tornado in residential area. Oak Street corridor. Multiple structures collapsed. Power lines down.",
76
+ incidents: [
77
+ inc(
78
+ "Oak Street between 10th and 15th",
79
+ "Tornado damage, homes collapsed, people trapped, gas lines ruptured",
80
+ "natural_disaster",
81
+ "critical",
82
+ ),
83
+ inc(
84
+ "Oak Street Elementary School",
85
+ "School roof partially collapsed, staff sheltering students",
86
+ "natural_disaster",
87
+ "critical",
88
+ ),
89
+ inc(
90
+ "Oak Street and 12th — utility",
91
+ "Downed power lines sparking, gas main rupture, area needs isolation",
92
+ "utility",
93
+ "urgent",
94
+ ),
95
+ ],
96
+ },
97
+ highway_pileup: {
98
+ narrative:
99
+ "20+ vehicle pileup on I-95 southbound mile marker 42. Fog. Multiple entrapments. Tanker truck involved.",
100
+ incidents: [
101
+ inc(
102
+ "I-95 southbound mile marker 42",
103
+ "Multi-vehicle pileup, 20+ vehicles, multiple entrapments, tanker with unknown cargo, heavy fog",
104
+ "traffic",
105
+ "critical",
106
+ ),
107
+ inc(
108
+ "I-95 southbound — hazmat",
109
+ "Tanker leaking unknown liquid, placards not visible, exclusion zone being set up",
110
+ "hazmat",
111
+ "critical",
112
+ ),
113
+ ],
114
+ },
115
+ } satisfies Record<string, ScenarioDef>;
116
+
117
+ type ScenarioName = keyof typeof scenarios;
118
+ const SCENARIO_NAMES = Object.keys(scenarios) as [ScenarioName, ...ScenarioName[]];
119
+
120
+ export const opsRunScenario = tool({
121
+ description: "Run a training scenario that creates simulated incidents for dispatch practice.",
122
+ parameters: z.object({
123
+ scenario: z.enum(SCENARIO_NAMES).describe("Scenario type to simulate"),
124
+ }),
125
+ async execute(args, ctx) {
126
+ return updateState(ctx, (state) => {
127
+ const s = scenarios[args.scenario];
128
+
129
+ const created: string[] = [];
130
+ for (const scenarioInc of s.incidents) {
131
+ const fullInc = createIncident(state, {
132
+ ...scenarioInc,
133
+ callerName: "Scenario",
134
+ callerPhone: "N/A",
135
+ triageScore: calculateTriageScore(scenarioInc.severity, scenarioInc.type, 0, 0),
136
+ timeline: [{ time: Date.now(), event: `SCENARIO: ${scenarioInc.description}` }],
137
+ });
138
+ created.push(fullInc.id);
139
+ }
140
+
141
+ return {
142
+ scenario: args.scenario,
143
+ narrative: s.narrative,
144
+ incidentsCreated: created,
145
+ message: `SCENARIO ACTIVE: ${s.narrative}. ${created.length} incidents created. Awaiting dispatch orders.`,
146
+ };
147
+ });
148
+ },
149
+ });
@@ -0,0 +1,97 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import type { Resource } from "../shared.ts";
4
+ import {
5
+ assertNotResolved,
6
+ findIncident,
7
+ logEvent,
8
+ recommendResources,
9
+ updateState,
10
+ } from "../shared.ts";
11
+
12
+ export const resourcesDispatch = tool({
13
+ description:
14
+ "Dispatch units to an incident. Can auto-dispatch recommended resources or manually specify callsigns.",
15
+ parameters: z.object({
16
+ incidentId: z.string().max(20).describe("The incident ID"),
17
+ callsigns: z
18
+ .array(z.string().max(50))
19
+ .max(20)
20
+ .describe(
21
+ "Resource callsigns to dispatch, or the single entry 'auto' for system-recommended resources.",
22
+ )
23
+ .optional(),
24
+ autoDispatch: z
25
+ .boolean()
26
+ .describe("If true, automatically dispatch recommended resources")
27
+ .optional(),
28
+ priority: z
29
+ .enum(["routine", "priority", "emergency"])
30
+ .describe("Dispatch priority — affects simulated ETA")
31
+ .optional(),
32
+ }),
33
+ async execute(args, ctx) {
34
+ return updateState(ctx, (state) => {
35
+ const inc = findIncident(state, args.incidentId);
36
+ if ("error" in inc) return inc;
37
+ const blocked = assertNotResolved(inc, "dispatch resources");
38
+ if (blocked) return blocked;
39
+
40
+ const dispatched: { callsign: string; type: string; eta: number }[] = [];
41
+ const failed: { callsign: string; reason: string }[] = [];
42
+
43
+ // The literal "auto" callsign means the same thing as autoDispatch.
44
+ const wantsAuto =
45
+ args.autoDispatch || args.callsigns?.some((cs) => cs.toLowerCase() === "auto");
46
+
47
+ let resourcesToDispatch: Resource[] = [];
48
+
49
+ if (wantsAuto) {
50
+ resourcesToDispatch = recommendResources(inc.type, inc.severity, state);
51
+ } else if (args.callsigns) {
52
+ for (const cs of args.callsigns) {
53
+ const r = state.resources.find((r) => r.callsign.toLowerCase() === cs.toLowerCase());
54
+ if (!r) {
55
+ failed.push({ callsign: cs, reason: "Not found" });
56
+ continue;
57
+ }
58
+ if (r.status !== "available") {
59
+ failed.push({ callsign: cs, reason: `Currently ${r.status}` });
60
+ continue;
61
+ }
62
+ resourcesToDispatch.push(r);
63
+ }
64
+ }
65
+
66
+ const etaBase = args.priority === "emergency" ? 3 : args.priority === "priority" ? 6 : 10;
67
+
68
+ for (const r of resourcesToDispatch) {
69
+ const eta = etaBase + Math.floor(Math.random() * 5);
70
+ r.status = "dispatched";
71
+ r.assignedIncident = args.incidentId;
72
+ r.eta = eta;
73
+ inc.assignedResources.push(r.id);
74
+ dispatched.push({ callsign: r.callsign, type: r.type, eta });
75
+ logEvent(inc, `Dispatched ${r.callsign} — ETA ${eta} min`);
76
+ }
77
+
78
+ if (dispatched.length > 0) {
79
+ inc.status = "dispatched";
80
+ }
81
+
82
+ const availableCount = state.resources.filter((r) => r.status === "available").length;
83
+
84
+ return {
85
+ incidentId: args.incidentId,
86
+ dispatched,
87
+ failed: failed.length > 0 ? failed : undefined,
88
+ totalAssignedToIncident: inc.assignedResources.length,
89
+ remainingAvailableResources: availableCount,
90
+ capacityWarning:
91
+ availableCount <= 3
92
+ ? "WARNING: Resource capacity critically low. Consider mutual aid."
93
+ : undefined,
94
+ };
95
+ });
96
+ },
97
+ });