@alexkroman1/aai-cli 5.14.0 → 6.1.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 (231) hide show
  1. package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
  2. package/dist/_agent.d.ts +18 -0
  3. package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
  4. package/dist/_api-client.d.ts +39 -0
  5. package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
  6. package/dist/_bundler.d.ts +43 -5
  7. package/dist/_cli-common.d.ts +67 -3
  8. package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
  9. package/dist/_deploy.d.ts +2 -5
  10. package/dist/_dev-restart.d.ts +6 -1
  11. package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
  12. package/dist/_dev-server.d.ts +30 -2
  13. package/dist/_e2e-test-utils.d.ts +80 -0
  14. package/dist/_fault-mode.d.ts +175 -0
  15. package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
  16. package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
  17. package/dist/_output.d.ts +9 -2
  18. package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
  19. package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
  20. package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
  21. package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
  22. package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
  23. package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
  24. package/dist/_utils.d.ts +18 -2
  25. package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
  26. package/dist/_vite-env.d.ts +30 -0
  27. package/dist/build-BO1Ni6oJ.mjs +62 -0
  28. package/dist/build.d.ts +19 -0
  29. package/dist/cli-workflow.d.ts +10 -0
  30. package/dist/cli.d.ts +1 -2
  31. package/dist/cli.mjs +406 -224
  32. package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
  33. package/dist/client-bundler.mjs +1 -1
  34. package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
  35. package/dist/delete.d.ts +2 -8
  36. package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
  37. package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
  38. package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
  39. package/dist/eject.d.ts +30 -3
  40. package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
  41. package/dist/init.d.ts +11 -0
  42. package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
  43. package/dist/project-config.mjs +1 -1
  44. package/dist/scaffold/CLAUDE.md +498 -91
  45. package/dist/scaffold/package.json +11 -8
  46. package/dist/scaffold/pnpm-workspace.yaml +8 -0
  47. package/dist/scaffold/server.mjs +53 -57
  48. package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
  49. package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
  50. package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
  51. package/dist/templates/code-interpreter/agent.ts +0 -2
  52. package/dist/templates/dispatch-center/agent.test.ts +17 -19
  53. package/dist/templates/dispatch-center/agent.ts +2 -29
  54. package/dist/templates/dispatch-center/client.tsx +33 -16
  55. package/dist/templates/dispatch-center/shared.ts +53 -19
  56. package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
  57. package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
  58. package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
  59. package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
  60. package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
  61. package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
  62. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
  63. package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
  66. package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
  67. package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
  68. package/dist/templates/embedded-assets/agent.test.ts +83 -0
  69. package/dist/templates/embedded-assets/agent.ts +1 -48
  70. package/dist/templates/embedded-assets/shared.ts +17 -0
  71. package/dist/templates/embedded-assets/system-prompt.md +13 -0
  72. package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
  73. package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
  74. package/dist/templates/health-assistant/agent.test.ts +169 -0
  75. package/dist/templates/health-assistant/agent.ts +1 -166
  76. package/dist/templates/health-assistant/fda.ts +100 -0
  77. package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
  78. package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
  79. package/dist/templates/infocom-adventure/agent.test.ts +205 -0
  80. package/dist/templates/infocom-adventure/agent.ts +3 -106
  81. package/dist/templates/infocom-adventure/client.tsx +28 -12
  82. package/dist/templates/infocom-adventure/shared.ts +28 -11
  83. package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
  84. package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
  85. package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
  86. package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
  87. package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
  88. package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
  89. package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
  90. package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
  91. package/dist/templates/link-digest/agent.test.ts +223 -0
  92. package/dist/templates/link-digest/agent.ts +79 -0
  93. package/dist/templates/link-digest/client.tsx +146 -0
  94. package/dist/templates/link-digest/workflows/digest.ts +248 -0
  95. package/dist/templates/math-buddy/agent.ts +0 -2
  96. package/dist/templates/night-owl/agent.test.ts +80 -0
  97. package/dist/templates/night-owl/agent.ts +1 -68
  98. package/dist/templates/night-owl/system-prompt.md +10 -0
  99. package/dist/templates/night-owl/tools/recommend.ts +64 -0
  100. package/dist/templates/personal-finance/agent.ts +0 -2
  101. package/dist/templates/pizza-ordering/agent.test.ts +52 -47
  102. package/dist/templates/pizza-ordering/agent.ts +9 -158
  103. package/dist/templates/pizza-ordering/shared.ts +35 -12
  104. package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
  105. package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
  106. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
  107. package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
  108. package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
  109. package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
  110. package/dist/templates/plan-and-execute/agent.test.ts +411 -0
  111. package/dist/templates/plan-and-execute/agent.ts +23 -0
  112. package/dist/templates/plan-and-execute/client.tsx +115 -0
  113. package/dist/templates/plan-and-execute/graph.ts +178 -0
  114. package/dist/templates/plan-and-execute/prompts.ts +110 -0
  115. package/dist/templates/plan-and-execute/shared.ts +161 -0
  116. package/dist/templates/plan-and-execute/system-prompt.md +18 -0
  117. package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
  118. package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
  119. package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
  120. package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
  121. package/dist/templates/recap-workflow/agent.test.ts +762 -0
  122. package/dist/templates/recap-workflow/agent.ts +95 -0
  123. package/dist/templates/recap-workflow/shared.ts +43 -0
  124. package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
  125. package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
  126. package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
  127. package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
  128. package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
  129. package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
  130. package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
  131. package/dist/templates/redline/agent.test.ts +222 -0
  132. package/dist/templates/redline/agent.ts +89 -0
  133. package/dist/templates/redline/client.tsx +184 -0
  134. package/dist/templates/redline/workflows/prompts.ts +68 -0
  135. package/dist/templates/redline/workflows/redline.ts +258 -0
  136. package/dist/templates/research-workflow/agent.test.ts +525 -0
  137. package/dist/templates/research-workflow/agent.ts +94 -0
  138. package/dist/templates/research-workflow/shared.ts +35 -0
  139. package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
  140. package/dist/templates/research-workflow/tools/request_research.ts +33 -0
  141. package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
  142. package/dist/templates/research-workflow/tools/research_status.ts +40 -0
  143. package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
  144. package/dist/templates/research-workflow/workflows/research.ts +558 -0
  145. package/dist/templates/retail/agent.test.ts +236 -172
  146. package/dist/templates/retail/agent.ts +5 -42
  147. package/dist/templates/retail/client.tsx +33 -11
  148. package/dist/templates/retail/registry.test.ts +31 -16
  149. package/dist/templates/retail/resolve.ts +28 -64
  150. package/dist/templates/retail/shared.test.ts +4 -3
  151. package/dist/templates/retail/shared.ts +24 -7
  152. package/dist/templates/retail/store.test.ts +12 -10
  153. package/dist/templates/retail/store.ts +69 -30
  154. package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
  155. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
  156. package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
  157. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
  158. package/dist/templates/retail/tools/get_item_details.ts +5 -7
  159. package/dist/templates/retail/tools/get_order_details.ts +5 -7
  160. package/dist/templates/retail/tools/get_product_details.ts +4 -7
  161. package/dist/templates/retail/tools/get_user_details.ts +4 -7
  162. package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
  163. package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
  164. package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
  165. package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
  166. package/dist/templates/retail/tools/modify_user_address.ts +4 -9
  167. package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
  168. package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
  169. package/dist/templates/solo-rpg/agent.test.ts +152 -29
  170. package/dist/templates/solo-rpg/agent.ts +3 -21
  171. package/dist/templates/solo-rpg/client.tsx +19 -6
  172. package/dist/templates/solo-rpg/shared.ts +19 -11
  173. package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
  174. package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
  175. package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
  176. package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
  177. package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
  178. package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
  179. package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
  180. package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
  181. package/dist/templates/support-line/agent.test.ts +343 -0
  182. package/dist/templates/support-line/agent.ts +27 -0
  183. package/dist/templates/support-line/client.tsx +122 -0
  184. package/dist/templates/support-line/graph.ts +224 -0
  185. package/dist/templates/support-line/knowledge.json +65 -0
  186. package/dist/templates/support-line/prompts.ts +95 -0
  187. package/dist/templates/support-line/shared.ts +240 -0
  188. package/dist/templates/support-line/system-prompt.md +17 -0
  189. package/dist/templates/support-line/tools/answer_question.ts +72 -0
  190. package/dist/templates/support-line/tools/list_topics.ts +22 -0
  191. package/dist/templates/support-line/tools/log_ticket.ts +30 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +563 -0
  193. package/dist/templates/transcription-workflow/agent.ts +92 -0
  194. package/dist/templates/transcription-workflow/client.tsx +234 -0
  195. package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
  196. package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
  197. package/dist/templates/travel-concierge/agent.test.ts +338 -0
  198. package/dist/templates/travel-concierge/agent.ts +29 -0
  199. package/dist/templates/travel-concierge/client.tsx +161 -0
  200. package/dist/templates/travel-concierge/routing.ts +138 -0
  201. package/dist/templates/travel-concierge/shared.ts +547 -0
  202. package/dist/templates/travel-concierge/system-prompt.md +24 -0
  203. package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
  204. package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
  205. package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
  206. package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
  207. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
  208. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
  209. package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
  210. package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
  211. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
  212. package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
  213. package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
  214. package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
  215. package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
  216. package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
  217. package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
  218. package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
  219. package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
  220. package/dist/templates/web-researcher/agent.ts +0 -2
  221. package/dist/templates/web-researcher/system-prompt.md +10 -0
  222. package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
  223. package/dist/typecheck.mjs +17 -15
  224. package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
  225. package/dist/worker-bundler.d.ts +14 -0
  226. package/dist/worker-bundler.mjs +1 -108
  227. package/dist/workflow-B-O97AqI.mjs +125 -0
  228. package/dist/workflow-bundler.d.ts +133 -0
  229. package/dist/workflow.d.ts +67 -0
  230. package/package.json +13 -9
  231. package/dist/build-DQJ1ZSm9.mjs +0 -39
