@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.
Files changed (64) hide show
  1. package/dist/scaffold/package.json +3 -3
  2. package/dist/scaffold/pnpm-workspace.yaml +24 -0
  3. package/dist/templates/dispatch-center/agent.test.ts +10 -17
  4. package/dist/templates/dispatch-center/agent.ts +2 -2
  5. package/dist/templates/dispatch-center/client.tsx +9 -13
  6. package/dist/templates/dispatch-center/shared.ts +36 -63
  7. package/dist/templates/dispatch-center/tools/incident_add_note.ts +4 -4
  8. package/dist/templates/dispatch-center/tools/incident_create.ts +2 -2
  9. package/dist/templates/dispatch-center/tools/incident_escalate.ts +4 -4
  10. package/dist/templates/dispatch-center/tools/incident_get.ts +9 -4
  11. package/dist/templates/dispatch-center/tools/incident_triage.ts +4 -4
  12. package/dist/templates/dispatch-center/tools/incident_update_status.ts +4 -4
  13. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -2
  14. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +2 -2
  15. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +4 -4
  16. package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -2
  17. package/dist/templates/dispatch-center/tools/resources_update_status.ts +2 -2
  18. package/dist/templates/infocom-adventure/agent.ts +10 -10
  19. package/dist/templates/infocom-adventure/client.tsx +5 -17
  20. package/dist/templates/infocom-adventure/shared.ts +17 -14
  21. package/dist/templates/pizza-ordering/agent.test.ts +27 -31
  22. package/dist/templates/pizza-ordering/agent.ts +8 -8
  23. package/dist/templates/pizza-ordering/client.tsx +4 -4
  24. package/dist/templates/pizza-ordering/shared.ts +14 -20
  25. package/dist/templates/retail/agent.test.ts +137 -138
  26. package/dist/templates/retail/agent.ts +7 -10
  27. package/dist/templates/retail/authenticate.ts +2 -2
  28. package/dist/templates/retail/client.tsx +13 -14
  29. package/dist/templates/retail/registry.test.ts +9 -9
  30. package/dist/templates/retail/resolve.test.ts +17 -16
  31. package/dist/templates/retail/resolve.ts +5 -4
  32. package/dist/templates/retail/shared.test.ts +17 -16
  33. package/dist/templates/retail/shared.ts +39 -18
  34. package/dist/templates/retail/store.test.ts +40 -47
  35. package/dist/templates/retail/store.ts +39 -65
  36. package/dist/templates/retail/swap.test.ts +20 -17
  37. package/dist/templates/retail/swap.ts +7 -14
  38. package/dist/templates/retail/tools/cancel_pending_order.ts +5 -4
  39. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +6 -5
  40. package/dist/templates/retail/tools/find_user_id_by_email.ts +2 -2
  41. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +2 -2
  42. package/dist/templates/retail/tools/get_item_details.ts +4 -3
  43. package/dist/templates/retail/tools/get_order_details.ts +4 -3
  44. package/dist/templates/retail/tools/get_product_details.ts +4 -3
  45. package/dist/templates/retail/tools/get_user_details.ts +4 -3
  46. package/dist/templates/retail/tools/list_all_product_types.ts +2 -2
  47. package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -3
  48. package/dist/templates/retail/tools/modify_pending_order_items.ts +6 -6
  49. package/dist/templates/retail/tools/modify_pending_order_payment.ts +6 -6
  50. package/dist/templates/retail/tools/modify_user_address.ts +4 -3
  51. package/dist/templates/retail/tools/return_delivered_order_items.ts +6 -6
  52. package/dist/templates/solo-rpg/agent.test.ts +26 -34
  53. package/dist/templates/solo-rpg/agent.ts +2 -2
  54. package/dist/templates/solo-rpg/client.tsx +1 -1
  55. package/dist/templates/solo-rpg/shared.ts +6 -14
  56. package/dist/templates/solo-rpg/tools/action_roll.ts +2 -2
  57. package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -2
  58. package/dist/templates/solo-rpg/tools/check_state.ts +2 -2
  59. package/dist/templates/solo-rpg/tools/load_game.ts +2 -2
  60. package/dist/templates/solo-rpg/tools/oracle.ts +3 -3
  61. package/dist/templates/solo-rpg/tools/save_game.ts +2 -2
  62. package/dist/templates/solo-rpg/tools/setup_character.ts +2 -2
  63. package/dist/templates/solo-rpg/tools/update_state.ts +12 -11
  64. package/package.json +3 -3
