@alexkroman1/aai-cli 5.13.2 → 5.14.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.
- package/dist/scaffold/package.json +3 -3
- package/dist/scaffold/pnpm-workspace.yaml +24 -0
- package/dist/templates/dispatch-center/agent.test.ts +10 -17
- package/dist/templates/dispatch-center/agent.ts +2 -2
- package/dist/templates/dispatch-center/client.tsx +9 -13
- package/dist/templates/dispatch-center/shared.ts +36 -63
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_create.ts +2 -2
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_get.ts +9 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +4 -4
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -2
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +2 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +4 -4
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -2
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +2 -2
- package/dist/templates/infocom-adventure/agent.ts +10 -10
- package/dist/templates/infocom-adventure/client.tsx +5 -17
- package/dist/templates/infocom-adventure/shared.ts +17 -14
- package/dist/templates/pizza-ordering/agent.test.ts +27 -31
- package/dist/templates/pizza-ordering/agent.ts +8 -8
- package/dist/templates/pizza-ordering/client.tsx +4 -4
- package/dist/templates/pizza-ordering/shared.ts +14 -20
- package/dist/templates/retail/agent.test.ts +137 -138
- package/dist/templates/retail/agent.ts +7 -10
- package/dist/templates/retail/authenticate.ts +2 -2
- package/dist/templates/retail/client.tsx +13 -14
- package/dist/templates/retail/registry.test.ts +9 -9
- package/dist/templates/retail/resolve.test.ts +17 -16
- package/dist/templates/retail/resolve.ts +5 -4
- package/dist/templates/retail/shared.test.ts +17 -16
- package/dist/templates/retail/shared.ts +39 -18
- package/dist/templates/retail/store.test.ts +40 -47
- package/dist/templates/retail/store.ts +39 -65
- package/dist/templates/retail/swap.test.ts +20 -17
- package/dist/templates/retail/swap.ts +7 -14
- package/dist/templates/retail/tools/cancel_pending_order.ts +5 -4
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +6 -5
- package/dist/templates/retail/tools/find_user_id_by_email.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +2 -2
- package/dist/templates/retail/tools/get_item_details.ts +4 -3
- package/dist/templates/retail/tools/get_order_details.ts +4 -3
- package/dist/templates/retail/tools/get_product_details.ts +4 -3
- package/dist/templates/retail/tools/get_user_details.ts +4 -3
- package/dist/templates/retail/tools/list_all_product_types.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -3
- package/dist/templates/retail/tools/modify_pending_order_items.ts +6 -6
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +6 -6
- package/dist/templates/retail/tools/modify_user_address.ts +4 -3
- package/dist/templates/retail/tools/return_delivered_order_items.ts +6 -6
- package/dist/templates/solo-rpg/agent.test.ts +26 -34
- package/dist/templates/solo-rpg/agent.ts +2 -2
- package/dist/templates/solo-rpg/client.tsx +1 -1
- package/dist/templates/solo-rpg/shared.ts +6 -14
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -2
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -2
- package/dist/templates/solo-rpg/tools/check_state.ts +2 -2
- package/dist/templates/solo-rpg/tools/load_game.ts +2 -2
- package/dist/templates/solo-rpg/tools/oracle.ts +3 -3
- package/dist/templates/solo-rpg/tools/save_game.ts +2 -2
- package/dist/templates/solo-rpg/tools/setup_character.ts +2 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +12 -11
- package/package.json +3 -3
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
"publish:agent": "aai publish"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@alexkroman1/aai": "^5.
|
|
16
|
-
"@alexkroman1/aai-ui": "^5.
|
|
15
|
+
"@alexkroman1/aai": "^5.14.0",
|
|
16
|
+
"@alexkroman1/aai-ui": "^5.14.0",
|
|
17
17
|
"react": "^19.2.8",
|
|
18
18
|
"react-dom": "^19.2.8",
|
|
19
19
|
"tailwindcss": "^4.0.0",
|
|
20
20
|
"zod": "^4.4.3"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@alexkroman1/aai-cli": "^5.
|
|
23
|
+
"@alexkroman1/aai-cli": "^5.14.0",
|
|
24
24
|
"@tailwindcss/vite": "^4.3.3",
|
|
25
25
|
"@types/node": "^26.1.1",
|
|
26
26
|
"@types/react": "^19.2.17",
|
|
@@ -11,3 +11,27 @@ onlyBuiltDependencies:
|
|
|
11
11
|
- esbuild
|
|
12
12
|
allowBuilds:
|
|
13
13
|
esbuild: true
|
|
14
|
+
|
|
15
|
+
# Exempt our own packages from pnpm's release-age quarantine, and ONLY ours.
|
|
16
|
+
# `minimumReleaseAge` holds a version back until it has been on the registry
|
|
17
|
+
# for N minutes (pnpm 11 turns it on by default; an org .npmrc or a corporate
|
|
18
|
+
# config can set it far higher). The scaffold pins `^<newest>` — see
|
|
19
|
+
# scripts/sync-scaffold-versions.mjs, which resyncs it on every `changeset
|
|
20
|
+
# version` — and this repo publishes several times a day, so EVERY version
|
|
21
|
+
# satisfying that range is younger than the window and resolution fails
|
|
22
|
+
# outright:
|
|
23
|
+
#
|
|
24
|
+
# ERR_PNPM_NO_MATURE_MATCHING_VERSION Version 5.13.2 (released 2 hours ago)
|
|
25
|
+
# of @alexkroman1/aai-cli does not meet the minimumReleaseAge constraint
|
|
26
|
+
#
|
|
27
|
+
# There is no version to fall back to, so a freshly scaffolded project cannot
|
|
28
|
+
# install at all — `aai init` fails at its own install step. Lowering the pin
|
|
29
|
+
# would not help: the floor has to admit the SDK the templates are written
|
|
30
|
+
# against, and that build is by definition new.
|
|
31
|
+
#
|
|
32
|
+
# Quarantine exists to blunt a compromised-release window, so this stays a
|
|
33
|
+
# per-package exemption rather than `minimumReleaseAge: 0` — every third-party
|
|
34
|
+
# dependency keeps whatever window the user has configured. We are the one
|
|
35
|
+
# publisher whose freshness the user is opting into by running `aai init`.
|
|
36
|
+
minimumReleaseAgeExclude:
|
|
37
|
+
- "@alexkroman1/*"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
2
|
+
import { createToolContext } from "@alexkroman1/aai/testing";
|
|
2
3
|
import { describe, expect, test } from "vitest";
|
|
3
|
-
import {
|
|
4
|
+
import { dispatchSlot, type StateSlot } from "./shared.ts";
|
|
4
5
|
import { incidentCreate } from "./tools/incident_create.ts";
|
|
5
6
|
import { incidentEscalate } from "./tools/incident_escalate.ts";
|
|
6
7
|
import { incidentTriage } from "./tools/incident_triage.ts";
|
|
@@ -8,19 +9,11 @@ import { incidentUpdateStatus } from "./tools/incident_update_status.ts";
|
|
|
8
9
|
import { resourcesDispatch } from "./tools/resources_dispatch.ts";
|
|
9
10
|
import { resourcesUpdateStatus } from "./tools/resources_update_status.ts";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function makeCtx(): { ctx: ToolContext } {
|
|
15
|
-
|
|
16
|
-
const ctx = {
|
|
17
|
-
sessionId,
|
|
18
|
-
send: () => {},
|
|
19
|
-
env: {},
|
|
20
|
-
state: {},
|
|
21
|
-
messages: [],
|
|
22
|
-
} as unknown as ToolContext;
|
|
23
|
-
return { ctx };
|
|
12
|
+
/** The dispatch board lives in ctx.state, one per session — and
|
|
13
|
+
* `createToolContext` gives each call its own session id, so two contexts are
|
|
14
|
+
* two boards by construction. */
|
|
15
|
+
function makeCtx(): { ctx: ToolContext<StateSlot> } {
|
|
16
|
+
return { ctx: createToolContext<StateSlot>() };
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
async function createIncidentFor(
|
|
@@ -49,7 +42,7 @@ describe("dispatch-center template", () => {
|
|
|
49
42
|
// Resolving the first incident must not touch Medic-1 anymore.
|
|
50
43
|
await incidentUpdateStatus.execute({ incidentId: inc1, status: "resolved" }, ctx);
|
|
51
44
|
|
|
52
|
-
const state =
|
|
45
|
+
const state = dispatchSlot.get(ctx);
|
|
53
46
|
const medic1 = state.resources.find((r) => r.callsign === "Medic-1");
|
|
54
47
|
expect(medic1?.status).toBe("dispatched");
|
|
55
48
|
expect(medic1?.assignedIncident).toBe(inc2);
|
|
@@ -82,7 +75,7 @@ describe("dispatch-center template", () => {
|
|
|
82
75
|
incidentCreate.execute({ location: "2 Second St", description: "vehicle crash" }, ctx),
|
|
83
76
|
])) as { incidentId: string }[];
|
|
84
77
|
|
|
85
|
-
const state =
|
|
78
|
+
const state = dispatchSlot.get(ctx);
|
|
86
79
|
expect(a?.incidentId).not.toBe(b?.incidentId);
|
|
87
80
|
expect(state.incidentCounter).toBe(2);
|
|
88
81
|
expect(Object.keys(state.incidents)).toHaveLength(2);
|
|
@@ -121,7 +114,7 @@ describe("dispatch-center template", () => {
|
|
|
121
114
|
ctx,
|
|
122
115
|
);
|
|
123
116
|
|
|
124
|
-
const state =
|
|
117
|
+
const state = dispatchSlot.get(ctx);
|
|
125
118
|
const mutualAid = state.resources.filter((r) => r.id.startsWith("MA-"));
|
|
126
119
|
expect(mutualAid).toHaveLength(4);
|
|
127
120
|
expect(new Set(mutualAid.map((r) => r.id)).size).toBe(4);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import { dashboardView } from "./shared.ts";
|
|
2
|
+
import { dashboardView, dispatchSlot } from "./shared.ts";
|
|
3
3
|
import systemPrompt from "./system-prompt.md?raw";
|
|
4
4
|
import { incidentAddNote } from "./tools/incident_add_note.ts";
|
|
5
5
|
import { incidentCreate } from "./tools/incident_create.ts";
|
|
@@ -18,7 +18,7 @@ export default agent({
|
|
|
18
18
|
name: "Dispatch Command Center",
|
|
19
19
|
// One projection replaces eleven `ctx.send("incidents", ...)` calls, and
|
|
20
20
|
// is the single place that decides caller PII stays server-side.
|
|
21
|
-
syncState: dashboardView,
|
|
21
|
+
syncState: dispatchSlot.projection(dashboardView),
|
|
22
22
|
// A dispatcher reads addresses and unit numbers in bursts with pauses inside
|
|
23
23
|
// one message ("unit twelve … respond to"). The default pipeline's
|
|
24
24
|
// `max_turn_silence` already tolerates that; reach for
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
2
|
import type { ChatMessage } from "@alexkroman1/aai-ui";
|
|
3
|
-
import { client, useAgentState, useSession } from "@alexkroman1/aai-ui";
|
|
4
|
-
import { useEffect, useRef } from "react";
|
|
3
|
+
import { AutoScroll, client, useAgentState, useSession } from "@alexkroman1/aai-ui";
|
|
5
4
|
import type { DashboardView, DispatchState, IncidentSummary, Severity, Status } from "./shared.ts";
|
|
6
|
-
import { dashboardView } from "./shared.ts";
|
|
5
|
+
import { dashboardView, dispatchSlot } from "./shared.ts";
|
|
7
6
|
|
|
8
7
|
const CSS = `
|
|
9
8
|
@keyframes dc-pulse {
|
|
@@ -54,7 +53,7 @@ const statusColors: Record<string, string> = {
|
|
|
54
53
|
|
|
55
54
|
// The board before the first tool call — derived from the projection so a
|
|
56
55
|
// new DashboardView field can't silently miss the pre-first-tool-call render.
|
|
57
|
-
const EMPTY_DASH = dashboardView(
|
|
56
|
+
const EMPTY_DASH: DashboardView = dispatchSlot.projection(dashboardView)(undefined);
|
|
58
57
|
|
|
59
58
|
function stateColor(state: string): string {
|
|
60
59
|
return state === "listening"
|
|
@@ -133,15 +132,10 @@ function IncidentCard({ inc }: { inc: IncidentSummary }) {
|
|
|
133
132
|
|
|
134
133
|
function App() {
|
|
135
134
|
const session = useSession();
|
|
136
|
-
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
137
135
|
// The agent's own board, projected by `syncState`. This replaced a
|
|
138
136
|
// useState mirror that merged incident deltas out of tool events — the
|
|
139
137
|
// projection is already the complete list, so there is nothing to merge.
|
|
140
|
-
const dash = useAgentState<DashboardView>()
|
|
141
|
-
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
144
|
-
}, [session.messages]);
|
|
138
|
+
const dash = useAgentState<DashboardView>(EMPTY_DASH);
|
|
145
139
|
|
|
146
140
|
const incidentList = [...dash.incidents].reverse();
|
|
147
141
|
const activeIncidents = incidentList.filter((i) => i.status !== "resolved");
|
|
@@ -222,7 +216,10 @@ function App() {
|
|
|
222
216
|
className="flex flex-col overflow-hidden"
|
|
223
217
|
style={{ borderRight: "1px solid #1e293b" }}
|
|
224
218
|
>
|
|
225
|
-
<
|
|
219
|
+
<AutoScroll
|
|
220
|
+
scrollClassName="dc-messages overflow-y-auto"
|
|
221
|
+
contentClassName="p-4 flex flex-col gap-2"
|
|
222
|
+
>
|
|
226
223
|
{session.messages.length === 0 && (
|
|
227
224
|
<div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
|
|
228
225
|
Dispatch Command Center standing by. Click START to begin operations.
|
|
@@ -250,8 +247,7 @@ function App() {
|
|
|
250
247
|
{m.content}
|
|
251
248
|
</div>
|
|
252
249
|
))}
|
|
253
|
-
|
|
254
|
-
</div>
|
|
250
|
+
</AutoScroll>
|
|
255
251
|
|
|
256
252
|
{session.userTranscript !== null && (
|
|
257
253
|
<div
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
3
|
+
import type { ToolFailure } from "@alexkroman1/aai";
|
|
4
|
+
import { pushCapped, type SlotStateOf, sessionSlot } from "@alexkroman1/aai";
|
|
5
5
|
|
|
6
6
|
export const SEVERITIES = ["critical", "urgent", "moderate", "minor"] as const;
|
|
7
7
|
export type Severity = (typeof SEVERITIES)[number];
|
|
@@ -111,22 +111,16 @@ export interface DashboardView {
|
|
|
111
111
|
incidents: IncidentSummary[];
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/** The `syncState` projection — the whole contract with client.tsx.
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
/** The `syncState` projection — the whole contract with client.tsx. Takes the
|
|
115
|
+
* board itself, not the slot: `dispatchSlot.projection` supplies a real one
|
|
116
|
+
* even before the first tool call. */
|
|
117
|
+
export function dashboardView(state: DispatchState): DashboardView {
|
|
117
118
|
return {
|
|
118
|
-
systemAlertLevel:
|
|
119
|
-
incidents: Object.values(
|
|
119
|
+
systemAlertLevel: state.alertLevel,
|
|
120
|
+
incidents: Object.values(state.incidents).map(incidentSummary),
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
// ─── State helpers ───────────────────────────────────────────────────────────
|
|
124
|
-
// The dispatch board lives in `ctx.state`, the agent's per-session mutable
|
|
125
|
-
// state — sessions must not see each other's incidents, and ctx.state gives
|
|
126
|
-
// that isolation by construction. Nothing here needs to outlive the session.
|
|
127
|
-
|
|
128
|
-
export type StateSlot = { dispatch?: DispatchState };
|
|
129
|
-
|
|
130
124
|
// ─── Resource generation ────────────────────────────────────────────────────
|
|
131
125
|
|
|
132
126
|
const RESOURCE_DEFS: [string, Resource["type"], string, string[]][] = [
|
|
@@ -167,15 +161,10 @@ export function createDefaultState(): DispatchState {
|
|
|
167
161
|
};
|
|
168
162
|
}
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
slot.dispatch ??= createDefaultState();
|
|
175
|
-
return slot.dispatch;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ─── Serialized state updates ────────────────────────────────────────────────
|
|
164
|
+
// ─── State helpers ───────────────────────────────────────────────────────────
|
|
165
|
+
// The dispatch board lives in `ctx.state`, the agent's per-session mutable
|
|
166
|
+
// state — sessions must not see each other's incidents, and ctx.state gives
|
|
167
|
+
// that isolation by construction. Nothing here needs to outlive the session.
|
|
179
168
|
|
|
180
169
|
/**
|
|
181
170
|
* Growth caps. The whole dispatch state is one object whose summaries feed
|
|
@@ -183,8 +172,10 @@ export function getState(ctx: ToolContext): DispatchState {
|
|
|
183
172
|
* timelines must be pruned or a long session's payloads grow without bound.
|
|
184
173
|
*/
|
|
185
174
|
const MAX_RESOLVED_KEPT = 10;
|
|
186
|
-
const MAX_TIMELINE_ENTRIES = 50;
|
|
175
|
+
export const MAX_TIMELINE_ENTRIES = 50;
|
|
187
176
|
|
|
177
|
+
/** Resolved incidents only — the timeline cap is held on append by
|
|
178
|
+
* `logEvent`, so nothing here has to re-trim it. */
|
|
188
179
|
function pruneState(state: DispatchState): void {
|
|
189
180
|
const resolved = Object.values(state.incidents)
|
|
190
181
|
.filter((i) => i.status === "resolved")
|
|
@@ -192,46 +183,29 @@ function pruneState(state: DispatchState): void {
|
|
|
192
183
|
for (const inc of resolved.slice(MAX_RESOLVED_KEPT)) {
|
|
193
184
|
delete state.incidents[inc.id];
|
|
194
185
|
}
|
|
195
|
-
for (const inc of Object.values(state.incidents)) {
|
|
196
|
-
if (inc.timeline.length > MAX_TIMELINE_ENTRIES) {
|
|
197
|
-
inc.timeline = inc.timeline.slice(-MAX_TIMELINE_ENTRIES);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
186
|
}
|
|
201
187
|
|
|
202
|
-
const sessionLock = createKeyedLock();
|
|
203
|
-
|
|
204
188
|
/**
|
|
205
|
-
*
|
|
189
|
+
* The session's dispatch board, as one typed slot inside `ctx.state`.
|
|
206
190
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* update run against the previous one's finished result. It also centralizes
|
|
212
|
-
* the shared bookkeeping every mutating tool needs: pruning and alert-level
|
|
213
|
-
* recalculation.
|
|
191
|
+
* Mutating tools go through `dispatchSlot.update`, which serializes them per
|
|
192
|
+
* session: the LLM loop runs a step's tool calls concurrently, and two
|
|
193
|
+
* interleaving async mutators would each observe the other's half-applied
|
|
194
|
+
* changes.
|
|
214
195
|
*
|
|
215
|
-
* `
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* the
|
|
219
|
-
*
|
|
220
|
-
* There is no post-save callback: pushing the board to the client used to
|
|
221
|
-
* need one, and `syncState` now does it after every tool call.
|
|
196
|
+
* `after` is the bookkeeping every mutating tool needs and none of them should
|
|
197
|
+
* have to remember — pruning resolved incidents, and recalculating the alert
|
|
198
|
+
* level the dashboard reads. Declaring it here rather than at each call site is
|
|
199
|
+
* the point: a new mutating tool gets both for free.
|
|
222
200
|
*/
|
|
223
|
-
export
|
|
224
|
-
|
|
225
|
-
mutator: (state: DispatchState) => R | Promise<R>,
|
|
226
|
-
): Promise<R> {
|
|
227
|
-
return withLock(sessionLock, ctx.sessionId, async () => {
|
|
228
|
-
const state = getState(ctx);
|
|
229
|
-
const result = await mutator(state);
|
|
201
|
+
export const dispatchSlot = sessionSlot("dispatch", createDefaultState, {
|
|
202
|
+
after: (state) => {
|
|
230
203
|
pruneState(state);
|
|
231
204
|
recalculateAlertLevel(state);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export type StateSlot = SlotStateOf<typeof dispatchSlot>;
|
|
235
209
|
|
|
236
210
|
// ─── Incident helpers ────────────────────────────────────────────────────────
|
|
237
211
|
|
|
@@ -263,17 +237,16 @@ export function createIncident(state: DispatchState, overrides: Partial<Incident
|
|
|
263
237
|
return incident;
|
|
264
238
|
}
|
|
265
239
|
|
|
266
|
-
export function findIncident(
|
|
267
|
-
state: DispatchState,
|
|
268
|
-
incidentId: string,
|
|
269
|
-
): Incident | { error: string } {
|
|
240
|
+
export function findIncident(state: DispatchState, incidentId: string): Incident | ToolFailure {
|
|
270
241
|
return state.incidents[incidentId] ?? { error: `Incident ${incidentId} not found` };
|
|
271
242
|
}
|
|
272
243
|
|
|
273
|
-
/** Append a timeline entry and touch `updatedAt
|
|
244
|
+
/** Append a timeline entry and touch `updatedAt`, holding
|
|
245
|
+
* {@link MAX_TIMELINE_ENTRIES}. Capped on APPEND rather than swept later, so
|
|
246
|
+
* a single long-running incident cannot outgrow the payload between sweeps. */
|
|
274
247
|
export function logEvent(inc: Incident, event: string): void {
|
|
275
248
|
const time = Date.now();
|
|
276
|
-
inc.timeline
|
|
249
|
+
pushCapped(inc.timeline, { time, event }, MAX_TIMELINE_ENTRIES);
|
|
277
250
|
inc.updatedAt = time;
|
|
278
251
|
}
|
|
279
252
|
|
|
@@ -296,7 +269,7 @@ export function resourceBrief(r: Resource): {
|
|
|
296
269
|
* resources have been released (and possibly reassigned), so escalating,
|
|
297
270
|
* re-resolving, or dispatching to it would corrupt resource assignments.
|
|
298
271
|
*/
|
|
299
|
-
export function assertNotResolved(inc: Incident, action: string):
|
|
272
|
+
export function assertNotResolved(inc: Incident, action: string): ToolFailure | null {
|
|
300
273
|
if (inc.status === "resolved") {
|
|
301
274
|
return {
|
|
302
275
|
error: `Incident ${inc.id} is resolved — cannot ${action}. Create a new incident if the situation has reopened.`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
1
|
+
import { isToolFailure, tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { findIncident, logEvent
|
|
3
|
+
import { dispatchSlot, findIncident, logEvent } from "../shared.ts";
|
|
4
4
|
|
|
5
5
|
export const incidentAddNote = tool({
|
|
6
6
|
description: "Add a situational update note to an incident's timeline.",
|
|
@@ -10,9 +10,9 @@ export const incidentAddNote = tool({
|
|
|
10
10
|
source: z.string().max(100).describe("Who reported this — unit callsign or caller").optional(),
|
|
11
11
|
}),
|
|
12
12
|
async execute(args, ctx) {
|
|
13
|
-
return
|
|
13
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
14
14
|
const inc = findIncident(state, args.incidentId);
|
|
15
|
-
if (
|
|
15
|
+
if (isToolFailure(inc)) return inc;
|
|
16
16
|
|
|
17
17
|
const entry = args.source ? `[${args.source}] ${args.note}` : args.note;
|
|
18
18
|
logEvent(inc, entry);
|
|
@@ -3,12 +3,12 @@ import { z } from "zod";
|
|
|
3
3
|
import {
|
|
4
4
|
calculateTriageScore,
|
|
5
5
|
createIncident,
|
|
6
|
+
dispatchSlot,
|
|
6
7
|
getApplicableProtocols,
|
|
7
8
|
recommendResources,
|
|
8
9
|
recommendSeverity,
|
|
9
10
|
recommendType,
|
|
10
11
|
resourceBrief,
|
|
11
|
-
updateState,
|
|
12
12
|
} from "../shared.ts";
|
|
13
13
|
|
|
14
14
|
export const incidentCreate = tool({
|
|
@@ -31,7 +31,7 @@ export const incidentCreate = tool({
|
|
|
31
31
|
.optional(),
|
|
32
32
|
}),
|
|
33
33
|
async execute(args, ctx) {
|
|
34
|
-
return
|
|
34
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
35
35
|
const recSeverity = recommendSeverity(args.description);
|
|
36
36
|
const recType = recommendType(args.description);
|
|
37
37
|
const triageScore = calculateTriageScore(
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
1
|
+
import { isToolFailure, tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
assertNotResolved,
|
|
5
5
|
calculateTriageScore,
|
|
6
|
+
dispatchSlot,
|
|
6
7
|
findIncident,
|
|
7
8
|
logEvent,
|
|
8
9
|
recommendResources,
|
|
9
|
-
updateState,
|
|
10
10
|
} from "../shared.ts";
|
|
11
11
|
|
|
12
12
|
export const incidentEscalate = tool({
|
|
@@ -21,9 +21,9 @@ export const incidentEscalate = tool({
|
|
|
21
21
|
newSeverity: z.enum(["critical", "urgent"]).describe("Escalated severity level").optional(),
|
|
22
22
|
}),
|
|
23
23
|
async execute(args, ctx) {
|
|
24
|
-
return
|
|
24
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
25
25
|
const inc = findIncident(state, args.incidentId);
|
|
26
|
-
if (
|
|
26
|
+
if (isToolFailure(inc)) return inc;
|
|
27
27
|
const blocked = assertNotResolved(inc, "escalate");
|
|
28
28
|
if (blocked) return blocked;
|
|
29
29
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
1
|
+
import { isToolFailure, tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
dispatchSlot,
|
|
5
|
+
findIncident,
|
|
6
|
+
getApplicableProtocols,
|
|
7
|
+
incidentAgeMinutes,
|
|
8
|
+
} from "../shared.ts";
|
|
4
9
|
|
|
5
10
|
export const incidentGet = tool({
|
|
6
11
|
description: "Get full details on a specific incident including timeline and assigned resources.",
|
|
@@ -8,9 +13,9 @@ export const incidentGet = tool({
|
|
|
8
13
|
incidentId: z.string().max(20).describe("The incident ID"),
|
|
9
14
|
}),
|
|
10
15
|
async execute(args, ctx) {
|
|
11
|
-
const state =
|
|
16
|
+
const state = dispatchSlot.get(ctx);
|
|
12
17
|
const inc = findIncident(state, args.incidentId);
|
|
13
|
-
if (
|
|
18
|
+
if (isToolFailure(inc)) return inc;
|
|
14
19
|
|
|
15
20
|
const assignedResourceDetails = inc.assignedResources
|
|
16
21
|
.map((rId) => {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
1
|
+
import { isToolFailure, tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
assertNotResolved,
|
|
5
5
|
calculateTriageScore,
|
|
6
|
+
dispatchSlot,
|
|
6
7
|
findIncident,
|
|
7
8
|
getApplicableProtocols,
|
|
8
9
|
INCIDENT_TYPES,
|
|
9
10
|
logEvent,
|
|
10
11
|
recommendResources,
|
|
11
12
|
SEVERITIES,
|
|
12
|
-
updateState,
|
|
13
13
|
} from "../shared.ts";
|
|
14
14
|
|
|
15
15
|
export const incidentTriage = tool({
|
|
@@ -28,9 +28,9 @@ export const incidentTriage = tool({
|
|
|
28
28
|
notes: z.string().max(1000).describe("Triage notes").optional(),
|
|
29
29
|
}),
|
|
30
30
|
async execute(args, ctx) {
|
|
31
|
-
return
|
|
31
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
32
32
|
const inc = findIncident(state, args.incidentId);
|
|
33
|
-
if (
|
|
33
|
+
if (isToolFailure(inc)) return inc;
|
|
34
34
|
const blocked = assertNotResolved(inc, "triage");
|
|
35
35
|
if (blocked) return blocked;
|
|
36
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
1
|
+
import { isToolFailure, tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { assertNotResolved, findIncident, logEvent
|
|
3
|
+
import { assertNotResolved, dispatchSlot, findIncident, logEvent } from "../shared.ts";
|
|
4
4
|
|
|
5
5
|
export const incidentUpdateStatus = tool({
|
|
6
6
|
description: "Update an incident's status (en_route, on_scene, resolved, escalated).",
|
|
@@ -17,9 +17,9 @@ export const incidentUpdateStatus = tool({
|
|
|
17
17
|
.optional(),
|
|
18
18
|
}),
|
|
19
19
|
async execute(args, ctx) {
|
|
20
|
-
return
|
|
20
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
21
21
|
const inc = findIncident(state, args.incidentId);
|
|
22
|
-
if (
|
|
22
|
+
if (isToolFailure(inc)) return inc;
|
|
23
23
|
const blocked = assertNotResolved(inc, `set status to ${args.status}`);
|
|
24
24
|
if (blocked) return blocked;
|
|
25
25
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { tool } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
2
|
+
import { dispatchSlot, incidentAgeMinutes, resourceBrief, resourceUtilization } from "../shared.ts";
|
|
3
3
|
|
|
4
4
|
export const opsDashboard = tool({
|
|
5
5
|
description:
|
|
6
6
|
"Get the full operational dashboard: alert level, resource utilization, active incidents, and available resources.",
|
|
7
7
|
async execute(_args, ctx) {
|
|
8
|
-
const state =
|
|
8
|
+
const state = dispatchSlot.get(ctx);
|
|
9
9
|
|
|
10
10
|
const activeIncidents = Object.values(state.incidents)
|
|
11
11
|
.filter((i) => i.status !== "resolved")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { Incident, IncidentType, Severity } from "../shared.ts";
|
|
4
|
-
import { calculateTriageScore, createIncident,
|
|
4
|
+
import { calculateTriageScore, createIncident, dispatchSlot } from "../shared.ts";
|
|
5
5
|
|
|
6
6
|
type ScenarioIncident = Pick<Incident, "location" | "description" | "type" | "severity">;
|
|
7
7
|
type ScenarioDef = { narrative: string; incidents: ScenarioIncident[] };
|
|
@@ -123,7 +123,7 @@ export const opsRunScenario = tool({
|
|
|
123
123
|
scenario: z.enum(SCENARIO_NAMES).describe("Scenario type to simulate"),
|
|
124
124
|
}),
|
|
125
125
|
async execute(args, ctx) {
|
|
126
|
-
return
|
|
126
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
127
127
|
const s = scenarios[args.scenario];
|
|
128
128
|
|
|
129
129
|
const created: string[] = [];
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
1
|
+
import { isToolFailure, tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { Resource } from "../shared.ts";
|
|
4
4
|
import {
|
|
5
5
|
assertNotResolved,
|
|
6
|
+
dispatchSlot,
|
|
6
7
|
findIncident,
|
|
7
8
|
logEvent,
|
|
8
9
|
recommendResources,
|
|
9
|
-
updateState,
|
|
10
10
|
} from "../shared.ts";
|
|
11
11
|
|
|
12
12
|
export const resourcesDispatch = tool({
|
|
@@ -31,9 +31,9 @@ export const resourcesDispatch = tool({
|
|
|
31
31
|
.optional(),
|
|
32
32
|
}),
|
|
33
33
|
async execute(args, ctx) {
|
|
34
|
-
return
|
|
34
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
35
35
|
const inc = findIncident(state, args.incidentId);
|
|
36
|
-
if (
|
|
36
|
+
if (isToolFailure(inc)) return inc;
|
|
37
37
|
const blocked = assertNotResolved(inc, "dispatch resources");
|
|
38
38
|
if (blocked) return blocked;
|
|
39
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { dispatchSlot, RESOURCE_TYPES } from "../shared.ts";
|
|
4
4
|
|
|
5
5
|
export const resourcesGetAvailable = tool({
|
|
6
6
|
description: "List available resources, optionally filtered by type.",
|
|
@@ -11,7 +11,7 @@ export const resourcesGetAvailable = tool({
|
|
|
11
11
|
.optional(),
|
|
12
12
|
}),
|
|
13
13
|
async execute(args, ctx) {
|
|
14
|
-
const state =
|
|
14
|
+
const state = dispatchSlot.get(ctx);
|
|
15
15
|
let resources = state.resources;
|
|
16
16
|
if (args.type && args.type !== "all") {
|
|
17
17
|
resources = resources.filter((r) => r.type === args.type);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { logEvent, RESOURCE_STATUSES
|
|
3
|
+
import { dispatchSlot, logEvent, RESOURCE_STATUSES } from "../shared.ts";
|
|
4
4
|
|
|
5
5
|
export const resourcesUpdateStatus = tool({
|
|
6
6
|
description: "Update a resource unit's status when it radios in.",
|
|
@@ -10,7 +10,7 @@ export const resourcesUpdateStatus = tool({
|
|
|
10
10
|
notes: z.string().max(1000).describe("Status notes").optional(),
|
|
11
11
|
}),
|
|
12
12
|
async execute(args, ctx) {
|
|
13
|
-
return
|
|
13
|
+
return dispatchSlot.update(ctx, (state) => {
|
|
14
14
|
const resource = state.resources.find(
|
|
15
15
|
(r) => r.callsign.toLowerCase() === args.callsign.toLowerCase(),
|
|
16
16
|
);
|