@@ -1,8 +1,8 @@
1
- import { isToolFailure, tool } from "@alexkroman1/aai";
1
+ import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
3
  import { assertNotResolved, dispatchSlot, findIncident, logEvent } from "../shared.ts";
4
4
 
5
- export const incidentUpdateStatus = tool({
5
+ export default dispatchSlot.updateTool({
6
6
  description: "Update an incident's status (en_route, on_scene, resolved, escalated).",
7
7
  inputSchema: z.object({
8
8
  incidentId: z.string().max(20).describe("The incident ID"),
@@ -16,53 +16,51 @@ export const incidentUpdateStatus = tool({
16
16
  .describe("Updated casualty numbers")
17
17
  .optional(),
18
18
  }),
19
- async execute(args, ctx) {
20
- return dispatchSlot.update(ctx, (state) => {
21
- const inc = findIncident(state, args.incidentId);
22
- if (isToolFailure(inc)) return inc;
23
- const blocked = assertNotResolved(inc, `set status to ${args.status}`);
24
- if (blocked) return blocked;
19
+ execute(args, state) {
20
+ const inc = findIncident(state, args.incidentId);
21
+ if (isToolFailure(inc)) return inc;
22
+ const blocked = assertNotResolved(inc, `set status to ${args.status}`);
23
+ if (blocked) return blocked;
25
24
 
26
- inc.status = args.status;
27
- logEvent(inc, `Status → ${args.status}${args.notes ? `: ${args.notes}` : ""}`);
25
+ inc.status = args.status;
26
+ logEvent(inc, `Status → ${args.status}${args.notes ? `: ${args.notes}` : ""}`);
28
27
 
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
- }
28
+ if (args.casualtyUpdate) {
29
+ if (args.casualtyUpdate.confirmed !== undefined) {
30
+ inc.casualties.confirmed = args.casualtyUpdate.confirmed;
36
31
  }
32
+ if (args.casualtyUpdate.treated !== undefined) {
33
+ inc.casualties.treated = args.casualtyUpdate.treated;
34
+ }
35
+ }
37
36
 
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);
37
+ // Only touch resources still assigned to THIS incident — a unit that
38
+ // radioed available and was re-dispatched elsewhere belongs to its
39
+ // new incident now.
40
+ const ownResources = state.resources.filter((r) => r.assignedIncident === inc.id);
42
41
 
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");
42
+ if (args.status === "resolved") {
43
+ for (const r of ownResources) {
44
+ r.status = "returning";
45
+ r.assignedIncident = null;
46
+ r.eta = null;
51
47
  }
48
+ inc.assignedResources = [];
49
+ logEvent(inc, "All resources released — incident closed");
50
+ }
52
51
 
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
- }
52
+ if (args.status === "en_route" || args.status === "on_scene") {
53
+ for (const r of ownResources) {
54
+ r.status = args.status;
55
+ if (args.status === "on_scene") r.eta = null;
58
56
  }
57
+ }
59
58
 
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
- });
59
+ return {
60
+ incidentId: args.incidentId,
61
+ newStatus: args.status,
62
+ timeline: inc.timeline.slice(-5).map((t) => t.event),
63
+ casualties: inc.casualties,
64
+ };
67
65
  },
68
66
  });
@@ -1,12 +1,9 @@
1
- import { tool } from "@alexkroman1/aai";
2
1
  import { dispatchSlot, incidentAgeMinutes, resourceBrief, resourceUtilization } from "../shared.ts";
3
2
 
4
- export const opsDashboard = tool({
3
+ export default dispatchSlot.tool({
5
4
  description:
6
5
  "Get the full operational dashboard: alert level, resource utilization, active incidents, and available resources.",
7
- async execute(_args, ctx) {
8
- const state = dispatchSlot.get(ctx);
9
-
6
+ execute(_args, state) {
10
7
  const activeIncidents = Object.values(state.incidents)
11
8
  .filter((i) => i.status !== "resolved")
12
9
  .sort((a, b) => b.triageScore - a.triageScore);
@@ -2,7 +2,7 @@ import { tool } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
3
  import { getApplicableProtocols, INCIDENT_TYPES, SEVERITIES } from "../shared.ts";
4
4
 
5
- export const opsProtocols = tool({
5
+ export default tool({
6
6
  description: "Look up step-by-step response protocols for a given incident type and severity.",
7
7
  inputSchema: z.object({
8
8
  incidentType: z.enum(INCIDENT_TYPES).describe("Type of incident"),
@@ -1,4 +1,3 @@
1
- import { tool } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
2
  import type { Incident, IncidentType, Severity } from "../shared.ts";
4
3
  import { calculateTriageScore, createIncident, dispatchSlot } from "../shared.ts";
@@ -117,33 +116,31 @@ const scenarios = {
117
116
  type ScenarioName = keyof typeof scenarios;
118
117
  const SCENARIO_NAMES = Object.keys(scenarios) as [ScenarioName, ...ScenarioName[]];
119
118
 
120
- export const opsRunScenario = tool({
119
+ export default dispatchSlot.updateTool({
121
120
  description: "Run a training scenario that creates simulated incidents for dispatch practice.",
122
121
  inputSchema: z.object({
123
122
  scenario: z.enum(SCENARIO_NAMES).describe("Scenario type to simulate"),
124
123
  }),
125
- async execute(args, ctx) {
126
- return dispatchSlot.update(ctx, (state) => {
127
- const s = scenarios[args.scenario];
124
+ execute(args, state) {
125
+ const s = scenarios[args.scenario];
128
126
 
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
- }
127
+ const created: string[] = [];
128
+ for (const scenarioInc of s.incidents) {
129
+ const fullInc = createIncident(state, {
130
+ ...scenarioInc,
131
+ callerName: "Scenario",
132
+ callerPhone: "N/A",
133
+ triageScore: calculateTriageScore(scenarioInc.severity, scenarioInc.type, 0, 0),
134
+ timeline: [{ time: Date.now(), event: `SCENARIO: ${scenarioInc.description}` }],
135
+ });
136
+ created.push(fullInc.id);
137
+ }
140
138
 
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
- });
139
+ return {
140
+ scenario: args.scenario,
141
+ narrative: s.narrative,
142
+ incidentsCreated: created,
143
+ message: `SCENARIO ACTIVE: ${s.narrative}. ${created.length} incidents created. Awaiting dispatch orders.`,
144
+ };
148
145
  },
149
146
  });
@@ -1,4 +1,4 @@
1
- import { isToolFailure, tool } from "@alexkroman1/aai";
1
+ import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
3
  import type { Resource } from "../shared.ts";
4
4
  import {
@@ -9,7 +9,7 @@ import {
9
9
  recommendResources,
10
10
  } from "../shared.ts";
11
11
 
12
- export const resourcesDispatch = tool({
12
+ export default dispatchSlot.updateTool({
13
13
  description:
14
14
  "Dispatch units to an incident. Can auto-dispatch recommended resources or manually specify callsigns.",
15
15
  inputSchema: z.object({
@@ -30,68 +30,66 @@ export const resourcesDispatch = tool({
30
30
  .describe("Dispatch priority — affects simulated ETA")
31
31
  .optional(),
32
32
  }),
33
- async execute(args, ctx) {
34
- return dispatchSlot.update(ctx, (state) => {
35
- const inc = findIncident(state, args.incidentId);
36
- if (isToolFailure(inc)) return inc;
37
- const blocked = assertNotResolved(inc, "dispatch resources");
38
- if (blocked) return blocked;
33
+ execute(args, state) {
34
+ const inc = findIncident(state, args.incidentId);
35
+ if (isToolFailure(inc)) return inc;
36
+ const blocked = assertNotResolved(inc, "dispatch resources");
37
+ if (blocked) return blocked;
39
38
 
40
- const dispatched: { callsign: string; type: string; eta: number }[] = [];
41
- const failed: { callsign: string; reason: string }[] = [];
39
+ const dispatched: { callsign: string; type: string; eta: number }[] = [];
40
+ const failed: { callsign: string; reason: string }[] = [];
42
41
 
43
- // The literal "auto" callsign means the same thing as autoDispatch.
44
- const wantsAuto =
45
- args.autoDispatch || args.callsigns?.some((cs) => cs.toLowerCase() === "auto");
42
+ // The literal "auto" callsign means the same thing as autoDispatch.
43
+ const wantsAuto =
44
+ args.autoDispatch || args.callsigns?.some((cs) => cs.toLowerCase() === "auto");
46
45
 
47
- let resourcesToDispatch: Resource[] = [];
46
+ let resourcesToDispatch: Resource[] = [];
48
47
 
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);
48
+ if (wantsAuto) {
49
+ resourcesToDispatch = recommendResources(inc.type, inc.severity, state);
50
+ } else if (args.callsigns) {
51
+ for (const cs of args.callsigns) {
52
+ const r = state.resources.find((r) => r.callsign.toLowerCase() === cs.toLowerCase());
53
+ if (!r) {
54
+ failed.push({ callsign: cs, reason: "Not found" });
55
+ continue;
63
56
  }
57
+ if (r.status !== "available") {
58
+ failed.push({ callsign: cs, reason: `Currently ${r.status}` });
59
+ continue;
60
+ }
61
+ resourcesToDispatch.push(r);
64
62
  }
63
+ }
65
64
 
66
- const etaBase = args.priority === "emergency" ? 3 : args.priority === "priority" ? 6 : 10;
65
+ const etaBase = args.priority === "emergency" ? 3 : args.priority === "priority" ? 6 : 10;
67
66
 
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
- }
67
+ for (const r of resourcesToDispatch) {
68
+ const eta = etaBase + Math.floor(Math.random() * 5);
69
+ r.status = "dispatched";
70
+ r.assignedIncident = args.incidentId;
71
+ r.eta = eta;
72
+ inc.assignedResources.push(r.id);
73
+ dispatched.push({ callsign: r.callsign, type: r.type, eta });
74
+ logEvent(inc, `Dispatched ${r.callsign} — ETA ${eta} min`);
75
+ }
77
76
 
78
- if (dispatched.length > 0) {
79
- inc.status = "dispatched";
80
- }
77
+ if (dispatched.length > 0) {
78
+ inc.status = "dispatched";
79
+ }
81
80
 
82
- const availableCount = state.resources.filter((r) => r.status === "available").length;
81
+ const availableCount = state.resources.filter((r) => r.status === "available").length;
83
82
 
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
- });
83
+ return {
84
+ incidentId: args.incidentId,
85
+ dispatched,
86
+ failed: failed.length > 0 ? failed : undefined,
87
+ totalAssignedToIncident: inc.assignedResources.length,
88
+ remainingAvailableResources: availableCount,
89
+ capacityWarning:
90
+ availableCount <= 3
91
+ ? "WARNING: Resource capacity critically low. Consider mutual aid."
92
+ : undefined,
93
+ };
96
94
  },
97
95
  });
@@ -1,8 +1,7 @@
1
- import { tool } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
2
  import { dispatchSlot, RESOURCE_TYPES } from "../shared.ts";
4
3
 
5
- export const resourcesGetAvailable = tool({
4
+ export default dispatchSlot.tool({
6
5
  description: "List available resources, optionally filtered by type.",
7
6
  inputSchema: z.object({
8
7
  type: z
@@ -10,8 +9,7 @@ export const resourcesGetAvailable = tool({
10
9
  .describe("Filter by resource type, or 'all'")
11
10
  .optional(),
12
11
  }),
13
- async execute(args, ctx) {
14
- const state = dispatchSlot.get(ctx);
12
+ execute(args, state) {
15
13
  let resources = state.resources;
16
14
  if (args.type && args.type !== "all") {
17
15
  resources = resources.filter((r) => r.type === args.type);
@@ -1,62 +1,59 @@
1
- import { tool } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
2
  import { dispatchSlot, logEvent, RESOURCE_STATUSES } from "../shared.ts";
4
3
 
5
- export const resourcesUpdateStatus = tool({
4
+ export default dispatchSlot.updateTool({
6
5
  description: "Update a resource unit's status when it radios in.",
7
6
  inputSchema: z.object({
8
7
  callsign: z.string().max(50).describe("The resource callsign"),
9
8
  status: z.enum(RESOURCE_STATUSES).describe("New status"),
10
9
  notes: z.string().max(1000).describe("Status notes").optional(),
11
10
  }),
12
- async execute(args, ctx) {
13
- return dispatchSlot.update(ctx, (state) => {
14
- const resource = state.resources.find(
15
- (r) => r.callsign.toLowerCase() === args.callsign.toLowerCase(),
16
- );
17
- if (!resource) {
18
- return { error: `Resource ${args.callsign} not found` };
11
+ execute(args, state) {
12
+ const resource = state.resources.find(
13
+ (r) => r.callsign.toLowerCase() === args.callsign.toLowerCase(),
14
+ );
15
+ if (!resource) {
16
+ return { error: `Resource ${args.callsign} not found` };
17
+ }
18
+
19
+ const previousStatus = resource.status;
20
+
21
+ // Log to the incident timeline BEFORE unassigning, so the
22
+ // return-to-available transition is recorded too.
23
+ if (resource.assignedIncident) {
24
+ const inc = state.incidents[resource.assignedIncident];
25
+ if (inc) {
26
+ logEvent(
27
+ inc,
28
+ `${resource.callsign}: ${previousStatus} → ${args.status}${args.notes ? ` (${args.notes})` : ""}`,
29
+ );
19
30
  }
31
+ }
20
32
 
21
- const previousStatus = resource.status;
33
+ resource.status = args.status;
22
34
 
23
- // Log to the incident timeline BEFORE unassigning, so the
24
- // return-to-available transition is recorded too.
35
+ if (args.status === "available") {
36
+ // Detach from the old incident on BOTH sides — leaving the id in
37
+ // assignedResources lets that incident later yank a unit that has
38
+ // been re-dispatched elsewhere.
25
39
  if (resource.assignedIncident) {
26
40
  const inc = state.incidents[resource.assignedIncident];
27
41
  if (inc) {
28
- logEvent(
29
- inc,
30
- `${resource.callsign}: ${previousStatus} → ${args.status}${args.notes ? ` (${args.notes})` : ""}`,
31
- );
42
+ inc.assignedResources = inc.assignedResources.filter((id) => id !== resource.id);
32
43
  }
33
44
  }
45
+ resource.assignedIncident = null;
46
+ resource.eta = null;
47
+ }
48
+ if (args.status === "on_scene") {
49
+ resource.eta = null;
50
+ }
34
51
 
35
- resource.status = args.status;
36
-
37
- if (args.status === "available") {
38
- // Detach from the old incident on BOTH sides — leaving the id in
39
- // assignedResources lets that incident later yank a unit that has
40
- // been re-dispatched elsewhere.
41
- if (resource.assignedIncident) {
42
- const inc = state.incidents[resource.assignedIncident];
43
- if (inc) {
44
- inc.assignedResources = inc.assignedResources.filter((id) => id !== resource.id);
45
- }
46
- }
47
- resource.assignedIncident = null;
48
- resource.eta = null;
49
- }
50
- if (args.status === "on_scene") {
51
- resource.eta = null;
52
- }
53
-
54
- return {
55
- callsign: resource.callsign,
56
- previousStatus,
57
- newStatus: args.status,
58
- assignedIncident: resource.assignedIncident,
59
- };
60
- });
52
+ return {
53
+ callsign: resource.callsign,
54
+ previousStatus,
55
+ newStatus: args.status,
56
+ assignedIncident: resource.assignedIncident,
57
+ };
61
58
  },
62
59
  });
@@ -0,0 +1,83 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import type { ToolContext } from "@alexkroman1/aai";
4
+ import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
5
+ import { describe, expect, test } from "vitest";
6
+ import authoredAgent from "./agent.ts";
7
+ import { faqs, searchable } from "./shared.ts";
8
+
9
+ /**
10
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
11
+ *
12
+ * The glob is written HERE rather than reached for from a shared helper because
13
+ * this file SHIPS — a scaffolded project has no `../../_discovery.ts` to import,
14
+ * and `import.meta.glob` is expanded against the file containing it either way.
15
+ */
16
+ const agentDef = withDiscoveredTools(
17
+ authoredAgent,
18
+ import.meta.glob("./tools/*.ts", { eager: true }),
19
+ );
20
+
21
+ const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
22
+ runTool(agentDef, name, args, ctx);
23
+
24
+ describe("embedded-assets template", () => {
25
+ test("the JSON asset really is bundled, and the index is built from it", () => {
26
+ // The template's whole subject: an asset imported with an import attribute,
27
+ // resolved at build time rather than fetched. An empty knowledge base would
28
+ // make every search below trivially "not found".
29
+ expect(faqs.length).toBeGreaterThan(0);
30
+ expect(searchable).toHaveLength(faqs.length);
31
+ for (const entry of faqs) {
32
+ expect(entry.question).toBeTypeOf("string");
33
+ expect(entry.answer).toBeTypeOf("string");
34
+ }
35
+ });
36
+
37
+ test("both tools are discovered from tools/", () => {
38
+ // `agent()` takes no `tools` field: a file in `tools/` IS the tool, and
39
+ // nothing imports it. Discovery is what puts it in front of the model, so a
40
+ // template whose tools are never resolved ships a model with no tools.
41
+ expect(Object.keys(agentDef.tools ?? {}).sort()).toEqual(["list_topics", "search_knowledge"]);
42
+ });
43
+ });
44
+
45
+ describe("list_topics", () => {
46
+ test("answers with every question in the knowledge base", async () => {
47
+ const topics = await run("list_topics", {}, createToolContext());
48
+ expect(topics).toEqual(faqs.map((f) => f.question));
49
+ });
50
+ });
51
+
52
+ describe("search_knowledge", () => {
53
+ test("scores by word overlap rather than by substring", async () => {
54
+ // The reason the tool does not just `includes()` the query: a caller asks a
55
+ // natural question, which is never a substring of an FAQ entry.
56
+ const target = faqs[0];
57
+ if (!target) throw new Error("the knowledge base is empty");
58
+ const words = target.question
59
+ .toLowerCase()
60
+ .split(/\W+/)
61
+ .filter((w) => w.length > 2);
62
+ const result = await run(
63
+ "search_knowledge",
64
+ { query: `hey, could you tell me ${words.join(" ")}?` },
65
+ createToolContext(),
66
+ );
67
+ expect(result).toEqual(target);
68
+ });
69
+
70
+ test("a query with no words longer than two characters is refused early", async () => {
71
+ // The guard before the scan: with no scoreable words every entry ties at
72
+ // zero, and returning the first one would be an answer to nothing.
73
+ expect(await run("search_knowledge", { query: "is it a" }, createToolContext())).toEqual({
74
+ result: "No matching FAQ found.",
75
+ });
76
+ });
77
+
78
+ test("a query that overlaps nothing reports no match rather than guessing", async () => {
79
+ expect(
80
+ await run("search_knowledge", { query: "zzzqqq wibbleflange" }, createToolContext()),
81
+ ).toEqual({ result: "No matching FAQ found." });
82
+ });
83
+ });
@@ -1,54 +1,7 @@
1
- import { agent, tool } from "@alexkroman1/aai";
2
- import { z } from "zod";
3
- import knowledge from "./knowledge.json" with { type: "json" };
4
-
5
- type FaqEntry = { question: string; answer: string };
6
- const faqs: FaqEntry[] = knowledge.faqs;
7
-
8
- // Search text is static, so it's normalized once at module load — per-call
9
- // work stays O(query) even when the knowledge base grows.
10
- const searchable = faqs.map((entry) => ({
11
- entry,
12
- text: `${entry.question} ${entry.answer}`.toLowerCase(),
13
- }));
1
+ import { agent } from "@alexkroman1/aai";
14
2
 
15
3
  export default agent({
16
4
  name: "FAQ Bot",
17
- systemPrompt:
18
- "You are a friendly FAQ assistant. Answer questions using ONLY the information from your embedded knowledge base. If the user asks something not covered by your knowledge base, say you don't have that information and suggest they check the official documentation.\n\nRules:\n- Keep answers concise and conversational — this is a voice agent\n- Quote the knowledge base accurately, do not embellish\n- If a question is ambiguous, ask the user to clarify\n- Use 'search_knowledge' to find answers to specific questions\n- Use 'list_topics' to see all available FAQ topics\n- Always be helpful and polite",
19
5
  greeting:
20
6
  "Hi! I'm your FAQ assistant. Ask me anything about the AAI agent framework and I'll look it up in my knowledge base.",
21
-
22
- tools: {
23
- list_topics: tool({
24
- description: "List all available topics in the embedded FAQ knowledge base.",
25
- async execute() {
26
- return faqs.map((f) => f.question);
27
- },
28
- }),
29
-
30
- search_knowledge: tool({
31
- description:
32
- "Search the embedded FAQ knowledge base for an answer matching the user's question.",
33
- inputSchema: z.object({
34
- query: z.string().describe("The user's question to search for"),
35
- }),
36
- async execute(args) {
37
- // Score by word overlap — natural questions rarely match an FAQ
38
- // entry as an exact substring.
39
- const words = args.query
40
- .toLowerCase()
41
- .split(/\W+/)
42
- .filter((w) => w.length > 2);
43
- if (words.length === 0) return { result: "No matching FAQ found." };
44
-
45
- let best: { entry: FaqEntry; score: number } | null = null;
46
- for (const { entry, text } of searchable) {
47
- const score = words.filter((w) => text.includes(w)).length;
48
- if (score > 0 && (!best || score > best.score)) best = { entry, score };
49
- }
50
- return best?.entry ?? { result: "No matching FAQ found." };
51
- },
52
- }),
53
- },
54
7
  });
@@ -0,0 +1,17 @@
1
+ import knowledge from "./knowledge.json" with { type: "json" };
2
+
3
+ export type FaqEntry = { question: string; answer: string };
4
+
5
+ export const faqs: FaqEntry[] = knowledge.faqs;
6
+
7
+ /**
8
+ * The searchable index, built once at module load.
9
+ *
10
+ * Search text is static, so normalizing it here keeps per-call work O(query)
11
+ * even when the knowledge base grows — and both tools import this module, so
12
+ * the index is built once for the process rather than once per tool.
13
+ */
14
+ export const searchable = faqs.map((entry) => ({
15
+ entry,
16
+ text: `${entry.question} ${entry.answer}`.toLowerCase(),
17
+ }));
@@ -0,0 +1,13 @@
1
+ You are a friendly FAQ assistant. Answer questions using ONLY the information
2
+ from your embedded knowledge base. If the user asks something not covered by
3
+ your knowledge base, say you don't have that information and suggest they check
4
+ the official documentation.
5
+
6
+ Rules:
7
+
8
+ - Keep answers concise and conversational — this is a voice agent
9
+ - Quote the knowledge base accurately, do not embellish
10
+ - If a question is ambiguous, ask the user to clarify
11
+ - Use 'search_knowledge' to find answers to specific questions
12
+ - Use 'list_topics' to see all available FAQ topics
13
+ - Always be helpful and polite
@@ -0,0 +1,9 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { faqs } from "../shared.ts";
3
+
4
+ export default tool({
5
+ description: "List all available topics in the embedded FAQ knowledge base.",
6
+ async execute() {
7
+ return faqs.map((f) => f.question);
8
+ },
9
+ });
@@ -0,0 +1,26 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { type FaqEntry, searchable } from "../shared.ts";
4
+
5
+ export default tool({
6
+ description: "Search the embedded FAQ knowledge base for an answer matching the user's question.",
7
+ inputSchema: z.object({
8
+ query: z.string().describe("The user's question to search for"),
9
+ }),
10
+ async execute(args) {
11
+ // Score by word overlap — natural questions rarely match an FAQ
12
+ // entry as an exact substring.
13
+ const words = args.query
14
+ .toLowerCase()
15
+ .split(/\W+/)
16
+ .filter((w) => w.length > 2);
17
+ if (words.length === 0) return { result: "No matching FAQ found." };
18
+
19
+ let best: { entry: FaqEntry; score: number } | null = null;
20
+ for (const { entry, text } of searchable) {
21
+ const score = words.filter((w) => text.includes(w)).length;
22
+ if (score > 0 && (!best || score > best.score)) best = { entry, score };
23
+ }
24
+ return best?.entry ?? { result: "No matching FAQ found." };
25
+ },
26
+ });