@@ -12,15 +12,15 @@
12
12
  "publish:agent": "aai publish"
13
13
  },
14
14
  "dependencies": {
15
- "@alexkroman1/aai": "^5.13.2",
16
- "@alexkroman1/aai-ui": "^5.13.2",
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.13.2",
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 { getState } from "./shared.ts";
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
- let sessionCounter = 0;
12
-
13
- /** ToolContext stub the dispatch board lives in ctx.state, one per session. */
14
- function makeCtx(): { ctx: ToolContext } {
15
- const sessionId = `test-session-${++sessionCounter}`;
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 = getState(ctx);
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 = getState(ctx);
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 = getState(ctx);
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>() ?? EMPTY_DASH;
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
- <div className="dc-messages flex-1 overflow-y-auto p-4 flex flex-col gap-2">
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
- <div ref={messagesEndRef} />
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 { ToolContext } from "@alexkroman1/aai";
4
- import { createKeyedLock, withLock } from "@alexkroman1/aai";
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
- export function dashboardView(state: StateSlot): DashboardView {
116
- const dispatch = state.dispatch;
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: dispatch?.alertLevel ?? "green",
119
- incidents: Object.values(dispatch?.incidents ?? {}).map(incidentSummary),
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
- /** The session's live dispatch state. Mutations to the returned object
171
- * stick it is the object stored in `ctx.state`. */
172
- export function getState(ctx: ToolContext): DispatchState {
173
- const slot = ctx.state as StateSlot;
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
- * Serialized update of the session's dispatch state.
189
+ * The session's dispatch board, as one typed slot inside `ctx.state`.
206
190
  *
207
- * The LLM loop executes parallel tool calls concurrently. The state lives in
208
- * `ctx.state` now (one shared object, no snapshot/save round-trip), but a
209
- * mutator may be async, and two interleaving async mutators can each observe
210
- * the other's half-applied changes. Holding the session's key makes each
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
- * `createKeyedLock` rather than a hand-rolled promise chain per session: the
216
- * SDK's drops each key's entry once its chain drains (so a long-running agent
217
- * does not accumulate one per session id) and does it by ownership, which is
218
- * the part a hand-rolled chain gets wrong.
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 function updateState<R>(
224
- ctx: ToolContext,
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
- return result;
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.push({ time, event });
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): { error: string } | null {
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, updateState } from "../shared.ts";
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 updateState(ctx, (state) => {
13
+ return dispatchSlot.update(ctx, (state) => {
14
14
  const inc = findIncident(state, args.incidentId);
15
- if ("error" in inc) return inc;
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 updateState(ctx, (state) => {
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 updateState(ctx, (state) => {
24
+ return dispatchSlot.update(ctx, (state) => {
25
25
  const inc = findIncident(state, args.incidentId);
26
- if ("error" in inc) return inc;
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 { findIncident, getApplicableProtocols, getState, incidentAgeMinutes } from "../shared.ts";
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 = getState(ctx);
16
+ const state = dispatchSlot.get(ctx);
12
17
  const inc = findIncident(state, args.incidentId);
13
- if ("error" in inc) return inc;
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 updateState(ctx, (state) => {
31
+ return dispatchSlot.update(ctx, (state) => {
32
32
  const inc = findIncident(state, args.incidentId);
33
- if ("error" in inc) return inc;
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, updateState } from "../shared.ts";
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 updateState(ctx, (state) => {
20
+ return dispatchSlot.update(ctx, (state) => {
21
21
  const inc = findIncident(state, args.incidentId);
22
- if ("error" in inc) return inc;
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 { getState, incidentAgeMinutes, resourceBrief, resourceUtilization } from "../shared.ts";
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 = getState(ctx);
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, updateState } from "../shared.ts";
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 updateState(ctx, (state) => {
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 updateState(ctx, (state) => {
34
+ return dispatchSlot.update(ctx, (state) => {
35
35
  const inc = findIncident(state, args.incidentId);
36
- if ("error" in inc) return inc;
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 { getState, RESOURCE_TYPES } from "../shared.ts";
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 = getState(ctx);
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, updateState } from "../shared.ts";
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 updateState(ctx, (state) => {
13
+ return dispatchSlot.update(ctx, (state) => {
14
14
  const resource = state.resources.find(
15
15
  (r) => r.callsign.toLowerCase() === args.callsign.toLowerCase(),
16
16
  );